From cd4c64adcd3a921f1db7cb7ad455b4402d3c0c44 Mon Sep 17 00:00:00 2001 From: Brendan Ejike Date: Thu, 15 Jul 2021 18:47:19 +0100 Subject: [PATCH 1/7] Add support for 'SelectableText' --- lib/auto_size_text.dart | 2 + lib/src/auto_size_text.dart | 270 +++++++++++++++++++++++++++++++++++- 2 files changed, 270 insertions(+), 2 deletions(-) diff --git a/lib/auto_size_text.dart b/lib/auto_size_text.dart index 81bb4c4..dc2f3ec 100644 --- a/lib/auto_size_text.dart +++ b/lib/auto_size_text.dart @@ -4,6 +4,8 @@ library auto_size_text; import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' show SelectableText; import 'package:flutter/widgets.dart'; part 'src/auto_size_text.dart'; diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index ec43838..a3aa096 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -33,6 +33,21 @@ class AutoSizeText extends StatefulWidget { this.maxLines, this.semanticsLabel, }) : textSpan = null, + autofocus = false, + showCursor = false, + cursorWidth = 2.0, + cursorHeight = null, + cursorRadius = null, + focusNode = null, + cursorColor = null, + enableInteractiveSelection = false, + selectionControls = null, + dragStartBehavior = DragStartBehavior.start, + toolbarOptions = null, + onTap = null, + scrollPhysics = null, + onSelectionChanged = null, + _isSelectableText = false, super(key: key); /// Creates a [AutoSizeText] widget with a [TextSpan]. @@ -58,6 +73,108 @@ class AutoSizeText extends StatefulWidget { this.maxLines, this.semanticsLabel, }) : data = null, + autofocus = false, + showCursor = false, + cursorWidth = 2.0, + cursorHeight = null, + cursorRadius = null, + focusNode = null, + cursorColor = null, + enableInteractiveSelection = false, + selectionControls = null, + dragStartBehavior = DragStartBehavior.start, + toolbarOptions = null, + onTap = null, + scrollPhysics = null, + onSelectionChanged = null, + _isSelectableText = false, + super(key: key); + + /// Creates a selectable [AutoSizeText] widget with a [TextSpan]. + const AutoSizeText.richSelectable( + this.textSpan, { + Key? key, + this.textKey, + this.style, + this.strutStyle, + this.minFontSize = 12, + this.maxFontSize = double.infinity, + this.stepGranularity = 1, + this.presetFontSizes, + this.group, + this.textAlign, + this.textDirection, + this.wrapWords = true, + this.textScaleFactor, + this.maxLines, + this.autofocus = false, + this.showCursor = false, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.focusNode, + this.cursorColor, + this.enableInteractiveSelection = true, + this.selectionControls, + this.dragStartBehavior = DragStartBehavior.start, + this.toolbarOptions, + this.onTap, + this.scrollPhysics, + this.onSelectionChanged, + }) : assert(textSpan != null, + 'A non-null TextSpan must be provided to a AutoSizeText.rich widget.'), + data = null, + locale = null, + softWrap = null, + overflow = null, + overflowReplacement = null, + semanticsLabel = null, + _isSelectableText = true, + super(key: key); + + /// Creates a selectable [AutoSizeText] widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + const AutoSizeText.selectable( + this.data, { + Key? key, + this.textKey, + this.style, + this.strutStyle, + this.minFontSize = 12, + this.maxFontSize = double.infinity, + this.stepGranularity = 1, + this.presetFontSizes, + this.group, + this.textAlign, + this.textDirection, + this.wrapWords = true, + this.textScaleFactor, + this.maxLines, + this.autofocus = false, + this.showCursor = false, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.focusNode, + this.cursorColor, + this.enableInteractiveSelection = true, + this.selectionControls, + this.dragStartBehavior = DragStartBehavior.start, + this.toolbarOptions, + this.onTap, + this.scrollPhysics, + this.onSelectionChanged, + }) : assert(data != null, + 'A non-null String must be provided to a AutoSizeText widget.'), + textSpan = null, + locale = null, + softWrap = null, + overflow = null, + overflowReplacement = null, + semanticsLabel = null, + _isSelectableText = true, super(key: key); /// Sets the key for the resulting [Text] widget. @@ -215,6 +332,99 @@ class AutoSizeText extends StatefulWidget { /// ``` final String? semanticsLabel; + final bool _isSelectableText; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// The color to use when painting the cursor. + /// + /// Defaults to the theme's `cursorColor` when null. + final Color? cursorColor; + + /// {@macro flutter.widgets.editableText.cursorHeight} + final double? cursorHeight; + + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius? cursorRadius; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// Whether to enable user interface affordances for changing the text selection. + /// + /// For example, setting this to true will enable features such as long-pressing the TextField to select text and show the cut/copy/paste menu, and tapping to move the text caret. + /// + /// When this is false, the text selection cannot be adjusted by the user, text cannot be copied, and the user cannot paste into the text field from the clipboard. + final bool enableInteractiveSelection; + + /// Defines the focus for this widget. + /// + /// Text is only selectable when widget is focused. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// focusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + final FocusNode? focusNode; + + /// {@macro flutter.widgets.editableText.onSelectionChanged} + final SelectionChangedCallback? onSelectionChanged; + + /// Called when the user taps on this selectable text. + /// + /// The selectable text builds a [GestureDetector] to handle input events like tap, + /// to trigger focus requests, to move the caret, adjust the selection, etc. + /// Handling some of those events by wrapping the selectable text with a competing + /// GestureDetector is problematic. + /// + /// To unconditionally handle taps, without interfering with the selectable text's + /// internal gesture detector, provide this callback. + /// + /// To be notified when the text field gains or loses the focus, provide a + /// [focusNode] and add a listener to that. + /// + /// To listen to arbitrary pointer events without competing with the + /// selectable text's internal gesture detector, use a [Listener]. + final GestureTapCallback? onTap; + + /// The ScrollPhysics to use when vertically scrolling the input. + /// + /// If not specified, it will behave according to the current platform. + final ScrollPhysics? scrollPhysics; + + /// {@macro flutter.widgets.editableText.selectionControls} + final TextSelectionControls? selectionControls; + + /// {@macro flutter.widgets.editableText.showCursor} + final bool showCursor; + + /// Configuration of toolbar options. + /// + /// Paste and cut will be disabled regardless. + /// + /// If not set, select all and copy will be enabled by default. + final ToolbarOptions? toolbarOptions; + @override _AutoSizeTextState createState() => _AutoSizeTextState(); } @@ -276,8 +486,10 @@ class _AutoSizeTextState extends State { } void _validateProperties(TextStyle style, int? maxLines) { - assert(widget.overflow == null || widget.overflowReplacement == null, - 'Either overflow or overflowReplacement must be null.'); + if (!widget._isSelectableText) { + assert(widget.overflow == null || widget.overflowReplacement == null, + 'Either overflow or overflowReplacement must be null.'); + } assert(maxLines == null || maxLines > 0, 'MaxLines must be greater than or equal to 1.'); assert(widget.key == null || widget.key != widget.textKey, @@ -411,6 +623,60 @@ class _AutoSizeTextState extends State { } Widget _buildText(double fontSize, TextStyle style, int? maxLines) { + if (widget._isSelectableText) { + if (widget.data != null) { + return SelectableText( + widget.data!, + key: widget.textKey, + style: style.copyWith(fontSize: fontSize), + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textDirection: widget.textDirection, + textScaleFactor: 1, + maxLines: maxLines, + autofocus: widget.autofocus, + cursorColor: widget.cursorColor, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + dragStartBehavior: widget.dragStartBehavior, + enableInteractiveSelection: widget.enableInteractiveSelection, + focusNode: widget.focusNode, + onSelectionChanged: widget.onSelectionChanged, + onTap: widget.onTap, + scrollPhysics: widget.scrollPhysics, + selectionControls: widget.selectionControls, + showCursor: widget.showCursor, + toolbarOptions: widget.toolbarOptions, + ); + } else { + return SelectableText.rich( + widget.textSpan!, + key: widget.textKey, + style: style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textDirection: widget.textDirection, + textScaleFactor: fontSize / style.fontSize!, + maxLines: maxLines, + autofocus: widget.autofocus, + cursorColor: widget.cursorColor, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + dragStartBehavior: widget.dragStartBehavior, + enableInteractiveSelection: widget.enableInteractiveSelection, + focusNode: widget.focusNode, + onSelectionChanged: widget.onSelectionChanged, + onTap: widget.onTap, + scrollPhysics: widget.scrollPhysics, + selectionControls: widget.selectionControls, + showCursor: widget.showCursor, + toolbarOptions: widget.toolbarOptions, + ); + } + } + if (widget.data != null) { return Text( widget.data!, From eefe24855aad30942967c530d2a6e1bf94571829 Mon Sep 17 00:00:00 2001 From: definitelyme Date: Thu, 24 Mar 2022 09:15:53 +0100 Subject: [PATCH 2/7] Fix extra padding caused by cursor width & caret gap --- lib/src/auto_size_text.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index a3aa096..261a39a 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -615,7 +615,11 @@ class _AutoSizeTextState extends State { strutStyle: widget.strutStyle, ); - textPainter.layout(maxWidth: constraints.maxWidth); + if (widget._isSelectableText) { + textPainter.layout(maxWidth: constraints.maxWidth - widget.cursorWidth - 1.0); + } else { + textPainter.layout(maxWidth: constraints.maxWidth); + } return !(textPainter.didExceedMaxLines || textPainter.height > constraints.maxHeight || From 1581dd29077b298e4d800bab3db984f9b9add776 Mon Sep 17 00:00:00 2001 From: definitelyme Date: Wed, 13 Jul 2022 12:00:12 +0100 Subject: [PATCH 3/7] Add minLines & semanticsLabel property --- lib/src/auto_size_text.dart | 61 +++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index 261a39a..8c8ccfd 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -47,6 +47,7 @@ class AutoSizeText extends StatefulWidget { onTap = null, scrollPhysics = null, onSelectionChanged = null, + minLines = null, _isSelectableText = false, super(key: key); @@ -87,6 +88,7 @@ class AutoSizeText extends StatefulWidget { onTap = null, scrollPhysics = null, onSelectionChanged = null, + minLines = null, _isSelectableText = false, super(key: key); @@ -106,6 +108,7 @@ class AutoSizeText extends StatefulWidget { this.textDirection, this.wrapWords = true, this.textScaleFactor, + this.minLines, this.maxLines, this.autofocus = false, this.showCursor = false, @@ -121,8 +124,7 @@ class AutoSizeText extends StatefulWidget { this.onTap, this.scrollPhysics, this.onSelectionChanged, - }) : assert(textSpan != null, - 'A non-null TextSpan must be provided to a AutoSizeText.rich widget.'), + }) : assert(textSpan != null, 'A non-null TextSpan must be provided to a AutoSizeText.rich widget.'), data = null, locale = null, softWrap = null, @@ -151,6 +153,7 @@ class AutoSizeText extends StatefulWidget { this.textDirection, this.wrapWords = true, this.textScaleFactor, + this.minLines, this.maxLines, this.autofocus = false, this.showCursor = false, @@ -166,8 +169,7 @@ class AutoSizeText extends StatefulWidget { this.onTap, this.scrollPhysics, this.onSelectionChanged, - }) : assert(data != null, - 'A non-null String must be provided to a AutoSizeText widget.'), + }) : assert(data != null, 'A non-null String must be provided to a AutoSizeText widget.'), textSpan = null, locale = null, softWrap = null, @@ -318,6 +320,9 @@ class AutoSizeText extends StatefulWidget { /// widget directly to entirely override the [DefaultTextStyle]. final int? maxLines; + /// {@macro flutter.widgets.editableText.minLines} + final int? minLines; + /// An alternative semantics label for this text. /// /// If present, the semantics of this widget will contain this value instead @@ -332,6 +337,7 @@ class AutoSizeText extends StatefulWidget { /// ``` final String? semanticsLabel; + /// Enable/disable selectable text. final bool _isSelectableText; /// {@macro flutter.widgets.editableText.autofocus} @@ -487,38 +493,29 @@ class _AutoSizeTextState extends State { void _validateProperties(TextStyle style, int? maxLines) { if (!widget._isSelectableText) { - assert(widget.overflow == null || widget.overflowReplacement == null, - 'Either overflow or overflowReplacement must be null.'); + assert(widget.overflow == null || widget.overflowReplacement == null, 'Either overflow or overflowReplacement must be null.'); } - assert(maxLines == null || maxLines > 0, - 'MaxLines must be greater than or equal to 1.'); - assert(widget.key == null || widget.key != widget.textKey, - 'Key and textKey must not be equal.'); + assert(maxLines == null || maxLines > 0, 'MaxLines must be greater than or equal to 1.'); + assert(widget.key == null || widget.key != widget.textKey, 'Key and textKey must not be equal.'); if (widget.presetFontSizes == null) { assert( widget.stepGranularity >= 0.1, 'StepGranularity must be greater than or equal to 0.1. It is not a ' 'good idea to resize the font with a higher accuracy.'); - assert(widget.minFontSize >= 0, - 'MinFontSize must be greater than or equal to 0.'); + assert(widget.minFontSize >= 0, 'MinFontSize must be greater than or equal to 0.'); assert(widget.maxFontSize > 0, 'MaxFontSize has to be greater than 0.'); - assert(widget.minFontSize <= widget.maxFontSize, - 'MinFontSize must be smaller or equal than maxFontSize.'); - assert(widget.minFontSize / widget.stepGranularity % 1 == 0, - 'MinFontSize must be a multiple of stepGranularity.'); + assert(widget.minFontSize <= widget.maxFontSize, 'MinFontSize must be smaller or equal than maxFontSize.'); + assert(widget.minFontSize / widget.stepGranularity % 1 == 0, 'MinFontSize must be a multiple of stepGranularity.'); if (widget.maxFontSize != double.infinity) { - assert(widget.maxFontSize / widget.stepGranularity % 1 == 0, - 'MaxFontSize must be a multiple of stepGranularity.'); + assert(widget.maxFontSize / widget.stepGranularity % 1 == 0, 'MaxFontSize must be a multiple of stepGranularity.'); } } else { - assert(widget.presetFontSizes!.isNotEmpty, - 'PresetFontSizes must not be empty.'); + assert(widget.presetFontSizes!.isNotEmpty, 'PresetFontSizes must not be empty.'); } } - List _calculateFontSize( - BoxConstraints size, TextStyle? style, int? maxLines) { + List _calculateFontSize(BoxConstraints size, TextStyle? style, int? maxLines) { final span = TextSpan( style: widget.textSpan?.style ?? style, text: widget.textSpan?.text ?? widget.data, @@ -526,16 +523,14 @@ class _AutoSizeTextState extends State { recognizer: widget.textSpan?.recognizer, ); - final userScale = - widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + final userScale = widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); int left; int right; final presetFontSizes = widget.presetFontSizes?.reversed.toList(); if (presetFontSizes == null) { - final num defaultFontSize = - style!.fontSize!.clamp(widget.minFontSize, widget.maxFontSize); + final num defaultFontSize = style!.fontSize!.clamp(widget.minFontSize, widget.maxFontSize); final defaultScale = defaultFontSize * userScale / style.fontSize!; if (_checkTextFits(span, defaultScale, maxLines, size)) { return [defaultFontSize * userScale, true]; @@ -579,8 +574,7 @@ class _AutoSizeTextState extends State { return [fontSize, lastValueFits]; } - bool _checkTextFits( - TextSpan text, double scale, int? maxLines, BoxConstraints constraints) { + bool _checkTextFits(TextSpan text, double scale, int? maxLines, BoxConstraints constraints) { if (!widget.wrapWords) { final words = text.toPlainText().split(RegExp('\\s+')); @@ -599,8 +593,7 @@ class _AutoSizeTextState extends State { wordWrapTextPainter.layout(maxWidth: constraints.maxWidth); - if (wordWrapTextPainter.didExceedMaxLines || - wordWrapTextPainter.width > constraints.maxWidth) { + if (wordWrapTextPainter.didExceedMaxLines || wordWrapTextPainter.width > constraints.maxWidth) { return false; } } @@ -621,9 +614,7 @@ class _AutoSizeTextState extends State { textPainter.layout(maxWidth: constraints.maxWidth); } - return !(textPainter.didExceedMaxLines || - textPainter.height > constraints.maxHeight || - textPainter.width > constraints.maxWidth); + return !(textPainter.didExceedMaxLines || textPainter.height > constraints.maxHeight || textPainter.width > constraints.maxWidth); } Widget _buildText(double fontSize, TextStyle style, int? maxLines) { @@ -637,6 +628,7 @@ class _AutoSizeTextState extends State { textAlign: widget.textAlign, textDirection: widget.textDirection, textScaleFactor: 1, + minLines: widget.minLines, maxLines: maxLines, autofocus: widget.autofocus, cursorColor: widget.cursorColor, @@ -652,6 +644,7 @@ class _AutoSizeTextState extends State { selectionControls: widget.selectionControls, showCursor: widget.showCursor, toolbarOptions: widget.toolbarOptions, + semanticsLabel: widget.semanticsLabel, ); } else { return SelectableText.rich( @@ -662,6 +655,7 @@ class _AutoSizeTextState extends State { textAlign: widget.textAlign, textDirection: widget.textDirection, textScaleFactor: fontSize / style.fontSize!, + minLines: widget.minLines, maxLines: maxLines, autofocus: widget.autofocus, cursorColor: widget.cursorColor, @@ -677,6 +671,7 @@ class _AutoSizeTextState extends State { selectionControls: widget.selectionControls, showCursor: widget.showCursor, toolbarOptions: widget.toolbarOptions, + semanticsLabel: widget.semanticsLabel, ); } } From 96fc9a375f300cfcf9c29175ee9201fe45b4a244 Mon Sep 17 00:00:00 2001 From: definitelyme Date: Wed, 13 Jul 2022 12:00:59 +0100 Subject: [PATCH 4/7] Add SelectableTextDemo --- demo/lib/main.dart | 12 ++++++ demo/lib/selectable_text_demo.dart | 62 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 demo/lib/selectable_text_demo.dart diff --git a/demo/lib/main.dart b/demo/lib/main.dart index a6c2a18..d3b9db0 100644 --- a/demo/lib/main.dart +++ b/demo/lib/main.dart @@ -7,6 +7,7 @@ import 'max_lines_demo.dart'; import 'min_font_size_demo.dart'; import 'overflow_replacement_demo.dart'; import 'preset_font_sizes_demo.dart'; +import 'selectable_text_demo.dart'; import 'step_granularity.dart'; import 'sync_demo.dart'; @@ -43,6 +44,7 @@ List colors = [ Colors.lightBlue, Colors.green, Colors.blueGrey, + Colors.teal, ]; List demoNames = [ @@ -52,6 +54,7 @@ List demoNames = [ 'StepGranularity', 'PresetFontSizes', 'OverflowReplacement', + 'Text Selection', ]; class _DemoAppState extends State { @@ -138,6 +141,11 @@ class _DemoAppState extends State { title: Text('replacement'), activeColor: colors[5], ), + BottomNavyBarItem( + icon: Icon(MdiIcons.selection), + title: Text('selection'), + activeColor: colors[6], + ), ], ), ); @@ -155,6 +163,10 @@ class _DemoAppState extends State { return StepGranularityDemo(_richText); case 4: return PresetFontSizesDemo(_richText); + case 5: + return OverflowReplacementDemo(_richText); + case 6: + return SelectableTextDemo(_richText); default: return OverflowReplacementDemo(_richText); } diff --git a/demo/lib/selectable_text_demo.dart b/demo/lib/selectable_text_demo.dart new file mode 100644 index 0000000..ad9f059 --- /dev/null +++ b/demo/lib/selectable_text_demo.dart @@ -0,0 +1,62 @@ +library selectable_text_demo.dart; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; + +import 'animated_input.dart'; +import 'text_card.dart'; +import 'utils.dart'; + +/// A stateful widget to render SelectableTextDemo. +class SelectableTextDemo extends StatelessWidget { + const SelectableTextDemo(this.richText, {Key? key}) : super(key: key); + + final bool richText; + + @override + Widget build(BuildContext context) { + return AnimatedInput( + text: '"AutoSizeText.selectable" & "AutoSizeText.richSelectable"' + ' widget displays a string of text with a single style' + ' or paragraphs with differently styled TextSpans.' + ' It is similar to "SelectableText" & "SelectableText.rich", but uses AutoSizeText to render.', + builder: (input) { + return Row( + children: [ + Expanded( + child: TextCard( + title: 'AutoSizeText', + child: !richText + ? AutoSizeText( + input, + style: TextStyle(fontSize: 30), + ) + : AutoSizeText.rich( + spanFromString(input), + style: TextStyle(fontSize: 30), + ), + ), + ), + SizedBox(width: 10), + Expanded( + child: TextCard( + title: 'AutoSizeText.selectable', + child: !richText + ? AutoSizeText.selectable( + input, + style: TextStyle(fontSize: 30), + maxLines: 2, + ) + : AutoSizeText.richSelectable( + spanFromString(input), + style: TextStyle(fontSize: 30), + maxLines: 2, + ), + ), + ), + ], + ); + }, + ); + } +} From f8dadaafe07f614608307abf45249bbf55c780fc Mon Sep 17 00:00:00 2001 From: definitelyme Date: Wed, 13 Jul 2022 12:00:59 +0100 Subject: [PATCH 5/7] Add SelectableTextDemo --- demo/lib/main.dart | 12 ++++++ demo/lib/selectable_text_demo.dart | 62 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 demo/lib/selectable_text_demo.dart diff --git a/demo/lib/main.dart b/demo/lib/main.dart index a6c2a18..d3b9db0 100644 --- a/demo/lib/main.dart +++ b/demo/lib/main.dart @@ -7,6 +7,7 @@ import 'max_lines_demo.dart'; import 'min_font_size_demo.dart'; import 'overflow_replacement_demo.dart'; import 'preset_font_sizes_demo.dart'; +import 'selectable_text_demo.dart'; import 'step_granularity.dart'; import 'sync_demo.dart'; @@ -43,6 +44,7 @@ List colors = [ Colors.lightBlue, Colors.green, Colors.blueGrey, + Colors.teal, ]; List demoNames = [ @@ -52,6 +54,7 @@ List demoNames = [ 'StepGranularity', 'PresetFontSizes', 'OverflowReplacement', + 'Text Selection', ]; class _DemoAppState extends State { @@ -138,6 +141,11 @@ class _DemoAppState extends State { title: Text('replacement'), activeColor: colors[5], ), + BottomNavyBarItem( + icon: Icon(MdiIcons.selection), + title: Text('selection'), + activeColor: colors[6], + ), ], ), ); @@ -155,6 +163,10 @@ class _DemoAppState extends State { return StepGranularityDemo(_richText); case 4: return PresetFontSizesDemo(_richText); + case 5: + return OverflowReplacementDemo(_richText); + case 6: + return SelectableTextDemo(_richText); default: return OverflowReplacementDemo(_richText); } diff --git a/demo/lib/selectable_text_demo.dart b/demo/lib/selectable_text_demo.dart new file mode 100644 index 0000000..1e36ce9 --- /dev/null +++ b/demo/lib/selectable_text_demo.dart @@ -0,0 +1,62 @@ +library selectable_text_demo.dart; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; + +import 'animated_input.dart'; +import 'text_card.dart'; +import 'utils.dart'; + +/// A stateful widget to render SelectableTextDemo. +class SelectableTextDemo extends StatelessWidget { + const SelectableTextDemo(this.richText, {Key? key}) : super(key: key); + + final bool richText; + + @override + Widget build(BuildContext context) { + return AnimatedInput( + text: '"AutoSizeText.selectable" & "AutoSizeText.richSelectable"' + ' widgets displays a string of text with a single style' + ' or paragraphs with differently styled TextSpans.' + ' It is similar to "SelectableText" & "SelectableText.rich", but uses AutoSizeText to render.', + builder: (input) { + return Row( + children: [ + Expanded( + child: TextCard( + title: 'AutoSizeText', + child: !richText + ? AutoSizeText( + input, + style: TextStyle(fontSize: 30), + ) + : AutoSizeText.rich( + spanFromString(input), + style: TextStyle(fontSize: 30), + ), + ), + ), + SizedBox(width: 10), + Expanded( + child: TextCard( + title: 'AutoSizeText.selectable', + child: !richText + ? AutoSizeText.selectable( + input, + style: TextStyle(fontSize: 30), + maxLines: 2, + ) + : AutoSizeText.richSelectable( + spanFromString(input), + style: TextStyle(fontSize: 30), + maxLines: 2, + ), + ), + ), + ], + ); + }, + ); + } +} From af239498fe39933af913956455656f46940bfbfe Mon Sep 17 00:00:00 2001 From: definitelyme Date: Wed, 13 Jul 2022 13:40:04 +0100 Subject: [PATCH 6/7] Add missing selectionHeightStyle & selectionWidthStyle --- lib/auto_size_text.dart | 1 + lib/src/auto_size_text.dart | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/auto_size_text.dart b/lib/auto_size_text.dart index 27decbf..cd64ef1 100644 --- a/lib/auto_size_text.dart +++ b/lib/auto_size_text.dart @@ -3,6 +3,7 @@ library auto_size_text; import 'dart:async'; +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show SelectableText; diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index 8c8ccfd..f4aa2ab 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -48,6 +48,8 @@ class AutoSizeText extends StatefulWidget { scrollPhysics = null, onSelectionChanged = null, minLines = null, + selectionHeightStyle = ui.BoxHeightStyle.tight, + selectionWidthStyle = ui.BoxWidthStyle.tight, _isSelectableText = false, super(key: key); @@ -89,6 +91,8 @@ class AutoSizeText extends StatefulWidget { scrollPhysics = null, onSelectionChanged = null, minLines = null, + selectionHeightStyle = ui.BoxHeightStyle.tight, + selectionWidthStyle = ui.BoxWidthStyle.tight, _isSelectableText = false, super(key: key); @@ -120,6 +124,8 @@ class AutoSizeText extends StatefulWidget { this.enableInteractiveSelection = true, this.selectionControls, this.dragStartBehavior = DragStartBehavior.start, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.toolbarOptions, this.onTap, this.scrollPhysics, @@ -165,6 +171,8 @@ class AutoSizeText extends StatefulWidget { this.enableInteractiveSelection = true, this.selectionControls, this.dragStartBehavior = DragStartBehavior.start, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.toolbarOptions, this.onTap, this.scrollPhysics, @@ -431,6 +439,16 @@ class AutoSizeText extends StatefulWidget { /// If not set, select all and copy will be enabled by default. final ToolbarOptions? toolbarOptions; + /// Controls how tall the selection highlight boxes are computed to be. + /// + /// See [ui.BoxHeightStyle] for details on available styles. + final ui.BoxHeightStyle selectionHeightStyle; + + /// Controls how wide the selection highlight boxes are computed to be. + /// + /// See [ui.BoxWidthStyle] for details on available styles. + final ui.BoxWidthStyle selectionWidthStyle; + @override _AutoSizeTextState createState() => _AutoSizeTextState(); } @@ -645,6 +663,8 @@ class _AutoSizeTextState extends State { showCursor: widget.showCursor, toolbarOptions: widget.toolbarOptions, semanticsLabel: widget.semanticsLabel, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, ); } else { return SelectableText.rich( @@ -672,6 +692,8 @@ class _AutoSizeTextState extends State { showCursor: widget.showCursor, toolbarOptions: widget.toolbarOptions, semanticsLabel: widget.semanticsLabel, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, ); } } From 25913950ed50db434e5a56cf65612ebb2e4d2162 Mon Sep 17 00:00:00 2001 From: definitelyme Date: Wed, 13 Jul 2022 13:40:56 +0100 Subject: [PATCH 7/7] Add basic tests --- test/selectable_test.dart | 77 +++++++++++++++++++++++++++++++++++++++ test/utils.dart | 33 ++++++++++++----- 2 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 test/selectable_test.dart diff --git a/test/selectable_test.dart b/test/selectable_test.dart new file mode 100644 index 0000000..1c0552f --- /dev/null +++ b/test/selectable_test.dart @@ -0,0 +1,77 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'utils.dart'; + +Widget testWidget(AutoSizeText text) { + return MaterialApp( + useInheritedMediaQuery: true, + home: text, + ); +} + +void main() { + testWidgets('Only Text', (tester) async { + await pump( + tester: tester, + widget: testWidget(AutoSizeText.selectable('Some Text')), + ); + }); + + testWidgets('Only text (rich)', (tester) async { + await pump( + tester: tester, + widget: testWidget(AutoSizeText.richSelectable(TextSpan(text: 'Some Text'))), + ); + }); + + testWidgets('Uses style fontSize', (tester) async { + await pumpAndExpectFontSize( + tester: tester, + expectedFontSize: 34, + selectable: true, + widget: testWidget(AutoSizeText.selectable( + 'Some Text', + style: TextStyle(fontSize: 34), + )), + ); + }); + + testWidgets('Uses style fontSize (rich)', (tester) async { + await pumpAndExpectFontSize( + tester: tester, + expectedFontSize: 35, + selectable: true, + widget: testWidget(AutoSizeText.richSelectable( + TextSpan(text: 'Some Text'), + style: TextStyle(fontSize: 35), + )), + ); + }); + + testWidgets('Applies scale even if initial fontSize fits (#25)', (tester) async { + await pumpAndExpectFontSize( + tester: tester, + expectedFontSize: 60, + selectable: true, + widget: testWidget(AutoSizeText.selectable( + 'Some Text', + style: TextStyle(fontSize: 15), + textScaleFactor: 4, + )), + ); + }); + + testWidgets('Uses textKey', (tester) async { + final textKey = GlobalKey(); + final text = await pumpAndGetSelectableText( + tester: tester, + widget: testWidget(AutoSizeText.selectable( + 'A text with key', + textKey: textKey, + )), + ); + expect(text.key, textKey); + }); +} diff --git a/test/utils.dart b/test/utils.dart index 9c33d88..d8d2fe4 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -5,8 +5,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -double effectiveFontSize(Text text) => - (text.textScaleFactor ?? 1) * text.style!.fontSize!; +double effectiveFontSize(Text text) => (text.textScaleFactor ?? 1) * text.style!.fontSize!; + +double selectableEffectiveFontSize(SelectableText text) => (text.textScaleFactor ?? 1) * text.style!.fontSize!; bool doesTextFit( Text text, [ @@ -33,9 +34,7 @@ bool doesTextFit( textPainter.layout(maxWidth: maxWidth); - return !(textPainter.didExceedMaxLines || - textPainter.height > maxHeight || - textPainter.width > maxWidth); + return !(textPainter.didExceedMaxLines || textPainter.height > maxHeight || textPainter.width > maxWidth); } bool prepared = false; @@ -47,9 +46,7 @@ Future prepareTests(WidgetTester tester) async { tester.binding.addTime(Duration(seconds: 10)); prepared = true; - final fontData = File('test/assets/Roboto-Regular.ttf') - .readAsBytes() - .then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer)); + final fontData = File('test/assets/Roboto-Regular.ttf').readAsBytes().then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer)); final fontLoader = FontLoader('Roboto')..addFont(fontData); await fontLoader.load(); @@ -77,17 +74,33 @@ Future pumpAndGetText({ return tester.widget(find.byType(Text)); } +Future pumpAndGetSelectableText({ + required WidgetTester tester, + required Widget widget, +}) async { + await pump(tester: tester, widget: widget); + return tester.widget(find.byType(SelectableText)); +} + Future pumpAndExpectFontSize({ required WidgetTester tester, required double expectedFontSize, required Widget widget, + bool selectable = false, }) async { + if (selectable) { + final text = await pumpAndGetSelectableText(tester: tester, widget: widget); + expect(selectableEffectiveFontSize(text), expectedFontSize); + return; + } + final text = await pumpAndGetText(tester: tester, widget: widget); expect(effectiveFontSize(text), expectedFontSize); } -RichText getRichText(WidgetTester tester) => - tester.widget(find.byType(RichText)); +RichText getRichText(WidgetTester tester) => tester.widget(find.byType(RichText)); + +EditableText getEditableText(WidgetTester tester) => tester.widget(find.byType(EditableText)); class OverflowNotifier extends StatelessWidget { final VoidCallback overflowCallback;