editor.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. import 'dart:math' as math;
  2. import 'package:flowy_editor/widget/selection.dart';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';
  6. import '../model/document/node/container.dart' as node;
  7. import '../model/document/document.dart';
  8. import 'box.dart';
  9. typedef TextSelectionChangeHandler = void Function(
  10. TextSelection selection,
  11. SelectionChangedCause cause,
  12. );
  13. /* ----------------------------- Abstract Editor ---------------------------- */
  14. abstract class RenderAbstractEditor {
  15. TextSelection selectWordAtPosition(TextPosition position);
  16. TextSelection selectLineAtPosition(TextPosition position);
  17. double preferredLineHeight(TextPosition position);
  18. TextPosition getPositionForOffset(Offset offset);
  19. List<TextSelectionPoint> getEndpointsForSelection(
  20. TextSelection textSelection);
  21. void handleTapDown(TapDownDetails details);
  22. void selectWordsInRange(Offset from, Offset to, SelectionChangedCause cause);
  23. void selectWordEdge(SelectionChangedCause cause);
  24. void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause);
  25. void selectWord(SelectionChangedCause cause);
  26. void selectPosition(SelectionChangedCause cause);
  27. }
  28. /* ------------------------------ Container Box ----------------------------- */
  29. class EditableContainerParentData
  30. extends ContainerBoxParentData<RenderEditableBox> {}
  31. class RenderEditableContainerBox extends RenderBox
  32. with
  33. ContainerRenderObjectMixin<RenderEditableBox,
  34. EditableContainerParentData>,
  35. RenderBoxContainerDefaultsMixin<RenderEditableBox,
  36. EditableContainerParentData> {
  37. RenderEditableContainerBox(
  38. List<RenderEditableBox>? children,
  39. this.textDirection,
  40. this.scrollBottomInset,
  41. this._container,
  42. this._padding,
  43. ) : assert(_padding.isNonNegative) {
  44. addAll(children);
  45. }
  46. TextDirection textDirection;
  47. double scrollBottomInset;
  48. node.Container _container;
  49. EdgeInsetsGeometry _padding;
  50. EdgeInsets? _resolvedPadding;
  51. node.Container get container => _container;
  52. set container(node.Container container) {
  53. if (_container == container) {
  54. return;
  55. }
  56. _container = container;
  57. markNeedsLayout();
  58. }
  59. EdgeInsetsGeometry get padding => _padding;
  60. set padding(EdgeInsetsGeometry value) {
  61. assert(value.isNonNegative);
  62. if (_padding == value) {
  63. return;
  64. }
  65. _padding = value;
  66. _markNeedsPaddingResolution();
  67. }
  68. EdgeInsets? get resolvedPadding => _resolvedPadding;
  69. void _resolvePadding() {
  70. if (_resolvedPadding != null) {
  71. return;
  72. }
  73. _resolvedPadding = _padding.resolve(textDirection);
  74. _resolvedPadding = _resolvedPadding!.copyWith(left: _resolvedPadding!.left);
  75. assert(_resolvedPadding!.isNonNegative);
  76. }
  77. void _markNeedsPaddingResolution() {
  78. _resolvedPadding = null;
  79. markNeedsLayout();
  80. }
  81. RenderEditableBox childAtPosition(TextPosition position) {
  82. assert(firstChild != null);
  83. final targetNode = _container.queryChild(position.offset, false).node;
  84. var targetChild = firstChild;
  85. while (targetChild != null) {
  86. if (targetChild.container == targetNode) {
  87. break;
  88. }
  89. targetChild = childAfter(targetChild);
  90. }
  91. if (targetChild == null) {
  92. throw '`targetChild` should not be null';
  93. }
  94. return targetChild;
  95. }
  96. RenderEditableBox? childAtOffset(Offset offset) {
  97. assert(firstChild != null);
  98. _resolvePadding();
  99. if (offset.dy <= _resolvedPadding!.top) {
  100. return firstChild;
  101. }
  102. if (offset.dy >= size.height - _resolvedPadding!.bottom) {
  103. return lastChild;
  104. }
  105. var child = firstChild;
  106. final dx = -offset.dx;
  107. var dy = _resolvedPadding!.top;
  108. while (child != null) {
  109. if (child.size.contains(offset.translate(dx, -dy))) {
  110. return child;
  111. }
  112. dy += child.size.height;
  113. child = childAfter(child);
  114. }
  115. throw 'No child';
  116. }
  117. @override
  118. void setupParentData(covariant RenderBox child) {
  119. if (child.parent is EditableContainerParentData) {
  120. return;
  121. }
  122. child.parentData = EditableContainerParentData();
  123. }
  124. @override
  125. void performLayout() {
  126. assert(constraints.hasBoundedWidth);
  127. assert(!constraints.hasBoundedHeight);
  128. _resolvePadding();
  129. assert(_resolvedPadding != null);
  130. var mainAxisExtent = _resolvedPadding!.top;
  131. var child = firstChild;
  132. final innerConstraints =
  133. BoxConstraints.tightFor(width: constraints.maxWidth)
  134. .deflate(_resolvedPadding!);
  135. while (child != null) {
  136. child.layout(innerConstraints, parentUsesSize: true);
  137. final childParentData = (child.parentData as EditableContainerParentData)
  138. ..offset = Offset(_resolvedPadding!.left, mainAxisExtent);
  139. mainAxisExtent += child.size.height;
  140. assert(child.parentData == childParentData);
  141. child = childParentData.nextSibling;
  142. }
  143. mainAxisExtent += _resolvedPadding!.bottom;
  144. size = constraints.constrain(Size(constraints.maxWidth, mainAxisExtent));
  145. assert(size.isFinite);
  146. }
  147. double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) {
  148. var extent = 0.0;
  149. var child = firstChild;
  150. while (child != null) {
  151. extent = math.max(extent, childSize(child));
  152. final childParentData = child.parentData as EditableContainerParentData;
  153. child = childParentData.nextSibling;
  154. }
  155. return extent;
  156. }
  157. double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) {
  158. var extent = 0.0;
  159. var child = firstChild;
  160. while (child != null) {
  161. extent += childSize(child);
  162. final childParentData = child.parentData as EditableContainerParentData;
  163. child = childParentData.nextSibling;
  164. }
  165. return extent;
  166. }
  167. @override
  168. double computeMinIntrinsicWidth(double height) {
  169. _resolvePadding();
  170. return _getIntrinsicCrossAxis((child) {
  171. final childHeight = math.max<double>(
  172. 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
  173. return child.getMinIntrinsicWidth(childHeight) +
  174. _resolvedPadding!.left +
  175. _resolvedPadding!.right;
  176. });
  177. }
  178. @override
  179. double computeMaxIntrinsicWidth(double height) {
  180. _resolvePadding();
  181. return _getIntrinsicCrossAxis((child) {
  182. final childHeight = math.max<double>(
  183. 0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
  184. return child.getMaxIntrinsicWidth(childHeight) +
  185. _resolvedPadding!.left +
  186. _resolvedPadding!.right;
  187. });
  188. }
  189. @override
  190. double computeMinIntrinsicHeight(double width) {
  191. _resolvePadding();
  192. return _getIntrinsicMainAxis((child) {
  193. final childWidth = math.max<double>(
  194. 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
  195. return child.getMinIntrinsicHeight(childWidth) +
  196. _resolvedPadding!.top +
  197. _resolvedPadding!.bottom;
  198. });
  199. }
  200. @override
  201. double computeMaxIntrinsicHeight(double width) {
  202. _resolvePadding();
  203. return _getIntrinsicMainAxis((child) {
  204. final childWidth = math.max<double>(
  205. 0, width - _resolvedPadding!.left + _resolvedPadding!.right);
  206. return child.getMaxIntrinsicHeight(childWidth) +
  207. _resolvedPadding!.top +
  208. _resolvedPadding!.bottom;
  209. });
  210. }
  211. @override
  212. double? computeDistanceToActualBaseline(TextBaseline baseline) {
  213. _resolvePadding();
  214. return defaultComputeDistanceToFirstActualBaseline(baseline)! +
  215. _resolvedPadding!.top;
  216. }
  217. }
  218. /* ------------------------------ Render Editor ----------------------------- */
  219. class RenderEditor extends RenderEditableContainerBox
  220. implements RenderAbstractEditor {
  221. RenderEditor(
  222. List<RenderEditableBox>? children,
  223. TextDirection textDirection,
  224. double scrollBottomInset,
  225. EdgeInsetsGeometry padding,
  226. EdgeInsets floatingCursorAddedMargin,
  227. this._document,
  228. this._selection,
  229. this._hasFocus,
  230. this.onSelectionChanged,
  231. this._startHandleLayerLink,
  232. this._endHandleLayerLink,
  233. ) : super(
  234. children,
  235. textDirection,
  236. scrollBottomInset,
  237. _document.root,
  238. padding,
  239. );
  240. TextSelectionChangeHandler onSelectionChanged;
  241. Document _document;
  242. TextSelection _selection;
  243. final ValueNotifier<bool> _selectionStartInViewport =
  244. ValueNotifier<bool>(true);
  245. final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
  246. bool _hasFocus = false;
  247. LayerLink _startHandleLayerLink;
  248. LayerLink _endHandleLayerLink;
  249. Offset? _lastTapDownPosition;
  250. Document get document => _document;
  251. ValueListenable<bool> get selectionStartInViewport =>
  252. _selectionStartInViewport;
  253. ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
  254. set document(Document value) {
  255. if (_document == value) {
  256. return;
  257. }
  258. _document = value;
  259. markNeedsLayout();
  260. }
  261. set hasFocus(bool value) {
  262. if (_hasFocus == value) {
  263. return;
  264. }
  265. _hasFocus = value;
  266. markNeedsSemanticsUpdate();
  267. }
  268. set selection(TextSelection value) {
  269. if (_selection == value) {
  270. return;
  271. }
  272. _selection = value;
  273. markNeedsPaint();
  274. }
  275. set startHandleLayerLink(LayerLink value) {
  276. if (_startHandleLayerLink == value) {
  277. return;
  278. }
  279. _startHandleLayerLink = value;
  280. markNeedsPaint();
  281. }
  282. set endHandleLayerLink(LayerLink value) {
  283. if (_endHandleLayerLink == value) {
  284. return;
  285. }
  286. _endHandleLayerLink = value;
  287. markNeedsPaint();
  288. }
  289. @override
  290. set scrollBottomInset(double value) {
  291. if (scrollBottomInset == value) {
  292. return;
  293. }
  294. scrollBottomInset = value;
  295. markNeedsPaint();
  296. }
  297. @override
  298. List<TextSelectionPoint> getEndpointsForSelection(
  299. TextSelection textSelection) {
  300. if (textSelection.isCollapsed) {
  301. final child = childAtPosition(textSelection.extent);
  302. final localPosition = TextPosition(
  303. offset: textSelection.extentOffset - child.container.offset,
  304. );
  305. final localOffset = child.getOffsetForCaret(localPosition);
  306. final parentData = child.parentData as BoxParentData;
  307. return [
  308. TextSelectionPoint(
  309. Offset(0, child.preferredLineHeight(localPosition)) +
  310. localOffset +
  311. parentData.offset,
  312. null,
  313. )
  314. ];
  315. }
  316. final baseNode = _container.queryChild(textSelection.start, false).node;
  317. var baseChild = firstChild;
  318. while (baseChild != null) {
  319. if (baseChild.container == baseNode) {
  320. break;
  321. }
  322. baseChild = childAfter(baseChild);
  323. }
  324. assert(baseChild != null);
  325. final baseParentData = baseChild!.parentData as BoxParentData;
  326. final baseSelection =
  327. localSelection(baseChild.container, textSelection, true);
  328. var basePoint = baseChild.getBaseEndpointForSelection(baseSelection);
  329. basePoint = TextSelectionPoint(
  330. basePoint.point + baseParentData.offset,
  331. basePoint.direction,
  332. );
  333. final extentNode = _container.queryChild(textSelection.end, false).node;
  334. RenderEditableBox? extentChild = baseChild;
  335. while (extentChild != null) {
  336. if (extentChild.container == extentNode) {
  337. break;
  338. }
  339. extentChild = childAfter(extentChild);
  340. }
  341. assert(extentChild != null);
  342. final extentParentData = extentChild!.parentData as BoxParentData;
  343. final extentSelection =
  344. localSelection(extentChild.container, textSelection, true);
  345. var extentPoint =
  346. extentChild.getExtentEndpointForSelection(extentSelection);
  347. extentPoint = TextSelectionPoint(
  348. extentPoint.point + extentParentData.offset,
  349. extentPoint.direction,
  350. );
  351. return <TextSelectionPoint>[basePoint, extentPoint];
  352. }
  353. @override
  354. TextPosition getPositionForOffset(Offset offset) {
  355. final local = globalToLocal(offset);
  356. final child = childAtOffset(local)!;
  357. final parentData = child.parentData as BoxParentData;
  358. final localOffset = local - parentData.offset;
  359. final localPosition = child.getPositionForOffset(localOffset);
  360. return TextPosition(
  361. offset: localPosition.offset + child.container.offset,
  362. affinity: localPosition.affinity,
  363. );
  364. }
  365. @override
  366. void handleTapDown(TapDownDetails details) {
  367. _lastTapDownPosition = details.globalPosition;
  368. }
  369. @override
  370. double preferredLineHeight(TextPosition position) {
  371. final child = childAtPosition(position);
  372. return child.preferredLineHeight(
  373. TextPosition(offset: position.offset - child.container.offset),
  374. );
  375. }
  376. @override
  377. TextSelection selectLineAtPosition(TextPosition position) {
  378. final child = childAtPosition(position);
  379. final nodeOffset = child.container.offset;
  380. final localPosition = TextPosition(
  381. offset: position.offset - nodeOffset,
  382. affinity: position.affinity,
  383. );
  384. final localLineRange = child.getLineBoundary(localPosition);
  385. final line = TextRange(
  386. start: localLineRange.start + nodeOffset,
  387. end: localLineRange.end + nodeOffset,
  388. );
  389. if (position.offset >= line.end) {
  390. return TextSelection.fromPosition(position);
  391. }
  392. return TextSelection(baseOffset: line.start, extentOffset: line.end);
  393. }
  394. @override
  395. void selectPosition(SelectionChangedCause cause) {
  396. selectPositionAt(_lastTapDownPosition!, null, cause);
  397. }
  398. @override
  399. void selectPositionAt(Offset from, Offset? to, SelectionChangedCause cause) {
  400. final fromPosition = getPositionForOffset(from);
  401. final toPosition = to == null ? null : getPositionForOffset(to);
  402. var baseOffset = fromPosition.offset;
  403. var extentOffset = fromPosition.offset;
  404. if (toPosition != null) {
  405. baseOffset = math.min(fromPosition.offset, toPosition.offset);
  406. extentOffset = math.max(fromPosition.offset, toPosition.offset);
  407. }
  408. final newSelection = TextSelection(
  409. baseOffset: baseOffset,
  410. extentOffset: extentOffset,
  411. affinity: fromPosition.affinity,
  412. );
  413. _handleSelectionChange(newSelection, cause);
  414. }
  415. @override
  416. void selectWord(SelectionChangedCause cause) {
  417. selectWordsInRange(_lastTapDownPosition!, null, cause);
  418. }
  419. @override
  420. TextSelection selectWordAtPosition(TextPosition position) {
  421. final child = childAtPosition(position);
  422. final nodeOffset = child.container.offset;
  423. final localPosition = TextPosition(
  424. offset: position.offset - nodeOffset,
  425. affinity: position.affinity,
  426. );
  427. final localWord = child.getWordBoundary(localPosition);
  428. final word = TextRange(
  429. start: localWord.start + nodeOffset, end: localWord.end + nodeOffset);
  430. if (position.offset >= word.end) {
  431. return TextSelection.fromPosition(position);
  432. }
  433. return TextSelection(baseOffset: word.start, extentOffset: word.end);
  434. }
  435. @override
  436. void selectWordEdge(SelectionChangedCause cause) {
  437. assert(_lastTapDownPosition != null);
  438. final position = getPositionForOffset(_lastTapDownPosition!);
  439. final child = childAtPosition(position);
  440. final nodeOffset = child.container.offset;
  441. final localPosition = TextPosition(
  442. offset: position.offset - nodeOffset,
  443. affinity: position.affinity,
  444. );
  445. final localWord = child.getWordBoundary(localPosition);
  446. final word = TextRange(
  447. start: localWord.start + nodeOffset,
  448. end: localWord.end + nodeOffset,
  449. );
  450. if (position.offset - word.start <= 1) {
  451. _handleSelectionChange(
  452. TextSelection.collapsed(offset: word.start), cause);
  453. } else {
  454. _handleSelectionChange(
  455. TextSelection.collapsed(
  456. offset: word.end, affinity: TextAffinity.upstream),
  457. cause,
  458. );
  459. }
  460. }
  461. @override
  462. void selectWordsInRange(
  463. Offset from, Offset? to, SelectionChangedCause cause) {
  464. final firstPosition = getPositionForOffset(from);
  465. final firstWord = selectWordAtPosition(firstPosition);
  466. final lastWord =
  467. to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to));
  468. _handleSelectionChange(
  469. TextSelection(
  470. baseOffset: firstWord.base.offset,
  471. extentOffset: lastWord.extent.offset,
  472. affinity: firstWord.affinity,
  473. ),
  474. cause);
  475. }
  476. @override
  477. void paint(PaintingContext context, Offset offset) {
  478. defaultPaint(context, offset);
  479. _paintHandleLayers(context, getEndpointsForSelection(_selection));
  480. }
  481. @override
  482. bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
  483. return defaultHitTestChildren(result, position: position);
  484. }
  485. /// Returns the y-offset of the editor at which [selection] is visible.
  486. ///
  487. /// The offset is the distance from the top of the editor and is the minimum
  488. /// from the current scroll position until [selection] becomes visible.
  489. /// Returns null if [selection] is already visible.
  490. double? getOffsetToRevealCursor(
  491. double viewportHeight, double scrollOffset, double offsetInViewport) {
  492. final endpoints = getEndpointsForSelection(_selection);
  493. final endpoint = endpoints.first;
  494. final child = childAtPosition(_selection.extent);
  495. const kMargin = 8.0;
  496. final lineHeight = child.preferredLineHeight(
  497. TextPosition(offset: _selection.extentOffset - child.container.offset),
  498. );
  499. final caretTop = endpoint.point.dy -
  500. lineHeight -
  501. kMargin +
  502. offsetInViewport +
  503. scrollBottomInset;
  504. final caretBottom =
  505. endpoint.point.dy + kMargin + offsetInViewport + scrollBottomInset;
  506. double? dy;
  507. if (caretTop < scrollOffset) {
  508. dy = caretTop;
  509. } else if (caretBottom > scrollOffset + viewportHeight) {
  510. dy = caretBottom - viewportHeight;
  511. }
  512. if (dy == null) {
  513. return null;
  514. }
  515. return math.max(dy, 0);
  516. }
  517. // Util
  518. void _handleSelectionChange(
  519. TextSelection nextSelection, SelectionChangedCause cause) {
  520. final focusingEmpty = nextSelection.baseOffset == 0 &&
  521. nextSelection.extentOffset == 0 &&
  522. !_hasFocus;
  523. if (nextSelection == _selection &&
  524. cause != SelectionChangedCause.keyboard &&
  525. !focusingEmpty) {
  526. return;
  527. }
  528. onSelectionChanged(nextSelection, cause);
  529. }
  530. void _paintHandleLayers(
  531. PaintingContext context, List<TextSelectionPoint> endpoints) {
  532. var startPoint = endpoints[0].point;
  533. startPoint = Offset(
  534. startPoint.dx.clamp(0.0, size.width),
  535. startPoint.dy.clamp(0.0, size.height),
  536. );
  537. context.pushLayer(
  538. LeaderLayer(link: _startHandleLayerLink, offset: startPoint),
  539. super.paint,
  540. Offset.zero,
  541. );
  542. if (endpoints.length == 2) {
  543. var endPoint = endpoints[1].point;
  544. endPoint = Offset(
  545. endPoint.dx.clamp(0.0, size.width),
  546. endPoint.dy.clamp(0.0, size.height),
  547. );
  548. context.pushLayer(
  549. LeaderLayer(link: _endHandleLayerLink, offset: endPoint),
  550. super.paint,
  551. Offset.zero,
  552. );
  553. }
  554. }
  555. }