Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WidgetSpan causes an error #61

Open
alessandrogarcez opened this issue Jun 18, 2020 · 8 comments
Open

WidgetSpan causes an error #61

alessandrogarcez opened this issue Jun 18, 2020 · 8 comments
Labels
bug Something isn't working

Comments

@alessandrogarcez
Copy link

Steps to Reproduce
It seems that the package doesn't support WidgetSpan within TestSpan.

Code sample

...
AutoSizeText.rich(
              TextSpan(children: [
                TextSpan(text: "Flutter Developer"),
                WidgetSpan(
                  child: Icon(
                    Icons.add,
                    size: 14,
                  ),
                )
              ]),
...

Screenshots
Simulator Screen Shot - iPhone SE (1st generation) - 2020-06-18 at 11 49 54

Version

  • auto_size_text version: ^2.1.0
@pawar-ashish
Copy link

Any updates on this?

@simc
Copy link
Owner

simc commented Sep 27, 2020

Any updates on this?

No, unfortunately I failed with my attempt to support WidgetSpans. If anyone has an idea how to do it, I would greatly appreciate a hint 👍

@kkoken
Copy link

kkoken commented Jan 24, 2022

I have the same issue

@ashishbeck
Copy link

Bump: Someone rescue this please

@Tregan
Copy link

Tregan commented Mar 28, 2022

So I've been playing around with this, since I had to support superscript/subscript... I've come up with a solution that works for our use case, but I hope this can help someone to make something that would work for anyone!

So in our case I used a tag parser heavily inspired by https://github.com/gmetekorkmaz/TextManip , with our superscript tag being defined as follows:

/// Displays text surrounded by `<sup></sup>` with a smaller fontSize (75% of original) at an [Offset] (40% above middle).
  static final OffsetTag superscript = OffsetTag(
      regExp: RegExp(r"<sup>(.*?)</sup>"),
      callback: (RegExpMatch regExpMatch, TextStyle parentStyle) {
        final double? parentFontSize = parentStyle.fontSize;
        if (parentFontSize == null) {
          throw ArgumentError.notNull('parentStyle.fontSize');
        }

        return ParsedOffsetText(
          text: regExpMatch.group(1)!,
          parentStyle: parentStyle,
          style: TextStyle(
            fontSize: parentFontSize * 0.75,
          ),
          offset: Offset(0, -parentFontSize * 0.4),
        );
      });

then in the parser we go through all the tags in the specified text, which also has a base style defined, and in case of OffsetTag we add a WidgetSpan as follows:

 final ParsedOffsetText offsetText = textManipTag.callback(regExpMatch, currentStyle) as ParsedOffsetText;
          children.add(WidgetSpan(
            child: Transform.translate(
              offset: offsetText.offset,
              child: Text(
                offsetText.text,
                style: currentStyle.merge(offsetText.style),
              ),
            ),
            alignment: PlaceholderAlignment.middle,
          ));

Then to get rid of the dimensions != null error it's required that every WidgetSpan has PlaceholderDimensions defined in the TextPainter before textPainter.layout(maxWidth: constraints.maxWidth); is called. So to make this work for us I changed _checkTextFits in auto_size_text.dart to this

bool _checkTextFits(TextSpan text, double scale, int? maxLines, BoxConstraints constraints) {
    if (!widget.wrapWords) {
      final words = text.toPlainText().split(RegExp('\\s+'));

      final wordWrapTextPainter = TextPainter(
        text: TextSpan(
          style: text.style,
          text: words.join('\n'),
        ),
        textAlign: widget.textAlign ?? TextAlign.left,
        textDirection: widget.textDirection ?? TextDirection.ltr,
        textScaleFactor: scale,
        maxLines: words.length,
        locale: widget.locale,
        strutStyle: widget.strutStyle,
      );

      wordWrapTextPainter.layout(maxWidth: constraints.maxWidth);

      if (wordWrapTextPainter.didExceedMaxLines || wordWrapTextPainter.width > constraints.maxWidth) {
        return false;
      }
    }

    final textPainter = TextPainter(
      text: text,
      textAlign: widget.textAlign ?? TextAlign.left,
      textDirection: widget.textDirection ?? TextDirection.ltr,
      textScaleFactor: scale,
      maxLines: maxLines,
      locale: widget.locale,
      strutStyle: widget.strutStyle,
    );

    final List<WidgetSpan> widgetSpans = _findWidgetSpans(text);
    if (widgetSpans.isNotEmpty) {
      textPainter.setPlaceholderDimensions(
        widgetSpans.map((widgetSpan) {
          final Transform transform = widgetSpan.child as Transform;
          final Text text = transform.child as Text;

          final TextPainter textPainter = TextPainter(
            text: TextSpan(text: text.data, style: text.style),
            textAlign: widget.textAlign ?? TextAlign.start,
            textDirection: widget.textDirection ?? TextDirection.ltr,
            textScaleFactor: scale,
            maxLines: maxLines,
            locale: widget.locale,
            strutStyle: widget.strutStyle,
          )..layout();

          return PlaceholderDimensions(
            size: textPainter.size,
            alignment: widgetSpan.alignment,
          );
        }).toList(),
      );
    }

    textPainter.layout(maxWidth: constraints.maxWidth);

    return !(textPainter.didExceedMaxLines || textPainter.height > constraints.maxHeight || textPainter.width > constraints.maxWidth);
  }

  List<WidgetSpan> _findWidgetSpans(TextSpan text) {
    final List<TextSpan> textSpans = text.children?.whereType<TextSpan>().toList() ?? [];
    final List<WidgetSpan> widgetSpans = text.children?.whereType<WidgetSpan>().toList() ?? [];
    for (TextSpan textSpan in textSpans) {
      widgetSpans.addAll(_findWidgetSpans(textSpan));
    }
    return widgetSpans;
  }

So as you can see, this solution will only work in this case since we're not working with additional boxes surrounding text or whatever. Since we only use an extra Transform it's pretty easy to obtain the PlaceholderDimensions for each Widgetspan.

I can imagine that once you start adding paddings or things like that, it's gonna be a whole lot more complicated since you'd have to go through every child and calculate the dimensions of the end result... Not sure what would be the best way to go about that.

@dagovalsusa
Copy link

Hello,
I've tried to find a solution starting from @Tregan post.
My solution works fine with WidgetSpan that contain a Row with two children that contain a Text, my hope is that someone generalize that solution.

I think that this is a possible way.

According with previous post i add this method to auto_size_text.dart

 List<WidgetSpan> _findWidgetSpans(TextSpan text) {
    final List<TextSpan> textSpans = text.children?.whereType<TextSpan>().toList() ?? [];
    final List<WidgetSpan> widgetSpans = text.children?.whereType<WidgetSpan>().toList() ?? [];
    for (TextSpan textSpan in textSpans) {
      widgetSpans.addAll(_findWidgetSpans(textSpan));
    }
    return widgetSpans;
  }

but i've added also _findText to find the Text widget

Text? _findText(widget) {
    print("${widget} ${widget.runtimeType}");
    if (widget is Text) return (widget as Text);
    if (widget.child == null) return null;
    print("${widget.child} ${widget.child.runtimeType}");
    if (widget.child is Text) return (widget.child as Text);
    return _findText(widget.child);
  }

and edit _checkTextFits with

final List<WidgetSpan> widgetSpans = _findWidgetSpans(text);
    if (widgetSpans.isNotEmpty) {
      textPainter.setPlaceholderDimensions(
        widgetSpans.map((widgetSpan) {
          final Row row = widgetSpan.child as Row;
          final List<Widget> widgets = row.children as List<Widget>;
          final Text text = _findText(widgets[1])!;

          final TextPainter textPainter = TextPainter(
            text: TextSpan(text: text.data, style: text.style),
            textAlign: widget.textAlign ?? TextAlign.start,
            textDirection: widget.textDirection ?? TextDirection.ltr,
            textScaleFactor: scale,
            maxLines: maxLines,
            locale: widget.locale,
            strutStyle: widget.strutStyle,
          )..layout();

          return PlaceholderDimensions(
            size: textPainter.size,
            alignment: widgetSpan.alignment,
          );
        }).toList(),
      );
    }

@ChrisElliotUK
Copy link

ChrisElliotUK commented Dec 7, 2023

I have found a bit of a hack way to remove the error. Above is better, but we simply need to add ..setPlaceholderDimensions() to our TextPainter.

If we provide a new param List<PlaceholderDimensions> placeholderDimensions we can set this on a case by case basis. I will open a PR and you can choose to accept it or not :)

