diff --git a/example/.metadata b/example/.metadata index c2aa44bdb..a4934ec1a 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "603104015dd692ea3403755b55d07813d5cf8965" + revision: "6fba2447e95c451518584c35e25f5433f14d888c" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: android - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: ios - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: linux - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: macos - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 + create_revision: 6fba2447e95c451518584c35e25f5433f14d888c + base_revision: 6fba2447e95c451518584c35e25f5433f14d888c - platform: web - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 - - platform: windows - create_revision: 603104015dd692ea3403755b55d07813d5cf8965 - base_revision: 603104015dd692ea3403755b55d07813d5cf8965 + create_revision: 6fba2447e95c451518584c35e25f5433f14d888c + base_revision: 6fba2447e95c451518584c35e25f5433f14d888c # User provided section diff --git a/example/pubspec.lock b/example/pubspec.lock index 766a88d90..25b1312ce 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -230,7 +230,7 @@ packages: path: ".." relative: true source: path - version: "11.4.1" + version: "11.4.1+fork" flutter_quill_delta_from_html: dependency: transitive description: @@ -245,7 +245,7 @@ packages: path: "../flutter_quill_extensions" relative: true source: path - version: "11.0.0" + version: "11.0.0+fork" flutter_quill_test: dependency: "direct dev" description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ac5083005..15a70538e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,8 +13,13 @@ dependencies: flutter_localizations: sdk: flutter - flutter_quill: ^11.0.0-dev.4 - flutter_quill_extensions: ^11.0.0-dev.3 + flutter_quill: + git: + url: git@github.com:singerdmx/flutter-quill.git + flutter_quill_extensions: + git: + url: git@github.com:singerdmx/flutter-quill.git + path: flutter_quill_extensions path: ^1.9.0 dev_dependencies: diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 6f5cae329..fb4dd6cfc 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill to support loading images and videos -version: 11.0.0 +version: 11.0.0+fork homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ diff --git a/lib/src/controller/quill_controller.dart b/lib/src/controller/quill_controller.dart index 0d2d291a4..0a41ff186 100644 --- a/lib/src/controller/quill_controller.dart +++ b/lib/src/controller/quill_controller.dart @@ -96,6 +96,46 @@ class QuillController extends ChangeNotifier { TextSelection get selection => _selection; TextSelection _selection; + // Drag selection tracking for smooth scrolling on web + bool _isDragging = false; + TextSelection? _previousSelection; + + bool get isDragging => _isDragging; + + void startDragSelection() { + _isDragging = true; + _previousSelection = _selection; + } + + void endDragSelection() { + _isDragging = false; + _previousSelection = null; + } + + /// Get the appropriate caret position for scrolling during drag selection + TextPosition getCaretPositionForScrolling() { + if (!_isDragging || _previousSelection == null) { + return _selection.extent; + } + + final oldSelection = _previousSelection!; + final newSelection = _selection; + + // Did the base position change? + if (oldSelection.baseOffset != newSelection.baseOffset) { + // Base changed, track the base position + return newSelection.base; + } + // Did the extent position change? + else if (oldSelection.extentOffset != newSelection.extentOffset) { + // Extent changed, track the extent position + return newSelection.extent; + } + + // If no change, use extent + return newSelection.extent; + } + /// Custom [replaceText] handler /// Return false to ignore the event ReplaceTextCallback? onReplaceText; @@ -463,6 +503,11 @@ class QuillController extends ChangeNotifier { void _updateSelection(TextSelection textSelection, {bool insertNewline = false}) { + // Drag sırasında önceki selection'ı güncelle + if (_isDragging) { + _previousSelection = _selection; + } + _selection = textSelection; final end = document.length - 1; _selection = selection.copyWith( diff --git a/lib/src/editor/config/editor_config.dart b/lib/src/editor/config/editor_config.dart index 89b0813c4..631c25292 100644 --- a/lib/src/editor/config/editor_config.dart +++ b/lib/src/editor/config/editor_config.dart @@ -42,6 +42,7 @@ class QuillEditorConfig { this.paintCursorAboveText, this.enableInteractiveSelection = true, this.enableSelectionToolbar = true, + this.paintSelectionBehindText = true, this.scrollBottomInset = 0, this.minHeight, this.maxHeight, @@ -269,6 +270,11 @@ class QuillEditorConfig { /// Whether to show the cut/copy/paste menu when selecting text. final bool enableSelectionToolbar; + /// Whether to paint the selection behind the text (true) or in front of the text (false). + /// When true, the selection color can be opaque. When false, the selection color should be transparent. + /// Defaults to true. + final bool paintSelectionBehindText; + /// The minimum height to be occupied by this editor. /// /// This only has effect if [scrollable] is set to `true` and [expands] is @@ -494,6 +500,7 @@ class QuillEditorConfig { MouseCursor? readOnlyMouseCursor, bool? enableInteractiveSelection, bool? enableSelectionToolbar, + bool? paintSelectionBehindText, double? minHeight, double? maxHeight, double? maxContentWidth, @@ -557,6 +564,8 @@ class QuillEditorConfig { enableInteractiveSelection ?? this.enableInteractiveSelection, enableSelectionToolbar: enableSelectionToolbar ?? this.enableSelectionToolbar, + paintSelectionBehindText: + paintSelectionBehindText ?? this.paintSelectionBehindText, minHeight: minHeight ?? this.minHeight, maxHeight: maxHeight ?? this.maxHeight, maxContentWidth: maxContentWidth ?? this.maxContentWidth, diff --git a/lib/src/editor/editor.dart b/lib/src/editor/editor.dart index 3f8831a3b..a2a9ae160 100644 --- a/lib/src/editor/editor.dart +++ b/lib/src/editor/editor.dart @@ -303,6 +303,7 @@ class QuillEditorState extends State expands: config.expands, autoFocus: config.autoFocus, selectionColor: selectionColor, + paintSelectionBehindText: config.paintSelectionBehindText, selectionCtrls: config.textSelectionControls ?? textSelectionControls, keyboardAppearance: config.keyboardAppearance, enableInteractiveSelection: config.enableInteractiveSelection, @@ -680,6 +681,7 @@ class RenderEditor extends RenderEditableContainerBox required this.onSelectionCompleted, required super.scrollBottomInset, required this.floatingCursorDisabled, + this.controller, ViewportOffset? offset, super.children, EdgeInsets floatingCursorAddedMargin = @@ -695,6 +697,7 @@ class RenderEditor extends RenderEditableContainerBox container: document.root, ); + final QuillController? controller; final CursorCont _cursorController; final bool floatingCursorDisabled; final bool scrollable; @@ -923,6 +926,8 @@ class RenderEditor extends RenderEditableContainerBox void handleDragStart(DragStartDetails details) { _isDragging = true; + // Notify controller that drag has started + controller?.startDragSelection(); final newSelection = selectPositionAt( from: details.globalPosition, @@ -936,6 +941,8 @@ class RenderEditor extends RenderEditableContainerBox void handleDragEnd(DragEndDetails details) { _isDragging = false; + // Notify controller that drag has ended + controller?.endDragSelection(); onSelectionCompleted(); } @@ -1233,9 +1240,19 @@ class RenderEditor extends RenderEditableContainerBox return childLocalRect.shift(Offset(0, boxParentData.offset.dy)); } - TextPosition get caretTextPosition => _floatingCursorRect == null - ? selection.extent - : _floatingCursorTextPosition; + TextPosition get caretTextPosition { + if (_floatingCursorRect != null) { + return _floatingCursorTextPosition; + } + + // Get smart caret position from controller during drag on web platform + if (kIsWeb && controller != null && controller!.isDragging) { + return controller!.getCaretPositionForScrolling(); + } + + // Use extent in normal cases (cursor position) + return selection.extent; + } // Start floating cursor diff --git a/lib/src/editor/raw_editor/config/raw_editor_config.dart b/lib/src/editor/raw_editor/config/raw_editor_config.dart index 98358d179..0f238afe7 100644 --- a/lib/src/editor/raw_editor/config/raw_editor_config.dart +++ b/lib/src/editor/raw_editor/config/raw_editor_config.dart @@ -29,6 +29,7 @@ class QuillRawEditorConfig { required this.embedBuilder, required this.textSpanBuilder, required this.autoFocus, + this.paintSelectionBehindText = true, this.characterShortcutEvents = const [], this.spaceShortcutEvents = const [], @experimental this.onKeyPressed, @@ -303,6 +304,11 @@ class QuillRawEditorConfig { /// The color to use when painting the selection. final Color selectionColor; + /// Whether to paint the selection behind the text (true) or in front of the text (false). + /// When true, the selection color can be opaque. When false, the selection color should be transparent. + /// Defaults to true. + final bool paintSelectionBehindText; + /// Delegate for building the text selection handles and toolbar. /// /// The [QuillRawEditor] widget used on its own will not trigger the display diff --git a/lib/src/editor/raw_editor/raw_editor_render_object.dart b/lib/src/editor/raw_editor/raw_editor_render_object.dart index d79536210..ab3b4c8ec 100644 --- a/lib/src/editor/raw_editor/raw_editor_render_object.dart +++ b/lib/src/editor/raw_editor/raw_editor_render_object.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ViewportOffset; +import '../../controller/quill_controller.dart'; import '../../document/document.dart'; import '../editor.dart'; import '../widgets/cursor.dart'; @@ -21,6 +22,7 @@ class QuillRawEditorMultiChildRenderObject required this.scrollBottomInset, required this.cursorController, required this.floatingCursorDisabled, + this.controller, super.key, this.padding = EdgeInsets.zero, this.maxContentWidth, @@ -42,6 +44,7 @@ class QuillRawEditorMultiChildRenderObject final double? maxContentWidth; final CursorCont cursorController; final bool floatingCursorDisabled; + final QuillController? controller; @override RenderEditor createRenderObject(BuildContext context) { @@ -61,6 +64,7 @@ class QuillRawEditorMultiChildRenderObject maxContentWidth: maxContentWidth, scrollBottomInset: scrollBottomInset, floatingCursorDisabled: floatingCursorDisabled, + controller: controller, ); } diff --git a/lib/src/editor/raw_editor/raw_editor_state.dart b/lib/src/editor/raw_editor/raw_editor_state.dart index cdc9793e5..baacd0b4c 100644 --- a/lib/src/editor/raw_editor/raw_editor_state.dart +++ b/lib/src/editor/raw_editor/raw_editor_state.dart @@ -477,6 +477,7 @@ class QuillRawEditorState extends EditorState cursorController: _cursorCont, floatingCursorDisabled: widget.config.floatingCursorDisabled, + controller: controller, children: _buildChildren(doc, context), ), ), @@ -508,6 +509,7 @@ class QuillRawEditorState extends EditorState padding: widget.config.padding, maxContentWidth: widget.config.maxContentWidth, floatingCursorDisabled: widget.config.floatingCursorDisabled, + controller: controller, children: _buildChildren(doc, context), ), ), @@ -653,6 +655,7 @@ class QuillRawEditorState extends EditorState color: widget.config.selectionColor, styles: _styles, enableInteractiveSelection: widget.config.enableInteractiveSelection, + paintSelectionBehindText: widget.config.paintSelectionBehindText, hasFocus: _hasFocus, contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) @@ -707,20 +710,22 @@ class QuillRawEditorState extends EditorState composingRange: composingRange.value, ); final editableTextLine = EditableTextLine( - node, - null, - textLine, - _getHorizontalSpacingForLine(node, _styles), - _getVerticalSpacingForLine(node, _styles), - _textDirection, - controller.selection, - widget.config.selectionColor, - widget.config.enableInteractiveSelection, - _hasFocus, - MediaQuery.devicePixelRatioOf(context), - _cursorCont, - _styles!.inlineCode!, - _getDecoration(node, _styles, attrs)); + node, + null, + textLine, + _getHorizontalSpacingForLine(node, _styles), + _getVerticalSpacingForLine(node, _styles), + _textDirection, + controller.selection, + widget.config.selectionColor, + widget.config.enableInteractiveSelection, + _hasFocus, + MediaQuery.devicePixelRatioOf(context), + _cursorCont, + _styles!.inlineCode!, + _getDecoration(node, _styles, attrs), + widget.config.paintSelectionBehindText, + ); return editableTextLine; } diff --git a/lib/src/editor/widgets/text/text_block.dart b/lib/src/editor/widgets/text/text_block.dart index 01a4208e4..6ce9a1cf3 100644 --- a/lib/src/editor/widgets/text/text_block.dart +++ b/lib/src/editor/widgets/text/text_block.dart @@ -67,6 +67,7 @@ class EditableTextBlock extends StatelessWidget { required this.color, required this.styles, required this.enableInteractiveSelection, + required this.paintSelectionBehindText, required this.hasFocus, required this.contentPadding, required this.embedBuilder, @@ -98,6 +99,7 @@ class EditableTextBlock extends StatelessWidget { final DefaultStyles? styles; final LeadingBlockNodeBuilder? customLeadingBlockBuilder; final bool enableInteractiveSelection; + final bool paintSelectionBehindText; final bool hasFocus; final EdgeInsets? contentPadding; final EmbedsBuilder embedBuilder; @@ -209,7 +211,9 @@ class EditableTextBlock extends StatelessWidget { MediaQuery.devicePixelRatioOf(context), cursorCont, styles!.inlineCode!, - null); + null, + paintSelectionBehindText, + ); final nodeTextDirection = getDirectionOfNode(line, textDirection); children.add( Directionality( diff --git a/lib/src/editor/widgets/text/text_line.dart b/lib/src/editor/widgets/text/text_line.dart index ee09dcc49..1eb788693 100644 --- a/lib/src/editor/widgets/text/text_line.dart +++ b/lib/src/editor/widgets/text/text_line.dart @@ -725,21 +725,23 @@ class _TextLineState extends State { class EditableTextLine extends RenderObjectWidget { const EditableTextLine( - this.line, - this.leading, - this.body, - this.horizontalSpacing, - this.verticalSpacing, - this.textDirection, - this.textSelection, - this.color, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.cursorCont, - this.inlineCodeStyle, - this.decoration, - {super.key}); + this.line, + this.leading, + this.body, + this.horizontalSpacing, + this.verticalSpacing, + this.textDirection, + this.textSelection, + this.color, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.cursorCont, + this.inlineCodeStyle, + this.decoration, + this.paintSelectionBehindText, { + super.key, + }); final Line line; final Widget? leading; @@ -755,6 +757,7 @@ class EditableTextLine extends RenderObjectWidget { final CursorCont cursorCont; final InlineCodeStyle inlineCodeStyle; final BoxDecoration? decoration; + final bool paintSelectionBehindText; @override RenderObjectElement createElement() { @@ -764,17 +767,19 @@ class EditableTextLine extends RenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { return RenderEditableTextLine( - line, - textDirection, - textSelection, - enableInteractiveSelection, - hasFocus, - devicePixelRatio, - _getPadding(), - color, - cursorCont, - inlineCodeStyle, - decoration); + line, + textDirection, + textSelection, + enableInteractiveSelection, + hasFocus, + devicePixelRatio, + _getPadding(), + color, + cursorCont, + inlineCodeStyle, + decoration, + paintSelectionBehindText, + ); } @override @@ -791,7 +796,10 @@ class EditableTextLine extends RenderObjectWidget { ..setDevicePixelRatio(devicePixelRatio) ..setCursorCont(cursorCont) ..setInlineCodeStyle(inlineCodeStyle) - ..setDecoration(decoration); + ..setDecoration(decoration) + ..setPaintSelectionBehindText( + paintSelectionBehindText, + ); } EdgeInsetsGeometry _getPadding() { @@ -819,6 +827,7 @@ class RenderEditableTextLine extends RenderEditableBox { this.cursorCont, this.inlineCodeStyle, this.decoration, + this.paintSelectionBehindText, ); RenderBox? _leading; @@ -838,6 +847,7 @@ class RenderEditableTextLine extends RenderEditableBox { late Rect _caretPrototype; InlineCodeStyle inlineCodeStyle; BoxDecoration? decoration; + bool paintSelectionBehindText; final Map children = {}; Iterable get _children sync* { @@ -959,6 +969,12 @@ class RenderEditableTextLine extends RenderEditableBox { markNeedsPaint(); } + void setPaintSelectionBehindText(bool newValue) { + if (paintSelectionBehindText == newValue) return; + paintSelectionBehindText = newValue; + markNeedsPaint(); + } + // Start selection implementation bool containsTextSelection() { @@ -1398,53 +1414,105 @@ class RenderEditableTextLine extends RenderEditableBox { } } - if (hasFocus && - cursorCont.show.value && - containsCursor() && - !cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset, line.hasEmbed); - } + // Paint selection and text based on paintSelectionBehindText setting + if (paintSelectionBehindText) { + // Paint selection behind text (new behavior) + if (enableInteractiveSelection && + line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1) { + final local = localSelection(line, textSelection, false); + _selectedRects ??= _body!.getBoxesForSelection( + local, + ); - context.paintChild(_body!, effectiveOffset); + // Paint a small rect at the start of empty lines that + // are contained by the selection. + if (line.isEmpty && + textSelection.baseOffset <= line.offset && + textSelection.extentOffset > line.offset) { + final lineHeight = preferredLineHeight( + TextPosition( + offset: line.offset, + ), + ); + _selectedRects?.add( + TextBox.fromLTRBD( + 0, + 0, + 3, + lineHeight, + textDirection, + ), + ); + } - if (hasFocus && - cursorCont.show.value && - containsCursor() && - cursorCont.style.paintAboveText) { - _paintCursor(context, effectiveOffset, line.hasEmbed); - } + _paintSelection(context, effectiveOffset); + } - // paint the selection on the top - if (enableInteractiveSelection && - line.documentOffset <= textSelection.end && - textSelection.start <= line.documentOffset + line.length - 1) { - final local = localSelection(line, textSelection, false); - _selectedRects ??= _body!.getBoxesForSelection( - local, - ); + if (hasFocus && + cursorCont.show.value && + containsCursor() && + !cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset, line.hasEmbed); + } - // Paint a small rect at the start of empty lines that - // are contained by the selection. - if (line.isEmpty && - textSelection.baseOffset <= line.offset && - textSelection.extentOffset > line.offset) { - final lineHeight = preferredLineHeight( - TextPosition( - offset: line.offset, - ), - ); - _selectedRects?.add( - TextBox.fromLTRBD( - 0, - 0, - 3, - lineHeight, - textDirection, - ), - ); + context.paintChild(_body!, effectiveOffset); + + if (hasFocus && + cursorCont.show.value && + containsCursor() && + cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset, line.hasEmbed); } + } else { + // Paint selection in front of text (old behavior) + if (hasFocus && + cursorCont.show.value && + containsCursor() && + !cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset, line.hasEmbed); + } + + context.paintChild(_body!, effectiveOffset); - _paintSelection(context, effectiveOffset); + if (hasFocus && + cursorCont.show.value && + containsCursor() && + cursorCont.style.paintAboveText) { + _paintCursor(context, effectiveOffset, line.hasEmbed); + } + + if (enableInteractiveSelection && + line.documentOffset <= textSelection.end && + textSelection.start <= line.documentOffset + line.length - 1) { + final local = localSelection(line, textSelection, false); + _selectedRects ??= _body!.getBoxesForSelection( + local, + ); + + // Paint a small rect at the start of empty lines that + // are contained by the selection. + if (line.isEmpty && + textSelection.baseOffset <= line.offset && + textSelection.extentOffset > line.offset) { + final lineHeight = preferredLineHeight( + TextPosition( + offset: line.offset, + ), + ); + _selectedRects?.add( + TextBox.fromLTRBD( + 0, + 0, + 3, + lineHeight, + textDirection, + ), + ); + } + + _paintSelection(context, effectiveOffset); + } } } } diff --git a/pubspec.yaml b/pubspec.yaml index b0decd6da..320d6dddb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: "A rich text editor built for Android, iOS, Web, and desktop platforms. It's the WYSIWYG editor and a Quill component for Flutter." -version: 11.4.1 +version: 11.4.1+fork homepage: https://github.com/singerdmx/flutter-quill/ repository: https://github.com/singerdmx/flutter-quill/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/