import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; import '../model/document/node/leaf.dart' as leaf; import '../model/document/attribute.dart'; import '../model/document/node/line.dart'; import '../model/document/node/node.dart'; import '../rendering/text_line.dart'; import '../widget/proxy.dart'; import '../service/style.dart'; import '../service/cursor.dart'; import '../util/color.dart'; /* --------------------------------- Widget --------------------------------- */ class TextLine extends StatelessWidget { const TextLine({ required this.line, required this.embedBuilder, required this.styles, this.textDirection, Key? key, }) : super(key: key); final Line line; final TextDirection? textDirection; final EmbedBuilderFuncion embedBuilder; final DefaultStyles styles; @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); if (line.hasEmbed) { final embed = line.children.single as leaf.Embed; return EmbedProxy(embedBuilder(context, embed)); } final textSpan = _buildTextSpan(context); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final textAlign = _getTextAlign(); final child = RichText( text: textSpan, textAlign: textAlign, textDirection: textDirection, strutStyle: strutStyle, textScaleFactor: MediaQuery.textScaleFactorOf(context), ); return RichTextProxy( child, textSpan.style!, textAlign, textDirection!, 1, Localizations.localeOf(context), strutStyle, TextWidthBasis.parent, null, ); } // TextStyle Decode TextAlign _getTextAlign() { final alignment = line.style.attributes[Attribute.align.key]; if (Attribute.leftAlignment == alignment) { return TextAlign.left; } else if (Attribute.centerAlignment == alignment) { return TextAlign.center; } else if (Attribute.rightAlignment == alignment) { return TextAlign.right; } else if (Attribute.justifyAlignment == alignment) { return TextAlign.justify; } return TextAlign.start; } // Span Util TextSpan _buildTextSpan(BuildContext context) { final defaultStyles = styles; final children = line.children .map((child) => _getTextSpanFromNode(defaultStyles, child)) .toList(growable: false); var textStyle = const TextStyle(); // Placeholder if (line.style.containsKey(Attribute.placeholder.key)) { textStyle = defaultStyles.placeHolder!.style; return TextSpan(children: children, style: textStyle); } // Header final header = line.style.attributes[Attribute.header.key]; final headerStyles = { Attribute.h1: defaultStyles.h1!.style, Attribute.h2: defaultStyles.h2!.style, Attribute.h3: defaultStyles.h3!.style, Attribute.h4: defaultStyles.h4!.style, Attribute.h5: defaultStyles.h5!.style, Attribute.h6: defaultStyles.h6!.style, }; textStyle = textStyle.merge(headerStyles[header] ?? defaultStyles.paragraph!.style); // Block final block = line.style.getBlockExceptHeader(); TextStyle? blockStyle; if (Attribute.quoteBlock == block) { blockStyle = defaultStyles.quote!.style; } else if (Attribute.codeBlock == block) { blockStyle = defaultStyles.code!.style; } else if (block != null) { blockStyle = defaultStyles.lists!.style; } textStyle = textStyle.merge(blockStyle); return TextSpan(children: children, style: textStyle); } TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { final textNode = node as leaf.Text; final style = textNode.style; // Inline Style final inlineStyles = { Attribute.bold.key: defaultStyles.bold, Attribute.italic.key: defaultStyles.italic, Attribute.link.key: defaultStyles.link, Attribute.underline.key: defaultStyles.underline, Attribute.strikeThrough.key: defaultStyles.strikeThrough, }; var result = const TextStyle(); inlineStyles.forEach((_key, _style) { if (style.values.any((val) => val.key == _key)) { result = _merge(result, _style!); } }); // Font final font = textNode.style.attributes[Attribute.font.key]; if (font != null && font.value != null) { result = result.merge(TextStyle(fontFamily: font.value)); } // Size final size = textNode.style.attributes[Attribute.size.key]; if (size != null && size.value != null) { switch (size.value) { case 'small': result = result.merge(defaultStyles.sizeSmall); break; case 'large': result = result.merge(defaultStyles.sizeLarge); break; case 'huge': result = result.merge(defaultStyles.sizeHuge); break; default: final fontSize = double.tryParse(size.value); if (fontSize != null) { result = result.merge(TextStyle(fontSize: fontSize)); } else { throw 'Invalid size ${size.value}'; } } } // Color final color = textNode.style.attributes[Attribute.color.key]; if (color != null && color.value != null) { var textColor = defaultStyles.color; if (color.value is String) { textColor = stringToColor(color.value); } if (textColor != null) { result = result.merge(TextStyle(color: textColor)); } } // Background final background = textNode.style.attributes[Attribute.background.key]; if (background != null && background.value != null) { final backgroundColor = stringToColor(background.value); result = result.merge(TextStyle(backgroundColor: backgroundColor)); } return TextSpan(text: textNode.value, style: result); } TextStyle _merge(TextStyle style, TextStyle otherStyle) { final decorations = []; if (style.decoration != null) { decorations.add(style.decoration); } if (otherStyle.decoration != null) { decorations.add(otherStyle.decoration); } return style.merge(otherStyle).apply( decoration: TextDecoration.combine( List.castFrom(decorations), ), ); } } /* -------------------------- Render Object Widget -------------------------- */ class EditableTextLine extends RenderObjectWidget { const EditableTextLine( this.line, this.leading, this.body, this.indentWidth, this.verticalSpacing, this.textDirection, this.textSelection, this.color, this.enableInteractiveSelection, this.hasFocus, this.devicePixelRatio, this.cursorController, ); final Line line; final Widget? leading; final Widget body; final double indentWidth; final Tuple2 verticalSpacing; final TextDirection textDirection; final TextSelection textSelection; final Color color; final bool enableInteractiveSelection; final bool hasFocus; final double devicePixelRatio; final CursorController cursorController; @override RenderObjectElement createElement() => TextLineElement(this); @override RenderEditableTextLine createRenderObject(BuildContext context) { return RenderEditableTextLine( line, textDirection, textSelection, enableInteractiveSelection, hasFocus, devicePixelRatio, _computePadding(), color, cursorController, ); } @override void updateRenderObject( BuildContext context, covariant RenderEditableTextLine renderObject) { renderObject ..line = line ..padding = _computePadding() ..textDirection = textDirection ..textSelection = textSelection ..color = color ..enableInteractiveSelection = enableInteractiveSelection ..hasFocus = hasFocus ..devicePixelRatio = devicePixelRatio ..cursorController = cursorController; } EdgeInsetsGeometry _computePadding() { return EdgeInsetsDirectional.only( start: indentWidth, top: verticalSpacing.item1, bottom: verticalSpacing.item2, ); } }