add `debugPaintTextLayoutBoxes` for debugging text layout (#168876)
Text vertical alignment is a common complaint and one of the most common
causes is different type faces being used in the same paragraph (for
instance, when there's English / Arabic in the same paragraph so the
default font doesn't contain the required glyphs for some characters).
Hopefully this flag is going to make debugging alignment issues a bit
easier, and in some github issues attaching screenshots with the flag on
will be helpful for diagnosing the issue.
### What the layout boxes look like
```dart
final testText =
'Quick brown fox, نص, trailing spaces${" " * 100}'
'new line at the end.\n\n\n\n';
Column([
Text(testText, style: TextStyle(height: kTextHeightNone)),
Text(testText, style: TextStyle(height: 1.0)),
Text(
testText,
style: TextStyle(height: kTextHeightNone),
strutStyle: StrutStyle(fontSize: 30),
),
])
```
<img width="410" alt="image"
src="https://github.com/user-attachments/assets/788b8089-373f-4af0-9144-306b3e755614"
/>
## Pre-launch Checklist
- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart
index af1ff10..c0ca1b0 100644
--- a/packages/flutter/lib/src/painting/text_painter.dart
+++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -1291,6 +1291,22 @@
_layoutCache = newLayoutCache;
}
+ /// Causes the paragraph to paint the layout boxes of the text.
+ ///
+ /// {@template flutter.painting.textPainter.debugPaintTextLayoutBoxes}
+ /// Each painted box illustrates how the encompassed text contributes to the
+ /// overall text layout. For instance, for paragraphs whose [StrutStyle] is
+ /// disabled, the line height of a line is the smallest vertical extent that
+ /// covers all text boxes on that line.
+ ///
+ /// Typically, only characters with a non-zero horizontal advance produce
+ /// these boxes. No boxes will be painted for lines that only consist of a new
+ /// line character.
+ /// {@endtemplate}
+ ///
+ /// The [paint] method reads this flag only in debug mode.
+ bool debugPaintTextLayoutBoxes = false;
+
/// Paints the text onto the given canvas at the given offset.
///
/// Valid only after [layout] has been called.
@@ -1335,9 +1351,31 @@
assert(debugSize == size);
}
assert(!_rebuildParagraphForPaint);
+
+ assert(
+ !debugPaintTextLayoutBoxes || _debugPaintCharacterLayoutBoxes(canvas, layoutCache, offset),
+ );
canvas.drawParagraph(layoutCache.paragraph, offset + layoutCache.paintOffset);
}
+ bool _debugPaintCharacterLayoutBoxes(
+ Canvas canvas,
+ _TextPainterLayoutCacheWithOffset layout,
+ Offset offset,
+ ) {
+ final Paint paint = Paint()
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = 1.0
+ ..color = const Color(0xFF00FFFF);
+ final List<TextBox> textBoxes = getBoxesForSelection(
+ TextSelection(baseOffset: 0, extentOffset: plainText.length),
+ );
+ for (final TextBox textBox in textBoxes) {
+ canvas.drawRect(textBox.toRect().shift(offset), paint);
+ }
+ return true;
+ }
+
// Returns true if value falls in the valid range of the UTF16 encoding.
static bool _isUTF16(int value) {
return value >= 0x0 && value <= 0xFFFFF;
@@ -1441,17 +1479,22 @@
return Offset(adjustedDx, rawOffset.dy + layoutCache.paintOffset.dy);
}
+ // The condition is derived from
+ // https://github.com/google/skia/blob/0086a17e0d4cc676cf88cae671ba5ee967eb7241/modules/skparagraph/src/TextLine.cpp#L1244-L1246
+ // which is set here:
+ // https://github.com/flutter/engine/blob/a821b8790c9fd0e095013cd5bd1f20273bc1ee47/third_party/txt/src/skia/paragraph_builder_skia.cc#L134
+ bool get _strutDisabled => switch (strutStyle) {
+ null || StrutStyle.disabled => true,
+ StrutStyle(:final double? fontSize) => fontSize == 0.0,
+ };
+
/// {@template flutter.painting.textPainter.getFullHeightForCaret}
/// Returns the strut bounded height of the glyph at the given `position`.
/// {@endtemplate}
///
/// Valid only after [layout] has been called.
double getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
- // The if condition is derived from
- // https://github.com/google/skia/blob/0086a17e0d4cc676cf88cae671ba5ee967eb7241/modules/skparagraph/src/TextLine.cpp#L1244-L1246
- // which is set here:
- // https://github.com/flutter/engine/blob/a821b8790c9fd0e095013cd5bd1f20273bc1ee47/third_party/txt/src/skia/paragraph_builder_skia.cc#L134
- if (strutStyle == null || strutStyle == StrutStyle.disabled || strutStyle?.fontSize == 0.0) {
+ if (_strutDisabled) {
final double? heightFromCaretMetrics = _computeCaretMetrics(position)?.height;
if (heightFromCaretMetrics != null) {
return heightFromCaretMetrics;
diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart
index 1066cc4..448ca2f 100644
--- a/packages/flutter/lib/src/rendering/debug.dart
+++ b/packages/flutter/lib/src/rendering/debug.dart
@@ -38,6 +38,15 @@
/// Causes each RenderBox to paint a line at each of its baselines.
bool debugPaintBaselinesEnabled = false;
+/// Causes each RenderParagraph to paint the layout boxes of its text.
+///
+/// {@macro flutter.painting.textPainter.debugPaintTextLayoutBoxes}
+///
+/// See also:
+///
+/// * [debugPaintBaselinesEnabled] which helps debug text alignment.
+bool debugPaintTextLayoutBoxes = false;
+
/// Causes each Layer to paint a box around its bounds.
bool debugPaintLayerBordersEnabled = false;
@@ -321,6 +330,7 @@
if (debugPaintSizeEnabled ||
debugPaintBaselinesEnabled ||
debugPaintLayerBordersEnabled ||
+ debugPaintTextLayoutBoxes ||
debugPaintPointersEnabled ||
debugRepaintRainbowEnabled ||
debugRepaintTextRainbowEnabled ||
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
index f3df3a5..e8834cc 100644
--- a/packages/flutter/lib/src/rendering/paragraph.dart
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
@@ -1007,6 +1007,11 @@
}
}
+ assert(() {
+ _textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes;
+ return true;
+ }());
+
_textPainter.paint(context.canvas, offset);
paintInlineChildren(context, offset);
diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart
index 7df7658..357f46b 100644
--- a/packages/flutter/test/painting/text_painter_test.dart
+++ b/packages/flutter/test/painting/text_painter_test.dart
@@ -1957,6 +1957,30 @@
expect(painter.height, 10);
});
+ test('debugPaintTextLayoutBoxes', () {
+ const TextSpan span = TextSpan(
+ text: 'M',
+ // ascent = 96, descent = 32
+ style: TextStyle(fontSize: 128),
+ children: <InlineSpan>[TextSpan(text: 'M', style: TextStyle(fontSize: 64))],
+ );
+
+ final TextPainter painter = TextPainter()
+ ..textDirection = TextDirection.ltr
+ ..text = span
+ ..layout();
+ expect(
+ (Canvas canvas) {
+ painter.debugPaintTextLayoutBoxes = true;
+ painter.paint(canvas, Offset.zero);
+ painter.debugPaintTextLayoutBoxes = false;
+ },
+ paints
+ ..rect(rect: Offset.zero & const Size.square(128))
+ ..rect(rect: const Offset(128, 96 - 48) & const Size.square(64)),
+ );
+ });
+
test('TextPainter dispatches memory events', () async {
await expectLater(
await memoryEvents(() => TextPainter().dispose(), TextPainter),