diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index a1c71d06..c79ffdb3 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -18,6 +18,7 @@ import 'package:flutter_quill_extensions/presentation/embeds/widgets/image.dart' import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import '../widgets/custom_magnifer.dart'; import '../widgets/time_stamp_embed_widget.dart'; import 'read_only_page.dart'; @@ -405,6 +406,11 @@ class _HomePageState extends State { } return QuillEditor( configurations: QuillEditorConfigurations( + magnifierConfiguration: TextMagnifierConfiguration( + magnifierBuilder: (_, __, magnifierInfo) => CustomMagnifier( + magnifierInfo: magnifierInfo, + ), + ), builder: (context, rawEditor) { return DropTarget( onDragDone: _onDragDone, diff --git a/example/lib/widgets/custom_magnifer.dart b/example/lib/widgets/custom_magnifer.dart new file mode 100644 index 00000000..eb283697 --- /dev/null +++ b/example/lib/widgets/custom_magnifer.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class CustomMagnifier extends StatelessWidget { + const CustomMagnifier({required this.magnifierInfo, super.key}); + + static const Size magnifierSize = Size(200, 200); + + // This magnifier will consume some text data and position itself + // based on the info in the magnifier. + final ValueNotifier magnifierInfo; + + @override + Widget build(BuildContext context) { + // Use a value listenable builder because we want to rebuild + // every time the text selection info changes. + // `CustomMagnifier` could also be a `StatefulWidget` and call `setState` + // when `magnifierInfo` updates. This would be useful for more complex + // positioning cases. + return ValueListenableBuilder( + valueListenable: magnifierInfo, + builder: (context, currentMagnifierInfo, _) { + // We want to position the magnifier at the global position of the gesture. + var magnifierPosition = currentMagnifierInfo.globalGesturePosition; + + // You may use the `MagnifierInfo` however you'd like: + // In this case, we make sure the magnifier never goes out of the current line bounds. + magnifierPosition = Offset( + clampDouble( + magnifierPosition.dx, + currentMagnifierInfo.currentLineBoundaries.left, + currentMagnifierInfo.currentLineBoundaries.right, + ), + clampDouble( + magnifierPosition.dy, + currentMagnifierInfo.currentLineBoundaries.top, + currentMagnifierInfo.currentLineBoundaries.bottom, + ), + ); + + // Finally, align the magnifier to the bottom center. The initial anchor is + // the top left, so subtract bottom center alignment. + magnifierPosition -= Alignment.bottomCenter.alongSize(magnifierSize); + + return Positioned( + left: magnifierPosition.dx, + top: magnifierPosition.dy, + child: RawMagnifier( + magnificationScale: 2, + // The focal point starts at the center of the magnifier. + // We probably want to point below the magnifier, so + // offset the focal point by half the magnifier height. + focalPointOffset: Offset(0, magnifierSize.height / 2), + // Decorate it however we'd like! + decoration: const MagnifierDecoration( + shape: StarBorder( + side: BorderSide( + color: Colors.green, + width: 2, + ), + ), + ), + size: magnifierSize, + ), + ); + }); + } +} diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index e6475fdd..5d533304 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -129,17 +129,18 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { class RichTextProxy extends SingleChildRenderObjectWidget { /// Child argument should be an instance of RichText widget. - const RichTextProxy( - {required RichText super.child, - required this.textStyle, - required this.textAlign, - required this.textDirection, - required this.locale, - required this.strutStyle, - this.textScaleFactor = 1.0, - this.textWidthBasis = TextWidthBasis.parent, - this.textHeightBehavior, - super.key}); + const RichTextProxy({ + required RichText super.child, + required this.textStyle, + required this.textAlign, + required this.textDirection, + required this.locale, + required this.strutStyle, + this.textScaleFactor = 1.0, + this.textWidthBasis = TextWidthBasis.parent, + this.textHeightBehavior, + super.key, + }); final TextStyle textStyle; final TextAlign textAlign; @@ -153,15 +154,16 @@ class RichTextProxy extends SingleChildRenderObjectWidget { @override RenderParagraphProxy createRenderObject(BuildContext context) { return RenderParagraphProxy( - null, - textStyle, - textAlign, - textDirection, - textScaleFactor, - strutStyle, - locale, - textWidthBasis, - textHeightBehavior); + null, + textStyle, + textAlign, + textDirection, + textScaleFactor, + strutStyle, + locale, + textWidthBasis, + textHeightBehavior, + ); } @override diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 9c903ee2..82808209 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -760,7 +760,9 @@ class QuillRawEditorState extends EditorState } EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { + Line node, + BuildContext context, + ) { final textLine = TextLine( line: node, textDirection: _textDirection, @@ -775,18 +777,19 @@ class QuillRawEditorState extends EditorState customLinkPrefixes: widget.customLinkPrefixes, ); final editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - controller.selection, - widget.selectionColor, - widget.enableInteractiveSelection, - _hasFocus, - MediaQuery.devicePixelRatioOf(context), - _cursorCont); + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + controller.selection, + widget.selectionColor, + widget.enableInteractiveSelection, + _hasFocus, + MediaQuery.devicePixelRatioOf(context), + _cursorCont, + ); return editableTextLine; } @@ -1188,7 +1191,10 @@ class QuillRawEditorState extends EditorState /// This property is typically used to notify the renderer of input gestures. @override RenderEditor get renderEditor => - _editorKey.currentContext!.findRenderObject() as RenderEditor; + (_editorKey.currentContext?.findRenderObject() as RenderEditor?) ?? + (throw StateError( + "The _editorKey current context can't be null", + )); /// Express interest in interacting with the keyboard. /// diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index de0c24bb..68ca145d 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -150,8 +150,14 @@ class _TextLineState extends State { // Creates correct node for custom embed final lineStyle = _getLineStyle(widget.styles); return EmbedProxy( - embedBuilder.build(context, widget.controller, embed, widget.readOnly, - false, lineStyle), + embedBuilder.build( + context, + widget.controller, + embed, + widget.readOnly, + false, + lineStyle, + ), ); } } @@ -167,12 +173,13 @@ class _TextLineState extends State { textScaleFactor: MediaQuery.textScaleFactorOf(context), ); return RichTextProxy( - textStyle: textSpan.style!, - textAlign: textAlign, - textDirection: widget.textDirection!, - strutStyle: strutStyle, - locale: Localizations.localeOf(context), - child: child); + textStyle: textSpan.style!, + textAlign: textAlign, + textDirection: widget.textDirection!, + strutStyle: strutStyle, + locale: Localizations.localeOf(context), + child: child, + ); } InlineSpan _getTextSpanForWholeLine(BuildContext context) {