Details

part of auto_size_text;

/// Flutter widget that automatically resizes text to fit perfectly within its
/// bounds.
///
/// All size constraints as well as maxLines are taken into account. If the text
/// overflows anyway, you should check if the parent widget actually constraints
/// the size of this widget.
class AutoSizeText extends StatefulWidget {
  /// Creates a [AutoSizeText] widget.
  ///
  /// If the [style] argument is null, the text will use the style from the
  /// closest enclosing [DefaultTextStyle].
  const AutoSizeText(
    String 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.locale,
    this.softWrap,
    this.wrapWords = true,
    this.overflow,
    this.overflowReplacement,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.placeholderDimensions = const [],
  })  : textSpan = null,
        super(key: key);

  /// Creates a [AutoSizeText] widget with a [TextSpan].
  const AutoSizeText.rich(
    TextSpan 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.locale,
    this.softWrap,
    this.wrapWords = true,
    this.overflow,
    this.overflowReplacement,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.placeholderDimensions = const [],
  })  : data = null,
        super(key: key);

  /// Sets the key for the resulting [Text] widget.
  ///
  /// This allows you to find the actual `Text` widget built by `AutoSizeText`.
  final Key? textKey;

  /// The text to display.
  ///
  /// This will be null if a [textSpan] is provided instead.
  final String? data;

  /// The text to display as a [TextSpan].
  ///
  /// This will be null if [data] is provided instead.
  final TextSpan? textSpan;

  /// If non-null, the style to use for this text.
  ///
  /// If the style's "inherit" property is true, the style will be merged with
  /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
  /// replace the closest enclosing [DefaultTextStyle].
  final TextStyle? style;

  // The default font size if none is specified.
  static const double _defaultFontSize = 14;

  /// The strut style to use. Strut style defines the strut, which sets minimum
  /// vertical layout metrics.
  ///
  /// Omitting or providing null will disable strut.
  ///
  /// Omitting or providing null for any properties of [StrutStyle] will result
  /// in default values being used. It is highly recommended to at least specify
  /// a font size.
  ///
  /// See [StrutStyle] for details.
  final StrutStyle? strutStyle;

  /// The minimum text size constraint to be used when auto-sizing text.
  ///
  /// Is being ignored if [presetFontSizes] is set.
  final double minFontSize;

  /// The maximum text size constraint to be used when auto-sizing text.
  ///
  /// Is being ignored if [presetFontSizes] is set.
  final double maxFontSize;

  /// The step size in which the font size is being adapted to constraints.
  ///
  /// The Text scales uniformly in a range between [minFontSize] and
  /// [maxFontSize].
  /// Each increment occurs as per the step size set in stepGranularity.
  ///
  /// Most of the time you don't want a stepGranularity below 1.0.
  ///
  /// Is being ignored if [presetFontSizes] is set.
  final double stepGranularity;

  /// Predefines all the possible font sizes.
  ///
  /// **Important:** PresetFontSizes have to be in descending order.
  final List<double>? presetFontSizes;

  /// Synchronizes the size of multiple [AutoSizeText]s.
  ///
  /// If you want multiple [AutoSizeText]s to have the same text size, give all
  /// of them the same [AutoSizeGroup] instance. All of them will have the
  /// size of the smallest [AutoSizeText]
  final AutoSizeGroup? group;

  /// How the text should be aligned horizontally.
  final TextAlign? textAlign;

  /// The directionality of the text.
  ///
  /// This decides how [textAlign] values like [TextAlign.start] and
  /// [TextAlign.end] are interpreted.
  ///
  /// This is also used to disambiguate how to render bidirectional text. For
  /// example, if the [data] is an English phrase followed by a Hebrew phrase,
  /// in a [TextDirection.ltr] context the English phrase will be on the left
  /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
  /// context, the English phrase will be on the right and the Hebrew phrase on
  /// its left.
  ///
  /// Defaults to the ambient [Directionality], if any.
  final TextDirection? textDirection;

  /// Used to select a font when the same Unicode character can
  /// be rendered differently, depending on the locale.
  ///
  /// It's rarely necessary to set this property. By default its value
  /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
  final Locale? locale;

  /// Whether the text should break at soft line breaks.
  ///
  /// If false, the glyphs in the text will be positioned as if there was
  /// unlimited horizontal space.
  final bool? softWrap;

  /// Whether words which don't fit in one line should be wrapped.
  ///
  /// If false, the fontSize is lowered as far as possible until all words fit
  /// into a single line.
  final bool wrapWords;

  /// How visual overflow should be handled.
  ///
  /// Defaults to retrieving the value from the nearest [DefaultTextStyle] ancestor.
  final TextOverflow? overflow;

  /// If the text is overflowing and does not fit its bounds, this widget is
  /// displayed instead.
  final Widget? overflowReplacement;

  /// The number of font pixels for each logical pixel.
  ///
  /// For example, if the text scale factor is 1.5, text will be 50% larger than
  /// the specified font size.
  ///
  /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes].
  ///
  /// The value given to the constructor as textScaleFactor. If null, will
  /// use the [MediaQueryData.textScaleFactor] obtained from the ambient
  /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
  final double? textScaleFactor;

  /// An optional maximum number of lines for the text to span, wrapping if necessary.
  /// If the text exceeds the given number of lines, it will be resized according
  /// to the specified bounds and if necessary truncated according to [overflow].
  ///
  /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
  /// edge of the box.
  ///
  /// If this is null, but there is an ambient [DefaultTextStyle] that specifies
  /// an explicit number for its [DefaultTextStyle.maxLines], then the
  /// [DefaultTextStyle] value will take precedence. You can use a [RichText]
  /// widget directly to entirely override the [DefaultTextStyle].
  final int? maxLines;

  /// An alternative semantics label for this text.
  ///
  /// If present, the semantics of this widget will contain this value instead
  /// of the actual text. This will overwrite any of the semantics labels applied
  /// directly to the [TextSpan]s.
  ///
  /// This is useful for replacing abbreviations or shorthands with the full
  /// text value:
  ///
  /// ```dart
  /// AutoSizeText(r'$$', semanticsLabel: 'Double dollars')
  /// ```
  final String? semanticsLabel;

  final List<PlaceholderDimensions> placeholderDimensions;

  @override
  _AutoSizeTextState createState() => _AutoSizeTextState();
}

