diff --git a/lib/src/auto_size_builder/auto_size.dart b/lib/src/auto_size_builder/auto_size.dart index 6273e5b..1b947da 100644 --- a/lib/src/auto_size_builder/auto_size.dart +++ b/lib/src/auto_size_builder/auto_size.dart @@ -21,6 +21,7 @@ class _AutoSize extends RenderObjectWidget { required this.stepGranularity, required this.presetFontSizes, this.onMaxPossibleFontSizeChanged, + this.placeholderDimensionsBuilder, }) { _validateProperties(); } @@ -45,6 +46,7 @@ class _AutoSize extends RenderObjectWidget { final List? presetFontSizes; final double? groupMaxFontSize; final void Function(double)? onMaxPossibleFontSizeChanged; + final AutoSizePlaceholderDimensionsBuilder? placeholderDimensionsBuilder; TextFitter _buildFitter() { return TextFitter( @@ -63,6 +65,7 @@ class _AutoSize extends RenderObjectWidget { maxFontSize: maxFontSize, stepGranularity: stepGranularity, presetFontSizes: presetFontSizes, + placeholderDimensionsBuilder: placeholderDimensionsBuilder, ); } diff --git a/lib/src/auto_size_builder/auto_size_builder.dart b/lib/src/auto_size_builder/auto_size_builder.dart index eb605ee..e27c79f 100644 --- a/lib/src/auto_size_builder/auto_size_builder.dart +++ b/lib/src/auto_size_builder/auto_size_builder.dart @@ -25,6 +25,7 @@ class _AutoSizeBuilder extends StatefulWidget { this.stepGranularity, this.presetFontSizes, this.group, + this.placeholderDimensionsBuilder, }); final _AutoSizeTextBuilder builder; @@ -81,6 +82,8 @@ class _AutoSizeBuilder extends StatefulWidget { /// {@macro auto_size_text.group} final AutoSizeGroup? group; + final AutoSizePlaceholderDimensionsBuilder? placeholderDimensionsBuilder; + @override State<_AutoSizeBuilder> createState() => _AutoSizeBuilderState(); } @@ -148,6 +151,7 @@ class _AutoSizeBuilderState extends State<_AutoSizeBuilder> { widget.group?._updateFontSize(this, maxPossibleFontSize); } : null, + placeholderDimensionsBuilder: widget.placeholderDimensionsBuilder, ); } diff --git a/lib/src/auto_size_text.dart b/lib/src/auto_size_text.dart index de3443b..c3f8922 100644 --- a/lib/src/auto_size_text.dart +++ b/lib/src/auto_size_text.dart @@ -1,5 +1,19 @@ part of '../flutter_auto_size_text.dart'; +/// Builds placeholder dimensions for [PlaceholderSpan]s while calculating the +/// font size. +/// +/// If no builder is provided, or if the builder returns null for a placeholder, +/// that placeholder defaults to a square using the current candidate font size. +/// +/// This may be called multiple times while [AutoSizeText] searches for the +/// largest fitting font size. +typedef AutoSizePlaceholderDimensionsBuilder = PlaceholderDimensions? Function( + PlaceholderSpan span, + int index, + double fontSize, +); + /// Flutter widget that automatically resizes text to fit perfectly within its /// bounds. /// @@ -160,6 +174,19 @@ class AutoSizeText extends StatelessWidget { /// {@endtemplate} final Function(bool overflow)? overflowCallback; + /// Builds dimensions for placeholders while calculating the font size. + /// + /// This is useful for [TextSpan] trees with [WidgetSpan]s whose rendered + /// sizes are larger than the surrounding text. If omitted, placeholders are + /// measured as a square using the current candidate font size. + /// + /// Return null to use the default dimensions for a placeholder. + /// + /// The builder may be called multiple times while searching for the largest + /// fitting font size, so it should return stable dimensions for the same + /// inputs. + final AutoSizePlaceholderDimensionsBuilder? placeholderDimensionsBuilder; + /// Creates a [AutoSizeText] /// /// If the [style] argument is null, the text will use the style from the @@ -188,6 +215,7 @@ class AutoSizeText extends StatelessWidget { this.wrapWords, this.overflowReplacement, this.overflowCallback, + this.placeholderDimensionsBuilder, }) : textSpan = null; /// Creates a [AutoSizeText] widget with a [TextSpan]. @@ -215,6 +243,7 @@ class AutoSizeText extends StatelessWidget { this.wrapWords, this.overflowReplacement, this.overflowCallback, + this.placeholderDimensionsBuilder, }) : data = null; @override @@ -262,6 +291,7 @@ class AutoSizeText extends StatelessWidget { maxLines: maxLines, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, + placeholderDimensionsBuilder: placeholderDimensionsBuilder, ); } } diff --git a/lib/src/text_fitter.dart b/lib/src/text_fitter.dart index 878e0ac..6acc6f8 100644 --- a/lib/src/text_fitter.dart +++ b/lib/src/text_fitter.dart @@ -19,6 +19,7 @@ class TextFitter { required this.maxFontSize, this.stepGranularity = 1.0, this.presetFontSizes, + this.placeholderDimensionsBuilder, }); final TextSpan text; @@ -36,6 +37,7 @@ class TextFitter { final double maxFontSize; final double stepGranularity; final List? presetFontSizes; + final AutoSizePlaceholderDimensionsBuilder? placeholderDimensionsBuilder; double? _longestWordWidth; @@ -131,6 +133,7 @@ class TextFitter { textHeightBehavior: textHeightBehavior, ); + textPainter.setPlaceholderDimensions(_placeholderDimensions(scale)); textPainter.layout(maxWidth: constraints.maxWidth); final overflow = textPainter.didExceedMaxLines || @@ -163,6 +166,37 @@ class TextFitter { return result; } + List? _placeholderDimensions(double scale) { + final dimensions = []; + final fontSize = (text.style?.fontSize ?? _kDefaultFontSize) * scale; + var index = 0; + + text.visitChildren((span) { + if (span is PlaceholderSpan) { + final placeholderDimensions = placeholderDimensionsBuilder?.call( + span, + index, + fontSize, + ); + + dimensions.add( + placeholderDimensions ?? + PlaceholderDimensions( + size: Size(fontSize, fontSize), + alignment: span.alignment, + baseline: span.baseline, + baselineOffset: fontSize * 0.8, + ), + ); + index += 1; + } + + return true; + }); + + return dimensions.isEmpty ? null : dimensions; + } + List _getWordSpans() { final wordRegex = RegExp('\\s+'); Iterable splitSpan(TextSpan span, TextStyle? style) sync* { @@ -203,7 +237,8 @@ class TextFitter { other.minFontSize == minFontSize && other.maxFontSize == maxFontSize && other.stepGranularity == stepGranularity && - other.presetFontSizes == presetFontSizes; + other.presetFontSizes == presetFontSizes && + other.placeholderDimensionsBuilder == placeholderDimensionsBuilder; } @override @@ -223,6 +258,7 @@ class TextFitter { maxFontSize, stepGranularity, presetFontSizes, + placeholderDimensionsBuilder, ); } } diff --git a/test/basic_test.dart b/test/basic_test.dart index 559dd9a..207281e 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -95,4 +95,119 @@ void main() { ); expect(text.key, textKey); }); + testWidgets('Uses placeholderDimensionsBuilder for WidgetSpans', + (tester) async { + final indexes = []; + final spans = []; + final fontSizes = []; + + await pump( + tester: tester, + widget: SizedBox( + width: 1000, + height: 1000, + child: AutoSizeText.rich( + const TextSpan( + children: [ + TextSpan(text: 'One '), + WidgetSpan(child: SizedBox(width: 12, height: 12)), + TextSpan(text: ' two '), + WidgetSpan(child: SizedBox(width: 16, height: 16)), + ], + ), + style: const TextStyle(fontSize: 20), + placeholderDimensionsBuilder: (span, index, fontSize) { + indexes.add(index); + spans.add(span); + fontSizes.add(fontSize); + + return PlaceholderDimensions( + size: Size(fontSize, fontSize), + alignment: span.alignment, + baseline: span.baseline, + baselineOffset: fontSize * 0.8, + ); + }, + ), + ), + ); + + expect(indexes, [0, 1]); + expect(spans, everyElement(isA())); + expect(fontSizes, [20, 20]); + }); + + testWidgets('Uses font-size placeholder dimensions by default', + (tester) async { + await pump( + tester: tester, + widget: const AutoSizeText.rich( + TextSpan( + children: [ + TextSpan(text: 'Inline '), + WidgetSpan(child: Icon(Icons.add, size: 14)), + ], + ), + style: TextStyle(fontSize: 20), + ), + ); + + expect(tester.takeException(), isNull); + }); + + testWidgets('Uses default placeholder dimensions when builder returns null', + (tester) async { + final text = await pumpAndGet( + tester: tester, + widget: SizedBox( + width: 50, + height: 100, + child: AutoSizeText.rich( + const TextSpan( + children: [ + TextSpan(text: 'A'), + WidgetSpan(child: SizedBox(width: 12, height: 12)), + ], + ), + maxLines: 1, + minFontSize: 10, + style: const TextStyle(fontSize: 40), + placeholderDimensionsBuilder: (span, index, fontSize) => null, + ), + ), + ); + + expect(effectiveFontSize(text), lessThan(40)); + }); + + testWidgets('Placeholder dimensions influence font size', (tester) async { + final text = await pumpAndGet( + tester: tester, + widget: SizedBox( + width: 130, + height: 100, + child: AutoSizeText.rich( + const TextSpan( + children: [ + TextSpan(text: 'A'), + WidgetSpan(child: SizedBox(width: 12, height: 12)), + ], + ), + maxLines: 1, + minFontSize: 10, + style: const TextStyle(fontSize: 40), + placeholderDimensionsBuilder: (span, index, fontSize) { + return PlaceholderDimensions( + size: Size(120, fontSize), + alignment: span.alignment, + baseline: span.baseline, + baselineOffset: fontSize * 0.8, + ); + }, + ), + ), + ); + + expect(effectiveFontSize(text), lessThan(40)); + }); }