text_line.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import 'dart:math' as math;
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/rendering.dart';
  5. import '../model/document/node/line.dart';
  6. import '../model/document/node/container.dart' as container_node;
  7. import '../service/cursor.dart';
  8. import '../widget/selection.dart';
  9. import '../widget/text_line.dart';
  10. import 'box.dart';
  11. enum TextLineSlot {
  12. LEADING,
  13. BODY,
  14. }
  15. /* ------------------------------- Render Box ------------------------------- */
  16. class RenderEditableTextLine extends RenderEditableBox {
  17. RenderEditableTextLine(
  18. this._line,
  19. this._textDirection,
  20. this._textSelection,
  21. this._enableInteractiveSelection,
  22. this.hasFocus,
  23. this._devicePixelRatio,
  24. this._padding,
  25. this._color,
  26. this._cursorController,
  27. );
  28. RenderBox? _leading;
  29. RenderContentProxyBox? _body;
  30. Line _line;
  31. TextDirection _textDirection;
  32. TextSelection _textSelection;
  33. Color _color;
  34. bool _enableInteractiveSelection;
  35. bool hasFocus = false;
  36. double _devicePixelRatio;
  37. EdgeInsetsGeometry _padding;
  38. CursorController _cursorController;
  39. EdgeInsets? _resolvedPadding;
  40. bool? _containesCursor;
  41. List<TextBox>? _selectedRects;
  42. Rect? _caretPrototype;
  43. final Map<TextLineSlot, RenderBox> children = {};
  44. // Getter && Setter
  45. Iterable<RenderBox> get _children sync* {
  46. if (_leading != null) {
  47. yield _leading!;
  48. }
  49. if (_body != null) {
  50. yield _body!;
  51. }
  52. }
  53. CursorController get cursorController => _cursorController;
  54. set cursorController(CursorController value) {
  55. if (_cursorController == value) {
  56. return;
  57. }
  58. _cursorController = value;
  59. markNeedsLayout();
  60. }
  61. double get devicePixelRatio => _devicePixelRatio;
  62. set devicePixelRatio(double value) {
  63. if (_devicePixelRatio == value) {
  64. return;
  65. }
  66. _devicePixelRatio = value;
  67. markNeedsLayout();
  68. }
  69. bool get enableInteractiveSelection => _enableInteractiveSelection;
  70. set enableInteractiveSelection(bool value) {
  71. if (_enableInteractiveSelection == value) {
  72. return;
  73. }
  74. _enableInteractiveSelection = value;
  75. markNeedsLayout();
  76. markNeedsSemanticsUpdate();
  77. }
  78. Color get color => _color;
  79. set color(Color value) {
  80. if (_color == value) {
  81. return;
  82. }
  83. _color = value;
  84. if (containsTextSelection()) {
  85. markNeedsPaint();
  86. }
  87. }
  88. TextDirection get textDirection => _textDirection;
  89. set textDirection(TextDirection value) {
  90. if (_textDirection == value) {
  91. return;
  92. }
  93. _textDirection = value;
  94. _resolvedPadding = null;
  95. markNeedsLayout();
  96. }
  97. TextSelection get textSelection => _textSelection;
  98. set textSelection(TextSelection value) {
  99. if (_textSelection == value) {
  100. return;
  101. }
  102. final containsSelection = containsTextSelection();
  103. if (attached && containsCursor()) {
  104. cursorController.removeListener(markNeedsLayout);
  105. cursorController.color.removeListener(markNeedsPaint);
  106. }
  107. _textSelection = value;
  108. _selectedRects = null;
  109. _containesCursor = null;
  110. if (attached && containsCursor()) {
  111. cursorController.addListener(markNeedsLayout);
  112. cursorController.color.addListener(markNeedsPaint);
  113. }
  114. if (containsSelection || containsTextSelection()) {
  115. markNeedsPaint();
  116. }
  117. }
  118. Line get line => _line;
  119. set line(Line value) {
  120. if (_line == value) {
  121. return;
  122. }
  123. _line = value;
  124. _containesCursor = null;
  125. markNeedsLayout();
  126. }
  127. EdgeInsetsGeometry get padding => _padding;
  128. set padding(EdgeInsetsGeometry value) {
  129. assert(value.isNonNegative);
  130. if (_padding == value) {
  131. return;
  132. }
  133. _padding = value;
  134. _resolvedPadding = null;
  135. markNeedsLayout();
  136. }
  137. RenderBox? get leading => _leading;
  138. set leading(RenderBox? value) {
  139. _leading = _updateChild(_leading, value, TextLineSlot.LEADING);
  140. }
  141. RenderContentProxyBox? get body => _body;
  142. set body(RenderContentProxyBox? value) {
  143. _body =
  144. _updateChild(_body, value, TextLineSlot.BODY) as RenderContentProxyBox?;
  145. }
  146. // Util
  147. bool containsTextSelection() {
  148. return line.documentOffset <= textSelection.end &&
  149. textSelection.start <= line.documentOffset + line.length - 1;
  150. }
  151. bool containsCursor() {
  152. return _containesCursor ??= textSelection.isCollapsed &&
  153. line.containsOffset(textSelection.baseOffset);
  154. }
  155. RenderBox? _updateChild(
  156. RenderBox? oldChild, RenderBox? newChild, TextLineSlot slot) {
  157. if (oldChild != null) {
  158. dropChild(oldChild);
  159. children.remove(slot);
  160. }
  161. if (newChild != null) {
  162. children[slot] = newChild;
  163. adoptChild(newChild);
  164. }
  165. return newChild;
  166. }
  167. List<TextBox> _getBoxes(TextSelection textSelection) {
  168. final parentData = _body!.parentData as BoxParentData?;
  169. return _body!.getBoxesForSelection(textSelection).map((box) {
  170. return TextBox.fromLTRBD(
  171. box.left + parentData!.offset.dx,
  172. box.top + parentData.offset.dy,
  173. box.right + parentData.offset.dx,
  174. box.bottom + parentData.offset.dy,
  175. box.direction,
  176. );
  177. }).toList(growable: false);
  178. }
  179. void _resolvePadding() {
  180. if (_resolvedPadding != null) {
  181. return;
  182. }
  183. _resolvedPadding = padding.resolve(textDirection);
  184. assert(_resolvedPadding!.isNonNegative);
  185. }
  186. // System Override: Selection && Cursor
  187. @override
  188. TextSelectionPoint getBaseEndpointForSelection(TextSelection textSelection) {
  189. return _getEndpointForSelection(textSelection, true);
  190. }
  191. @override
  192. TextSelectionPoint getExtentEndpointForSelection(
  193. TextSelection textSelection) {
  194. return _getEndpointForSelection(textSelection, false);
  195. }
  196. TextSelectionPoint _getEndpointForSelection(
  197. TextSelection textSelection, bool first) {
  198. if (textSelection.isCollapsed) {
  199. return TextSelectionPoint(
  200. Offset(0, preferredLineHeight(textSelection.extent)) +
  201. getOffsetForCaret(textSelection.extent),
  202. null,
  203. );
  204. }
  205. final boxes = _getBoxes(textSelection);
  206. assert(boxes.isNotEmpty);
  207. final targetBox = first ? boxes.first : boxes.last;
  208. return TextSelectionPoint(
  209. Offset(first ? targetBox.start : targetBox.end, targetBox.bottom),
  210. targetBox.direction,
  211. );
  212. }
  213. @override
  214. TextRange getLineBoundary(TextPosition position) {
  215. final lineDy = getOffsetForCaret(position)
  216. .translate(0, 0.5 * preferredLineHeight(position))
  217. .dy;
  218. final lineBoxes = _getBoxes(TextSelection(
  219. baseOffset: 0,
  220. extentOffset: line.length - 1,
  221. ))
  222. .where((element) => element.top < lineDy && element.bottom > lineDy)
  223. .toList(growable: false);
  224. return TextRange(
  225. start: getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset,
  226. end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset,
  227. );
  228. }
  229. @override
  230. Offset getOffsetForCaret(TextPosition position) {
  231. return _body!.getOffsetForCaret(position, _caretPrototype) +
  232. (body!.parentData as BoxParentData).offset;
  233. }
  234. @override
  235. TextPosition? getPositionAbove(TextPosition position) {
  236. return _getPosition(position, -0.5);
  237. }
  238. @override
  239. TextPosition? getPositionBelow(TextPosition position) {
  240. return _getPosition(position, 1.5);
  241. }
  242. TextPosition? _getPosition(TextPosition textPosition, double dyScale) {
  243. assert(textPosition.offset < line.length);
  244. final offset = getOffsetForCaret(textPosition)
  245. .translate(0, dyScale * preferredLineHeight(textPosition));
  246. if (_body!.size
  247. .contains(offset - (_body!.parentData as BoxParentData).offset)) {
  248. return getPositionForOffset(offset);
  249. }
  250. return null;
  251. }
  252. @override
  253. TextPosition getPositionForOffset(Offset offset) {
  254. return _body!.getPositionForOffset(
  255. offset - (_body!.parentData as BoxParentData).offset);
  256. }
  257. @override
  258. TextRange getWordBoundary(TextPosition position) {
  259. return _body!.getWordBoundary(position);
  260. }
  261. @override
  262. double preferredLineHeight(TextPosition position) {
  263. return _body!.getPreferredLineHeight();
  264. }
  265. @override
  266. container_node.Container get container => line;
  267. double get cursorWidth => cursorController.style.width;
  268. double get cursorHeight =>
  269. cursorController.style.height ??
  270. preferredLineHeight(const TextPosition(offset: 0));
  271. void _computeCaretPrototype() {
  272. switch (defaultTargetPlatform) {
  273. case TargetPlatform.iOS:
  274. case TargetPlatform.macOS:
  275. _caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2);
  276. break;
  277. case TargetPlatform.android:
  278. case TargetPlatform.fuchsia:
  279. case TargetPlatform.linux:
  280. case TargetPlatform.windows:
  281. _caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0);
  282. break;
  283. default:
  284. throw 'Invalid platform';
  285. }
  286. }
  287. @override
  288. void attach(covariant PipelineOwner owner) {
  289. super.attach(owner);
  290. for (final child in _children) {
  291. child.attach(owner);
  292. }
  293. if (containsCursor()) {
  294. cursorController.addListener(markNeedsLayout);
  295. cursorController.cursorColor.addListener(markNeedsPaint);
  296. }
  297. }
  298. @override
  299. void detach() {
  300. super.detach();
  301. for (final child in _children) {
  302. child.detach();
  303. }
  304. if (containsCursor()) {
  305. cursorController.removeListener(markNeedsLayout);
  306. cursorController.cursorColor.removeListener(markNeedsPaint);
  307. }
  308. }
  309. @override
  310. void redepthChildren() {
  311. _children.forEach(redepthChild);
  312. }
  313. @override
  314. void visitChildren(RenderObjectVisitor visitor) {
  315. _children.forEach(visitor);
  316. }
  317. @override
  318. List<DiagnosticsNode> debugDescribeChildren() {
  319. final value = <DiagnosticsNode>[];
  320. void add(RenderBox? child, String name) {
  321. if (child != null) {
  322. value.add(child.toDiagnosticsNode(name: name));
  323. }
  324. }
  325. add(_leading, 'leading');
  326. add(_body, 'body');
  327. return value;
  328. }
  329. @override
  330. bool get sizedByParent => false;
  331. @override
  332. double computeMinIntrinsicWidth(double height) {
  333. _resolvePadding();
  334. final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
  335. final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
  336. final leadingWidth = _leading == null
  337. ? 0
  338. : _leading!.getMinIntrinsicWidth(height - verticalPadding) as int;
  339. final bodyWidth = _body == null
  340. ? 0
  341. : _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding))
  342. as int;
  343. return horizontalPadding + leadingWidth + bodyWidth;
  344. }
  345. @override
  346. double computeMaxIntrinsicWidth(double height) {
  347. _resolvePadding();
  348. final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
  349. final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
  350. final leadingWidth = _leading == null
  351. ? 0
  352. : _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int;
  353. final bodyWidth = _body == null
  354. ? 0
  355. : _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding))
  356. as int;
  357. return horizontalPadding + leadingWidth + bodyWidth;
  358. }
  359. @override
  360. double computeMinIntrinsicHeight(double width) {
  361. _resolvePadding();
  362. final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
  363. final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
  364. if (_body != null) {
  365. return _body!
  366. .getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) +
  367. verticalPadding;
  368. }
  369. return verticalPadding;
  370. }
  371. @override
  372. double computeMaxIntrinsicHeight(double width) {
  373. _resolvePadding();
  374. final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
  375. final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
  376. if (_body != null) {
  377. return _body!
  378. .getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) +
  379. verticalPadding;
  380. }
  381. return verticalPadding;
  382. }
  383. @override
  384. double? computeDistanceToActualBaseline(TextBaseline baseline) {
  385. _resolvePadding();
  386. return _body!.getDistanceToActualBaseline(baseline)! +
  387. _resolvedPadding!.top;
  388. }
  389. @override
  390. void performLayout() {
  391. final constraints = this.constraints;
  392. _selectedRects = null;
  393. _resolvePadding();
  394. assert(_resolvedPadding != null);
  395. if (_body == null && _leading == null) {
  396. size = constraints.constrain(Size(
  397. _resolvedPadding!.left + _resolvedPadding!.right,
  398. _resolvedPadding!.top + _resolvedPadding!.bottom,
  399. ));
  400. return;
  401. }
  402. final innerConstraints = constraints.deflate(_resolvedPadding!);
  403. final indentWidth = textDirection == TextDirection.ltr
  404. ? _resolvedPadding!.left
  405. : _resolvedPadding!.right;
  406. _body!.layout(innerConstraints, parentUsesSize: true);
  407. (_body!.parentData as BoxParentData).offset =
  408. Offset(_resolvedPadding!.left, _resolvedPadding!.top);
  409. if (leading != null) {
  410. final leadingConstraints = innerConstraints.copyWith(
  411. minWidth: indentWidth,
  412. maxWidth: indentWidth,
  413. maxHeight: _body!.size.height,
  414. );
  415. _leading!.layout(leadingConstraints, parentUsesSize: true);
  416. (_leading!.parentData as BoxParentData).offset =
  417. Offset(0, _resolvedPadding!.top);
  418. }
  419. size = constraints.constrain(Size(
  420. _resolvedPadding!.left + _body!.size.width + _resolvedPadding!.right,
  421. _resolvedPadding!.top + _body!.size.height + _resolvedPadding!.bottom,
  422. ));
  423. _computeCaretPrototype();
  424. }
  425. CursorPainter get _cursorPainter => CursorPainter(
  426. _body,
  427. cursorController.style,
  428. _caretPrototype,
  429. cursorController.cursorColor.value,
  430. devicePixelRatio,
  431. );
  432. @override
  433. void paint(PaintingContext context, Offset offset) {
  434. if (_leading != null) {
  435. final parentData = _leading!.parentData as BoxParentData;
  436. final effectiveOffset = offset + parentData.offset;
  437. context.paintChild(_leading!, effectiveOffset);
  438. }
  439. if (_body != null) {
  440. final parentData = _body!.parentData as BoxParentData;
  441. final effectiveOffset = offset + parentData.offset;
  442. if (enableInteractiveSelection &&
  443. line.documentOffset <= textSelection.end &&
  444. textSelection.start <= line.documentOffset + line.length - 1) {
  445. final local = localSelection(line, textSelection, false);
  446. _selectedRects ??= _body!.getBoxesForSelection(local);
  447. _paintSelection(context, effectiveOffset);
  448. }
  449. if (hasFocus &&
  450. cursorController.show.value &&
  451. containsCursor() &&
  452. !cursorController.style.paintAboveText) {
  453. _paintCursor(context, effectiveOffset);
  454. }
  455. context.paintChild(_body!, effectiveOffset);
  456. if (hasFocus &&
  457. cursorController.show.value &&
  458. containsCursor() &&
  459. cursorController.style.paintAboveText) {
  460. _paintCursor(context, effectiveOffset);
  461. }
  462. }
  463. }
  464. void _paintSelection(PaintingContext context, Offset effectiveOffset) {
  465. assert(_selectedRects != null);
  466. final paint = Paint()..color = color;
  467. for (final box in _selectedRects!) {
  468. context.canvas.drawRect(box.toRect().shift(effectiveOffset), paint);
  469. }
  470. }
  471. void _paintCursor(PaintingContext context, Offset effectiveOffset) {
  472. final position = TextPosition(
  473. offset: textSelection.extentOffset - line.documentOffset,
  474. affinity: textSelection.base.affinity,
  475. );
  476. _cursorPainter.paint(context.canvas, effectiveOffset, position);
  477. }
  478. @override
  479. bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
  480. return _children.first.hitTest(result, position: position);
  481. }
  482. }
  483. /* --------------------------------- Element -------------------------------- */
  484. class TextLineElement extends RenderObjectElement {
  485. TextLineElement(EditableTextLine line) : super(line);
  486. final Map<TextLineSlot, Element> _slotToChildren = <TextLineSlot, Element>{};
  487. @override
  488. EditableTextLine get widget => super.widget as EditableTextLine;
  489. @override
  490. RenderEditableTextLine get renderObject =>
  491. super.renderObject as RenderEditableTextLine;
  492. @override
  493. void visitChildren(ElementVisitor visitor) {
  494. _slotToChildren.values.forEach(visitor);
  495. }
  496. @override
  497. void forgetChild(Element child) {
  498. assert(_slotToChildren.containsValue(child));
  499. assert(child.slot is TextLineSlot);
  500. assert(_slotToChildren.containsKey(child.slot));
  501. _slotToChildren.remove(child.slot);
  502. super.forgetChild(child);
  503. }
  504. @override
  505. void mount(Element? parent, dynamic newSlot) {
  506. super.mount(parent, newSlot);
  507. _mountChild(widget.leading, TextLineSlot.LEADING);
  508. _mountChild(widget.body, TextLineSlot.BODY);
  509. }
  510. @override
  511. void update(covariant EditableTextLine newWidget) {
  512. super.update(newWidget);
  513. assert(widget == newWidget);
  514. _updateChild(widget.leading, TextLineSlot.LEADING);
  515. _updateChild(widget.body, TextLineSlot.BODY);
  516. }
  517. @override
  518. void insertRenderObjectChild(RenderBox child, TextLineSlot? slot) {
  519. _updateRenderObject(child, slot);
  520. assert(renderObject.children.keys.contains(slot));
  521. }
  522. @override
  523. void removeRenderObjectChild(RenderObject child, TextLineSlot? slot) {
  524. assert(child is RenderBox);
  525. assert(renderObject.children[slot!] == child);
  526. _updateRenderObject(null, slot);
  527. assert(!renderObject.children.keys.contains(slot));
  528. }
  529. @override
  530. void moveRenderObjectChild(
  531. RenderObject child, dynamic oldSlot, dynamic newSlot) {
  532. throw UnimplementedError();
  533. }
  534. void _mountChild(Widget? widget, TextLineSlot slot) {
  535. final oldChild = _slotToChildren[slot];
  536. final newChild = updateChild(oldChild, widget, slot);
  537. if (oldChild != null) {
  538. _slotToChildren.remove(slot);
  539. }
  540. if (newChild != null) {
  541. _slotToChildren[slot] = newChild;
  542. }
  543. }
  544. void _updateRenderObject(RenderBox? child, TextLineSlot? slot) {
  545. switch (slot) {
  546. case TextLineSlot.LEADING:
  547. renderObject.leading = child;
  548. break;
  549. case TextLineSlot.BODY:
  550. renderObject.body = child as RenderContentProxyBox?;
  551. break;
  552. default:
  553. throw UnimplementedError();
  554. }
  555. }
  556. void _updateChild(Widget? widget, TextLineSlot slot) {
  557. final oldChild = _slotToChildren[slot];
  558. final newChild = updateChild(oldChild, widget, slot);
  559. if (oldChild != null) {
  560. _slotToChildren.remove(slot);
  561. }
  562. if (newChild != null) {
  563. _slotToChildren[slot] = newChild;
  564. }
  565. }
  566. }