text_block.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import 'package:flutter/material.dart';
  2. import 'package:tuple/tuple.dart';
  3. import '../model/document/node/block.dart';
  4. import '../model/document/node/line.dart';
  5. import '../model/document/attribute.dart';
  6. import '../rendering/text_block.dart';
  7. import '../service/cursor.dart';
  8. import '../service/style.dart';
  9. import '../widget/text_line.dart';
  10. import '../widget/proxy.dart';
  11. /* --------------------------------- Widget --------------------------------- */
  12. class EditableTextBlock extends StatelessWidget {
  13. const EditableTextBlock(
  14. this.block,
  15. this.textDirection,
  16. this.textSelection,
  17. this.scrollBottomInset,
  18. this.verticalSpacing,
  19. this.color,
  20. this.styles,
  21. this.enableInteractiveSelection,
  22. this.hasFocus,
  23. this.contentPadding,
  24. this.embedBuilder,
  25. this.cursorController,
  26. this.indentLevelCounts,
  27. );
  28. final Block block;
  29. final TextDirection textDirection;
  30. final TextSelection textSelection;
  31. final double scrollBottomInset;
  32. final Tuple2 verticalSpacing;
  33. final Color color;
  34. final DefaultStyles? styles;
  35. final bool enableInteractiveSelection;
  36. final bool hasFocus;
  37. final EdgeInsets? contentPadding;
  38. final EmbedBuilderFuncion embedBuilder;
  39. final CursorController cursorController;
  40. final Map<int, int> indentLevelCounts;
  41. @override
  42. Widget build(BuildContext context) {
  43. assert(debugCheckHasDirectionality(context));
  44. final defaultStyle = EditorStyles.getStyles(context, false);
  45. return _EditableBlock(
  46. block,
  47. textDirection,
  48. verticalSpacing as Tuple2<double, double>,
  49. scrollBottomInset,
  50. _getDecorationForBlock(block, defaultStyle) ?? const BoxDecoration(),
  51. contentPadding,
  52. _buildChildren(context, indentLevelCounts),
  53. );
  54. }
  55. // Builder
  56. BoxDecoration? _getDecorationForBlock(
  57. Block node, DefaultStyles? defaultStyles) {
  58. final attrs = block.style.attributes;
  59. if (attrs.containsKey(Attribute.quoteBlock.key)) {
  60. return defaultStyles!.quote!.decoration;
  61. } else if (attrs.containsKey(Attribute.codeBlock.key)) {
  62. return defaultStyles!.code!.decoration;
  63. }
  64. return null;
  65. }
  66. List<Widget> _buildChildren(
  67. BuildContext context, Map<int, int> indentLevelCounts) {
  68. final defaultStyles = EditorStyles.getStyles(context, false);
  69. final count = block.children.length;
  70. final children = <Widget>[];
  71. var index = 0;
  72. for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
  73. index++;
  74. final editableTextLine = EditableTextLine(
  75. line,
  76. _buildLeading(context, line, index, indentLevelCounts, count),
  77. TextLine(
  78. line: line,
  79. textDirection: textDirection,
  80. embedBuilder: embedBuilder,
  81. styles: styles!,
  82. ),
  83. _getIndentWidth(),
  84. _getSpacingForLine(line, index, count, defaultStyles),
  85. textDirection,
  86. textSelection,
  87. color,
  88. enableInteractiveSelection,
  89. hasFocus,
  90. MediaQuery.of(context).devicePixelRatio,
  91. cursorController,
  92. );
  93. children.add(editableTextLine);
  94. }
  95. return children.toList(growable: false);
  96. }
  97. double _getIndentWidth() {
  98. final attrs = block.style.attributes;
  99. final indent = attrs[Attribute.indent.key];
  100. var extraIndent = 0.0;
  101. if (indent != null && indent.value != null) {
  102. extraIndent = 16.0 * indent.value;
  103. }
  104. if (attrs.containsKey(Attribute.quoteBlock.key)) {
  105. return 16.0 + extraIndent;
  106. }
  107. return 32.0 + extraIndent;
  108. }
  109. Widget? _buildLeading(BuildContext context, Line line, int index,
  110. Map<int, int> indentLevelCounts, int count) {
  111. final defaultStyles = EditorStyles.getStyles(context, false);
  112. final attrs = line.style.attributes;
  113. // List Type (OrderedList, BulletList, CheckedList)
  114. if (attrs[Attribute.list.key] == Attribute.ordered) {
  115. return _NumberPoint(
  116. index: index,
  117. indentLevelCounts: indentLevelCounts,
  118. count: count,
  119. style: defaultStyles!.leading!.style,
  120. attrs: attrs,
  121. width: 32,
  122. padding: 8,
  123. );
  124. } else if (attrs[Attribute.list.key] == Attribute.bullet) {
  125. return _BulletPoint(
  126. style:
  127. defaultStyles!.leading!.style.copyWith(fontWeight: FontWeight.bold),
  128. width: 32,
  129. );
  130. } else if (attrs[Attribute.list.key] == Attribute.checked) {
  131. return _Checkbox(
  132. style: defaultStyles!.leading!.style,
  133. width: 32,
  134. isChecked: true,
  135. );
  136. } else if (attrs[Attribute.list.key] == Attribute.unchecked) {
  137. return _Checkbox(
  138. style: defaultStyles!.leading!.style,
  139. width: 32,
  140. isChecked: false,
  141. );
  142. }
  143. // Code Block
  144. if (attrs.containsKey(Attribute.codeBlock.key)) {
  145. return _NumberPoint(
  146. index: index,
  147. indentLevelCounts: indentLevelCounts,
  148. count: count,
  149. style: defaultStyles!.code!.style
  150. .copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)),
  151. width: 32,
  152. padding: 16,
  153. withDot: false,
  154. attrs: attrs,
  155. );
  156. }
  157. return null;
  158. }
  159. Tuple2 _getSpacingForLine(
  160. Line node, int index, int count, DefaultStyles? defaultStyles) {
  161. var top = 0.0, bottom = 0.0;
  162. final attrs = block.style.attributes;
  163. if (attrs.containsKey(Attribute.header.key)) {
  164. final level = attrs[Attribute.header.key]!.value;
  165. final headerStyles = <int, DefaultTextBlockStyle>{
  166. 1: defaultStyles!.h1!,
  167. 2: defaultStyles.h2!,
  168. 3: defaultStyles.h3!,
  169. 4: defaultStyles.h4!,
  170. 5: defaultStyles.h5!,
  171. 6: defaultStyles.h6!,
  172. };
  173. if (!headerStyles.containsKey(level)) {
  174. throw 'Invalid level $level';
  175. }
  176. top = headerStyles[level]!.verticalSpacing.item1;
  177. bottom = headerStyles[level]!.verticalSpacing.item2;
  178. } else {
  179. Tuple2? lineSpacing;
  180. final blockStyles = <String, DefaultTextBlockStyle>{
  181. Attribute.quoteBlock.key: defaultStyles!.quote!,
  182. Attribute.indent.key: defaultStyles.indent!,
  183. Attribute.list.key: defaultStyles.lists!,
  184. Attribute.codeBlock.key: defaultStyles.code!,
  185. Attribute.align.key: defaultStyles.align!,
  186. };
  187. blockStyles.forEach((k, v) {
  188. if (attrs.containsKey(k) && lineSpacing != null) {
  189. lineSpacing = v.lineSpacing;
  190. }
  191. });
  192. top = lineSpacing?.item1 ?? top;
  193. bottom = lineSpacing?.item2 ?? bottom;
  194. }
  195. // remove first and last edge padding
  196. if (index == 1) {
  197. top = 0.0;
  198. }
  199. if (index == count) {
  200. bottom = 0.0;
  201. }
  202. return Tuple2(top, bottom);
  203. }
  204. }
  205. /* ------------------------ Multi Child RenderObject ------------------------ */
  206. class _EditableBlock extends MultiChildRenderObjectWidget {
  207. _EditableBlock(
  208. this.block,
  209. this.textDirection,
  210. this.padding,
  211. this.scrollBottomInset,
  212. this.decoration,
  213. this.contentPadding,
  214. List<Widget> children,
  215. ) : super(children: children);
  216. final Block block;
  217. final TextDirection textDirection;
  218. final Tuple2<double, double> padding;
  219. final double scrollBottomInset;
  220. final Decoration decoration;
  221. final EdgeInsets? contentPadding;
  222. EdgeInsets get _padding =>
  223. EdgeInsets.only(top: padding.item1, bottom: padding.item2);
  224. EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero;
  225. @override
  226. RenderEditableTextBlock createRenderObject(BuildContext context) {
  227. return RenderEditableTextBlock(
  228. block: block,
  229. textDirection: textDirection,
  230. padding: _padding,
  231. scrollBottomInset: scrollBottomInset,
  232. decoration: decoration,
  233. contentPadding: _contentPadding,
  234. );
  235. }
  236. @override
  237. void updateRenderObject(
  238. BuildContext context, covariant RenderEditableTextBlock renderObject) {
  239. renderObject
  240. ..container = block
  241. ..textDirection = textDirection
  242. ..scrollBottomInset = scrollBottomInset
  243. ..padding = _padding
  244. ..decoration = decoration
  245. ..contentPadding = _contentPadding;
  246. }
  247. }
  248. /* ------------------------- Block Supplement Widget ------------------------ */
  249. const List<int> arabianRomanNumbers = [
  250. 1000,
  251. 900,
  252. 500,
  253. 400,
  254. 100,
  255. 90,
  256. 50,
  257. 40,
  258. 10,
  259. 9,
  260. 5,
  261. 4,
  262. 1
  263. ];
  264. const List<String> romanNumbers = [
  265. 'M',
  266. 'CM',
  267. 'D',
  268. 'CD',
  269. 'C',
  270. 'XC',
  271. 'L',
  272. 'XL',
  273. 'X',
  274. 'IX',
  275. 'V',
  276. 'IV',
  277. 'I'
  278. ];
  279. class _NumberPoint extends StatelessWidget {
  280. const _NumberPoint({
  281. required this.index,
  282. required this.indentLevelCounts,
  283. required this.count,
  284. required this.style,
  285. required this.width,
  286. required this.attrs,
  287. this.withDot = true,
  288. this.padding = 0.0,
  289. Key? key,
  290. }) : super(key: key);
  291. final int index;
  292. final Map<int?, int> indentLevelCounts;
  293. final int count;
  294. final TextStyle style;
  295. final double width;
  296. final Map<String, Attribute> attrs;
  297. final bool withDot;
  298. final double padding;
  299. @override
  300. Widget build(BuildContext context) {
  301. var s = index.toString();
  302. int? level = 0;
  303. if (!attrs.containsKey(Attribute.indent.key) &&
  304. !indentLevelCounts.containsKey(1)) {
  305. indentLevelCounts.clear();
  306. return Container(
  307. alignment: AlignmentDirectional.topEnd,
  308. width: width,
  309. padding: EdgeInsetsDirectional.only(end: padding),
  310. child: Text(withDot ? '$s.' : s, style: style),
  311. );
  312. }
  313. if (attrs.containsKey(Attribute.indent.key)) {
  314. level = attrs[Attribute.indent.key]!.value;
  315. } else {
  316. // first level but is back from previous indent level
  317. // supposed to be "2."
  318. indentLevelCounts[0] = 1;
  319. }
  320. if (indentLevelCounts.containsKey(level! + 1)) {
  321. // last visited level is done, going up
  322. indentLevelCounts.remove(level + 1);
  323. }
  324. final count = (indentLevelCounts[level] ?? 0) + 1;
  325. indentLevelCounts[level] = count;
  326. s = count.toString();
  327. if (level % 3 == 1) {
  328. // a. b. c. d. e. ...
  329. s = _toExcelSheetColumnTitle(count);
  330. } else if (level % 3 == 2) {
  331. // i. ii. iii. ...
  332. s = _intToRoman(count);
  333. }
  334. // level % 3 == 0 goes back to 1. 2. 3.
  335. return Container(
  336. alignment: AlignmentDirectional.topEnd,
  337. width: width,
  338. padding: EdgeInsetsDirectional.only(end: padding),
  339. child: Text(withDot ? '$s.' : s, style: style),
  340. );
  341. }
  342. String _toExcelSheetColumnTitle(int n) {
  343. final result = StringBuffer();
  344. while (n > 0) {
  345. n--;
  346. result.write(String.fromCharCode((n % 26).floor() + 97));
  347. n = (n / 26).floor();
  348. }
  349. return result.toString().split('').reversed.join();
  350. }
  351. String _intToRoman(int input) {
  352. var num = input;
  353. if (num < 0) {
  354. return '';
  355. } else if (num == 0) {
  356. return 'nulla';
  357. }
  358. final builder = StringBuffer();
  359. for (var a = 0; a < arabianRomanNumbers.length; a++) {
  360. final times = (num / arabianRomanNumbers[a])
  361. .truncate(); // equals 1 only when arabianRomanNumbers[a] = num
  362. // executes n times where n is the number of times you have to add
  363. // the current roman number value to reach current num.
  364. builder.write(romanNumbers[a] * times);
  365. num -= times *
  366. arabianRomanNumbers[
  367. a]; // subtract previous roman number value from num
  368. }
  369. return builder.toString().toLowerCase();
  370. }
  371. }
  372. class _BulletPoint extends StatelessWidget {
  373. const _BulletPoint({
  374. required this.style,
  375. required this.width,
  376. Key? key,
  377. }) : super(key: key);
  378. final TextStyle style;
  379. final double width;
  380. @override
  381. Widget build(BuildContext context) {
  382. return Container(
  383. alignment: AlignmentDirectional.topEnd,
  384. width: width,
  385. padding: const EdgeInsetsDirectional.only(end: 13),
  386. child: Text('•', style: style),
  387. );
  388. }
  389. }
  390. class _Checkbox extends StatefulWidget {
  391. const _Checkbox({
  392. Key? key,
  393. this.style,
  394. this.width,
  395. this.isChecked,
  396. this.onChanged,
  397. }) : super(key: key);
  398. final TextStyle? style;
  399. final double? width;
  400. final bool? isChecked;
  401. final Function(bool?)? onChanged;
  402. @override
  403. __CheckboxState createState() => __CheckboxState();
  404. }
  405. class __CheckboxState extends State<_Checkbox> {
  406. bool? isChecked;
  407. void _onCheckboxChanged(bool? newValue) {
  408. setState(() {
  409. isChecked = newValue;
  410. if (widget.onChanged != null) {
  411. widget.onChanged!(isChecked);
  412. }
  413. });
  414. }
  415. @override
  416. void initState() {
  417. super.initState();
  418. isChecked = widget.isChecked;
  419. }
  420. @override
  421. Widget build(BuildContext context) {
  422. return Container(
  423. alignment: AlignmentDirectional.topEnd,
  424. width: widget.width,
  425. padding: const EdgeInsetsDirectional.only(end: 13),
  426. child: Checkbox(
  427. value: widget.isChecked,
  428. onChanged: _onCheckboxChanged,
  429. ),
  430. );
  431. }
  432. }