class _AutoSizeTextState extends State<AutoSizeText> {
  @override
  void initState() {
    super.initState();

    widget.group?._register(this);
  }

  @override
  void didUpdateWidget(AutoSizeText oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.group != widget.group) {
      oldWidget.group?._remove(this);
      widget.group?._register(this);
    }
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, size) {
      final defaultTextStyle = DefaultTextStyle.of(context);

      var style = widget.style;
      if (widget.style == null || widget.style!.inherit) {
        style = defaultTextStyle.style.merge(widget.style);
      }
      if (style!.fontSize == null) {
        style = style.copyWith(fontSize: AutoSizeText._defaultFontSize);
      }

      final maxLines = widget.maxLines ?? defaultTextStyle.maxLines;

      _validateProperties(style, maxLines);

      final result = _calculateFontSize(size, style, maxLines);
      final fontSize = result[0] as double;
      final textFits = result[1] as bool;

      Widget text;

      if (widget.group != null) {
        widget.group!._updateFontSize(this, fontSize);
        text = _buildText(widget.group!._fontSize, style, maxLines);
      } else {
        text = _buildText(fontSize, style, maxLines);
      }

      if (widget.overflowReplacement != null && !textFits) {
        return widget.overflowReplacement!;
      } else {
        return text;
      }
    });
  }

  void _validateProperties(TextStyle style, int? maxLines) {
    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.');

    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.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.');
      if (widget.maxFontSize != double.infinity) {
        assert(widget.maxFontSize / widget.stepGranularity % 1 == 0,
            'MaxFontSize must be a multiple of stepGranularity.');
      }
    } else {
      assert(widget.presetFontSizes!.isNotEmpty,
          'PresetFontSizes must not be empty.');
    }
  }

  List _calculateFontSize(
      BoxConstraints size, TextStyle? style, int? maxLines) {
    final span = TextSpan(
      style: widget.textSpan?.style ?? style,
      text: widget.textSpan?.text ?? widget.data,
      children: widget.textSpan?.children,
      recognizer: widget.textSpan?.recognizer,
    );

    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 defaultScale = defaultFontSize * userScale / style.fontSize!;
      if (_checkTextFits(span, defaultScale, maxLines, size)) {
        return <Object>[defaultFontSize * userScale, true];
      }

      left = (widget.minFontSize / widget.stepGranularity).floor();
      right = (defaultFontSize / widget.stepGranularity).ceil();
    } else {
      left = 0;
      right = presetFontSizes.length - 1;
    }

    var lastValueFits = false;
    while (left <= right) {
      final mid = (left + (right - left) / 2).floor();
      double scale;
      if (presetFontSizes == null) {
        scale = mid * userScale * widget.stepGranularity / style!.fontSize!;
      } else {
        scale = presetFontSizes[mid] * userScale / style!.fontSize!;
      }
      if (_checkTextFits(span, scale, maxLines, size)) {
        left = mid + 1;
        lastValueFits = true;
      } else {
        right = mid - 1;
      }
    }

    if (!lastValueFits) {
      right += 1;
    }

    double fontSize;
    if (presetFontSizes == null) {
      fontSize = right * userScale * widget.stepGranularity;
    } else {
      fontSize = presetFontSizes[right] * userScale;
    }

    return <Object>[fontSize, lastValueFits];
  }

  bool _checkTextFits(
      TextSpan text, double scale, int? maxLines, BoxConstraints constraints) {
    if (!widget.wrapWords) {
      final words = text.toPlainText().split(RegExp('\\s+'));

      final wordWrapTextPainter = TextPainter(
        text: TextSpan(
          style: text.style,
          text: words.join('\n'),
        ),
        textAlign: widget.textAlign ?? TextAlign.left,
        textDirection: widget.textDirection ?? TextDirection.ltr,
        textScaleFactor: scale,
        maxLines: words.length,
        locale: widget.locale,
        strutStyle: widget.strutStyle,
      );

      wordWrapTextPainter.layout(maxWidth: constraints.maxWidth);

      if (wordWrapTextPainter.didExceedMaxLines ||
          wordWrapTextPainter.width > constraints.maxWidth) {
        return false;
      }
    }

    final textPainter = TextPainter(
      text: text,
      textAlign: widget.textAlign ?? TextAlign.left,
      textDirection: widget.textDirection ?? TextDirection.ltr,
      textScaleFactor: scale,
      maxLines: maxLines,
      locale: widget.locale,
      strutStyle: widget.strutStyle,
    )..setPlaceholderDimensions(widget.placeholderDimensions);

    textPainter.layout(maxWidth: constraints.maxWidth);

    return !(textPainter.didExceedMaxLines ||
        textPainter.height > constraints.maxHeight ||
        textPainter.width > constraints.maxWidth);
  }

  Widget _buildText(double fontSize, TextStyle style, int? maxLines) {
    if (widget.data != null) {
      return Text(
        widget.data!,
        key: widget.textKey,
        style: style.copyWith(fontSize: fontSize),
        strutStyle: widget.strutStyle,
        textAlign: widget.textAlign,
        textDirection: widget.textDirection,
        locale: widget.locale,
        softWrap: widget.softWrap,
        overflow: widget.overflow,
        textScaleFactor: 1,
        maxLines: maxLines,
        semanticsLabel: widget.semanticsLabel,
      );
    } else {
      return Text.rich(
        widget.textSpan!,
        key: widget.textKey,
        style: style,
        strutStyle: widget.strutStyle,
        textAlign: widget.textAlign,
        textDirection: widget.textDirection,
        locale: widget.locale,
        softWrap: widget.softWrap,
        overflow: widget.overflow,
        textScaleFactor: fontSize / style.fontSize!,
        maxLines: maxLines,
        semanticsLabel: widget.semanticsLabel,
      );
    }
  }

  void _notifySync() {
    setState(() {});
  }

  @override
  void dispose() {
    if (widget.group != null) {
      widget.group!._remove(this);
    }
    super.dispose();
  }
}

@ChrisElliotUK
Copy link

#139

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

8 participants