Make the output of TextPainter match rendered text 1:1
- Dominant language
- Dart
- Stars
- 179k
- Forks
- 31.1k
- PR merge metrics
- PR metrics pending
Description
### Use case
[Related to issue 141172](https://github.com/flutter/flutter/issues/141172).
I've created a widget for accessibility purposes that adds a`Tooltip` to a `Text` widget, but only if that `Text` overflows its container. This is a compromise mainly for users that have high text scaling - the layout truncates their text, but on long press it's still visible.
In order to show the `Tooltip` only when the `Text` is overflowing, I need to lay out the text in advance, and decide if I need to wrap it in a `Tooltip` or not. Here's the resulting widget, but it doesn't work as expected:
```
// Flutter imports:
import 'package:flutter/material.dart';
/// Wraps a [textSpan] in a [Tooltip], but only if the text would overflow.
///
/// Creates a [Text.rich] widget with [textSpan], and passes in the other
/// [Text] arguments as-is. If the [Text.rich] widget cannot be contained in
/// the constraints without overflowing, then it's wrapped in a [Tooltip] with
/// a corresponding [message] (or [textSpan.toPlainText] if [message] is null).
///
/// The use case here is mainly for text scaling - we want to ensure that on a
/// small device with high text scaling, all text that is intended to be
/// visible is readable somehow. Not for use or text that is designed to
/// overflow (like a preview), or text that is already wrapped in a [Tooltip]
/// (like a button).
class OverflowTextTooltip extends StatelessWidget {
/// The styled Text as it is expected to be drawn.
final TextSpan textSpan;
final int? maxLines;
final TextAlign textAlign;
final TextOverflow? overflow;
const OverflowTextTooltip({
super.key,
required this.textSpan,
this.maxLines,
this.textAlign = TextAlign.start,
this.overflow,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final painter = TextPainter(
text: textSpan,
textAlign: textAlign,
maxLines: maxLines,
textDirection: Directionality.of(context),
textScaler: MediaQuery.of(context).textScaler,
)..layout(maxWidth: constraints.maxWidth);
// TODO didExceedMaxLines is unreliable
if (painter.didExceedMaxLines) {
return Tooltip(
message: textSpan.toPlainText(),
child: Text.rich(
textSpan,
maxLines: maxLines,
textAlign: textAlign,
overflow: overflow,
),
);
} else {
return Text.rich(
textSpan,
maxLines: maxLines,
textAlign: textAlign,
overflow: overflow,
);
}
},
);
}
}
```
When I check `painter.didExceedMaxLines` it is correct *most* of the time, but not all the time. Testing with print statements, no max lines, and checking the widths, I can find examples where the TextPainter lays out the text in 310 pixels, but when Flutter places it on screen, it's only 295 pixels, with no overflow. This seems to get worse with higher font sizes / font size scaling.
[Per the comments in the related issue](https://github.com/flutter/flutter/issues/141172#issuecomment-2023134702), it seems like the workaround is to be extremely verbose in declaring the TextSpan so that `DefaultTextStyle` is completely ignored by both the `TextPainter` and the resulting `Text` widget.
### Proposal
What I'd like Flutter to do is one of the following:
1. Make TextPainter layout and the resulting Text layout 1:1 without having to manage overrides of `DefaultTextStyle`.
2. Add an argument to `Text` that wraps the `Text` in a `Tooltip` (or any widget without it's own dimensions) when it overflows, making my custom widget moot.
I'd actually prefer #2 (or something like it) - my custom widget uses a `LayoutBuilder` which can't be used inside of a widget like `SliverFillRemaining`, so it has limitations.
Contributor guide
Assessment
This issue has not been assessed yet.