text_line.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import 'package:flutter/material.dart';
  2. import 'package:tuple/tuple.dart';
  3. import '../model/document/node/leaf.dart' as leaf;
  4. import '../model/document/attribute.dart';
  5. import '../model/document/node/line.dart';
  6. import '../model/document/node/node.dart';
  7. import '../rendering/text_line.dart';
  8. import '../widget/proxy.dart';
  9. import '../service/style.dart';
  10. import '../service/cursor.dart';
  11. import '../util/color.dart';
  12. /* --------------------------------- Widget --------------------------------- */
  13. class TextLine extends StatelessWidget {
  14. const TextLine({
  15. required this.line,
  16. required this.embedBuilder,
  17. required this.styles,
  18. this.textDirection,
  19. Key? key,
  20. }) : super(key: key);
  21. final Line line;
  22. final TextDirection? textDirection;
  23. final EmbedBuilderFuncion embedBuilder;
  24. final DefaultStyles styles;
  25. @override
  26. Widget build(BuildContext context) {
  27. assert(debugCheckHasMediaQuery(context));
  28. if (line.hasEmbed) {
  29. final embed = line.children.single as leaf.Embed;
  30. return EmbedProxy(embedBuilder(context, embed));
  31. }
  32. final textSpan = _buildTextSpan(context);
  33. final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
  34. final textAlign = _getTextAlign();
  35. final child = RichText(
  36. text: textSpan,
  37. textAlign: textAlign,
  38. textDirection: textDirection,
  39. strutStyle: strutStyle,
  40. textScaleFactor: MediaQuery.textScaleFactorOf(context),
  41. );
  42. return RichTextProxy(
  43. child,
  44. textSpan.style!,
  45. textAlign,
  46. textDirection!,
  47. 1,
  48. Localizations.localeOf(context),
  49. strutStyle,
  50. TextWidthBasis.parent,
  51. null,
  52. );
  53. }
  54. // TextStyle Decode
  55. TextAlign _getTextAlign() {
  56. final alignment = line.style.attributes[Attribute.align.key];
  57. if (Attribute.leftAlignment == alignment) {
  58. return TextAlign.left;
  59. } else if (Attribute.centerAlignment == alignment) {
  60. return TextAlign.center;
  61. } else if (Attribute.rightAlignment == alignment) {
  62. return TextAlign.right;
  63. } else if (Attribute.justifyAlignment == alignment) {
  64. return TextAlign.justify;
  65. }
  66. return TextAlign.start;
  67. }
  68. // Span Util
  69. TextSpan _buildTextSpan(BuildContext context) {
  70. final defaultStyles = styles;
  71. final children = line.children
  72. .map((child) => _getTextSpanFromNode(defaultStyles, child))
  73. .toList(growable: false);
  74. var textStyle = const TextStyle();
  75. // Placeholder
  76. if (line.style.containsKey(Attribute.placeholder.key)) {
  77. textStyle = defaultStyles.placeHolder!.style;
  78. return TextSpan(children: children, style: textStyle);
  79. }
  80. // Header
  81. final header = line.style.attributes[Attribute.header.key];
  82. final headerStyles = <Attribute, TextStyle>{
  83. Attribute.h1: defaultStyles.h1!.style,
  84. Attribute.h2: defaultStyles.h2!.style,
  85. Attribute.h3: defaultStyles.h3!.style,
  86. Attribute.h4: defaultStyles.h4!.style,
  87. Attribute.h5: defaultStyles.h5!.style,
  88. Attribute.h6: defaultStyles.h6!.style,
  89. };
  90. textStyle =
  91. textStyle.merge(headerStyles[header] ?? defaultStyles.paragraph!.style);
  92. // Block
  93. final block = line.style.getBlockExceptHeader();
  94. TextStyle? blockStyle;
  95. if (Attribute.quoteBlock == block) {
  96. blockStyle = defaultStyles.quote!.style;
  97. } else if (Attribute.codeBlock == block) {
  98. blockStyle = defaultStyles.code!.style;
  99. } else if (block != null) {
  100. blockStyle = defaultStyles.lists!.style;
  101. }
  102. textStyle = textStyle.merge(blockStyle);
  103. return TextSpan(children: children, style: textStyle);
  104. }
  105. TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) {
  106. final textNode = node as leaf.Text;
  107. final style = textNode.style;
  108. // Inline Style
  109. final inlineStyles = <String, TextStyle?>{
  110. Attribute.bold.key: defaultStyles.bold,
  111. Attribute.italic.key: defaultStyles.italic,
  112. Attribute.link.key: defaultStyles.link,
  113. Attribute.underline.key: defaultStyles.underline,
  114. Attribute.strikeThrough.key: defaultStyles.strikeThrough,
  115. };
  116. var result = const TextStyle();
  117. inlineStyles.forEach((_key, _style) {
  118. if (style.values.any((val) => val.key == _key)) {
  119. result = _merge(result, _style!);
  120. }
  121. });
  122. // Font
  123. final font = textNode.style.attributes[Attribute.font.key];
  124. if (font != null && font.value != null) {
  125. result = result.merge(TextStyle(fontFamily: font.value));
  126. }
  127. // Size
  128. final size = textNode.style.attributes[Attribute.size.key];
  129. if (size != null && size.value != null) {
  130. switch (size.value) {
  131. case 'small':
  132. result = result.merge(defaultStyles.sizeSmall);
  133. break;
  134. case 'large':
  135. result = result.merge(defaultStyles.sizeLarge);
  136. break;
  137. case 'huge':
  138. result = result.merge(defaultStyles.sizeHuge);
  139. break;
  140. default:
  141. final fontSize = double.tryParse(size.value);
  142. if (fontSize != null) {
  143. result = result.merge(TextStyle(fontSize: fontSize));
  144. } else {
  145. throw 'Invalid size ${size.value}';
  146. }
  147. }
  148. }
  149. // Color
  150. final color = textNode.style.attributes[Attribute.color.key];
  151. if (color != null && color.value != null) {
  152. var textColor = defaultStyles.color;
  153. if (color.value is String) {
  154. textColor = stringToColor(color.value);
  155. }
  156. if (textColor != null) {
  157. result = result.merge(TextStyle(color: textColor));
  158. }
  159. }
  160. // Background
  161. final background = textNode.style.attributes[Attribute.background.key];
  162. if (background != null && background.value != null) {
  163. final backgroundColor = stringToColor(background.value);
  164. result = result.merge(TextStyle(backgroundColor: backgroundColor));
  165. }
  166. return TextSpan(text: textNode.value, style: result);
  167. }
  168. TextStyle _merge(TextStyle style, TextStyle otherStyle) {
  169. final decorations = <TextDecoration?>[];
  170. if (style.decoration != null) {
  171. decorations.add(style.decoration);
  172. }
  173. if (otherStyle.decoration != null) {
  174. decorations.add(otherStyle.decoration);
  175. }
  176. return style.merge(otherStyle).apply(
  177. decoration: TextDecoration.combine(
  178. List.castFrom<dynamic, TextDecoration>(decorations),
  179. ),
  180. );
  181. }
  182. }
  183. /* -------------------------- Render Object Widget -------------------------- */
  184. class EditableTextLine extends RenderObjectWidget {
  185. const EditableTextLine(
  186. this.line,
  187. this.leading,
  188. this.body,
  189. this.indentWidth,
  190. this.verticalSpacing,
  191. this.textDirection,
  192. this.textSelection,
  193. this.color,
  194. this.enableInteractiveSelection,
  195. this.hasFocus,
  196. this.devicePixelRatio,
  197. this.cursorController,
  198. );
  199. final Line line;
  200. final Widget? leading;
  201. final Widget body;
  202. final double indentWidth;
  203. final Tuple2 verticalSpacing;
  204. final TextDirection textDirection;
  205. final TextSelection textSelection;
  206. final Color color;
  207. final bool enableInteractiveSelection;
  208. final bool hasFocus;
  209. final double devicePixelRatio;
  210. final CursorController cursorController;
  211. @override
  212. RenderObjectElement createElement() => TextLineElement(this);
  213. @override
  214. RenderEditableTextLine createRenderObject(BuildContext context) {
  215. return RenderEditableTextLine(
  216. line,
  217. textDirection,
  218. textSelection,
  219. enableInteractiveSelection,
  220. hasFocus,
  221. devicePixelRatio,
  222. _computePadding(),
  223. color,
  224. cursorController,
  225. );
  226. }
  227. @override
  228. void updateRenderObject(
  229. BuildContext context, covariant RenderEditableTextLine renderObject) {
  230. renderObject
  231. ..line = line
  232. ..padding = _computePadding()
  233. ..textDirection = textDirection
  234. ..textSelection = textSelection
  235. ..color = color
  236. ..enableInteractiveSelection = enableInteractiveSelection
  237. ..hasFocus = hasFocus
  238. ..devicePixelRatio = devicePixelRatio
  239. ..cursorController = cursorController;
  240. }
  241. EdgeInsetsGeometry _computePadding() {
  242. return EdgeInsetsDirectional.only(
  243. start: indentWidth,
  244. top: verticalSpacing.item1,
  245. bottom: verticalSpacing.item2,
  246. );
  247. }
  248. }