diff --git a/lib/src/widgets/editor/editor.dart b/lib/src/widgets/editor/editor.dart index 7af8da25..c2f823d8 100644 --- a/lib/src/widgets/editor/editor.dart +++ b/lib/src/widgets/editor/editor.dart @@ -1386,6 +1386,13 @@ class RenderEditor extends RenderEditableContainerBox ); } + /// Returns the TextPosition after moving by the vertical offset. + TextPosition getTextPositionMoveVertical( + TextPosition position, double verticalOffset) { + final caretOfs = localToGlobal(_getOffsetForCaret(position)); + return getPositionForOffset(caretOfs.translate(0, verticalOffset)); + } + /// Returns the TextPosition above the given offset into the text. /// /// If the offset is already on the first line, the offset of the first @@ -1436,11 +1443,10 @@ class RenderEditor extends RenderEditableContainerBox var newPosition = child.getPositionBelow(localPosition); if (newPosition == null) { - // There was no text above in the current child, check the direct - // sibling. + // There was no text below in the current child, check the direct sibling. final sibling = childAfter(child); if (sibling == null) { - // reached beginning of the document, move to the + // reached end of the document, move to the // last character newPosition = TextPosition(offset: document.length - 1); } else { @@ -1503,6 +1509,11 @@ class QuillVerticalCaretMovementRun implements Iterator { _currentTextPosition = _editor.getTextPositionAbove(_currentTextPosition); return true; } + + void moveVertical(double verticalOffset) { + _currentTextPosition = _editor.getTextPositionMoveVertical( + _currentTextPosition, verticalOffset); + } } class EditableContainerParentData @@ -1570,7 +1581,6 @@ class RenderEditableContainerBox extends RenderBox RenderEditableBox childAtPosition(TextPosition position) { assert(firstChild != null); - final targetNode = container.queryChild(position.offset, false).node; var targetChild = firstChild; @@ -1580,6 +1590,8 @@ class RenderEditableContainerBox extends RenderBox } final newChild = childAfter(targetChild); if (newChild == null) { + // At start of document fails to find the position + targetChild = childAtOffset(const Offset(0, 0)); break; } targetChild = newChild; diff --git a/lib/src/widgets/quill/text_block.dart b/lib/src/widgets/quill/text_block.dart index c0b6b187..e3de668c 100644 --- a/lib/src/widgets/quill/text_block.dart +++ b/lib/src/widgets/quill/text_block.dart @@ -691,7 +691,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox @override TextPosition globalToLocalPosition(TextPosition position) { - assert(container.containsOffset(position.offset), + assert(container.containsOffset(position.offset) || container.length == 0, 'The provided text position is not in the current node'); return TextPosition( offset: position.offset - container.documentOffset, diff --git a/lib/src/widgets/quill/text_line.dart b/lib/src/widgets/quill/text_line.dart index 14ae2a09..a9f2a321 100644 --- a/lib/src/widgets/quill/text_line.dart +++ b/lib/src/widgets/quill/text_line.dart @@ -924,7 +924,13 @@ class RenderEditableTextLine extends RenderEditableBox { @override TextPosition? getPositionAbove(TextPosition position) { - return _getPosition(position, -0.5); + /// Move up by fraction of the default font height, larger font sizes need larger offset + for (var offset = -0.5;; offset -= 0.25) { + final pos = _getPosition(position, offset); + if (pos != position || offset <= -2.0) { + return pos; + } + } } @override diff --git a/lib/src/widgets/raw_editor/raw_editor_actions.dart b/lib/src/widgets/raw_editor/raw_editor_actions.dart index 82645ac1..a6e3fb2f 100644 --- a/lib/src/widgets/raw_editor/raw_editor_actions.dart +++ b/lib/src/widgets/raw_editor/raw_editor_actions.dart @@ -608,3 +608,97 @@ class NavigateToDocumentBoundaryAction ); } } + +/// An [Action] that scrolls the Quill editor scroll bar by the amount configured +/// in the [ScrollIntent] given to it. +/// +/// The default for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the +/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical pixels. +/// Modelled on 'class ScrollAction' in flutter's scrollable_helpers.dart +class QuillEditorScrollAction extends ContextAction { + QuillEditorScrollAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(ScrollIntent intent, [BuildContext? context]) { + final sc = state.scrollController; + final increment = switch (intent.type) { + ScrollIncrementType.line => 50.0, + ScrollIncrementType.page => 0.8 * sc.position.viewportDimension, + }; + sc.position.moveTo( + sc.position.pixels + + (intent.direction == AxisDirection.down ? increment : -increment), + duration: const Duration(milliseconds: 100), + curve: Curves.easeInOut, + ); + } +} + +/// An [Action] that moves the caret by a page. +/// +/// The default movement is 80% of the size of the scroll window. +/// Modelled on 'class _UpdateTextSelectionVerticallyAction' in flutter's editable_text.dart +class QuillEditorUpdateTextSelectionToAdjacentPageAction< + T extends DirectionalCaretMovementIntent> extends ContextAction { + QuillEditorUpdateTextSelectionToAdjacentPageAction(this.state); + + final QuillRawEditorState state; + + QuillVerticalCaretMovementRun? _verticalMovementRun; + TextSelection? _runSelection; + + void stopCurrentVerticalRunIfSelectionChanges() { + final runSelection = _runSelection; + if (runSelection == null) { + assert(_verticalMovementRun == null); + return; + } + _runSelection = state.textEditingValue.selection; + final currentSelection = state.controller.selection; + final continueCurrentRun = currentSelection.isValid && + currentSelection.isCollapsed && + currentSelection.baseOffset == runSelection.baseOffset && + currentSelection.extentOffset == runSelection.extentOffset; + if (!continueCurrentRun) { + _verticalMovementRun = null; + _runSelection = null; + } + } + + @override + void invoke(T intent, [BuildContext? context]) { + assert(state.textEditingValue.selection.isValid); + + final collapseSelection = intent.collapseSelection || + !state.widget.configurations.selectionEnabled; + final value = state.textEditingValue; + if (!value.selection.isValid) { + return; + } + + final currentRun = state.renderEditor + .startVerticalCaretMovement(state.renderEditor.selection.extent); + + final pageOffset = 0.8 * state.scrollController.position.viewportDimension; + currentRun.moveVertical(intent.forward ? pageOffset : -pageOffset); + final newExtent = currentRun.current; + final newSelection = collapseSelection + ? TextSelection.fromPosition(newExtent) + : value.selection.extendTo(newExtent); + + Actions.invoke( + context!, + UpdateSelectionIntent( + value, newSelection, SelectionChangedCause.keyboard), + ); + if (state.textEditingValue.selection == newSelection) { + _verticalMovementRun = currentRun; + _runSelection = newSelection; + } + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 963de027..7b7b6646 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -713,6 +713,30 @@ class QuillRawEditorState extends EditorState control: !isDesktopMacOS, meta: isDesktopMacOS, ): const ScrollToDocumentBoundaryIntent(forward: true), + + // Arrow key scrolling + SingleActivator( + LogicalKeyboardKey.arrowUp, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ScrollIntent(direction: AxisDirection.up), + SingleActivator( + LogicalKeyboardKey.arrowDown, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ScrollIntent(direction: AxisDirection.down), + SingleActivator( + LogicalKeyboardKey.pageUp, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ScrollIntent( + direction: AxisDirection.up, type: ScrollIncrementType.page), + SingleActivator( + LogicalKeyboardKey.pageDown, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ScrollIntent( + direction: AxisDirection.down, type: ScrollIncrementType.page), }, { ...?widget.configurations.customShortcuts }), @@ -1624,6 +1648,10 @@ class QuillRawEditorState extends EditorState QuillEditorUpdateTextSelectionToAdjacentLineAction< ExtendSelectionVerticallyToAdjacentLineIntent>(this); + late final _adjacentPageAction = + QuillEditorUpdateTextSelectionToAdjacentPageAction< + ExtendSelectionVerticallyToAdjacentPageIntent>(this); + late final QuillEditorToggleTextStyleAction _formatSelectionAction = QuillEditorToggleTextStyleAction(this); @@ -1697,7 +1725,11 @@ class QuillRawEditorState extends EditorState QuillEditorApplyHeaderIntent: _applyHeaderAction, QuillEditorApplyCheckListIntent: _applyCheckListAction, QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(this), - ScrollToDocumentBoundaryIntent: NavigateToDocumentBoundaryAction(this) + ScrollToDocumentBoundaryIntent: NavigateToDocumentBoundaryAction(this), + + // Paging and scrolling + ExtendSelectionVerticallyToAdjacentPageIntent: _adjacentPageAction, + ScrollIntent: QuillEditorScrollAction(this), }; @override