From 844edbcd91ba0aa7b1b2d8d809689c69ebeb7222 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:49:30 -0700 Subject: [PATCH] add `debugPaintTextLayoutBoxes` for debugging text layout (#168876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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), ), ]) ``` image ## 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]. [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 --- .../lib/src/painting/text_painter.dart | 53 +++++++++++++++++-- packages/flutter/lib/src/rendering/debug.dart | 10 ++++ .../flutter/lib/src/rendering/paragraph.dart | 5 ++ .../test/painting/text_painter_test.dart | 24 +++++++++ 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index af1ff100a04..c0ca1b01c78 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -1291,6 +1291,22 @@ class TextPainter { _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 @@ class TextPainter { 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 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 @@ class TextPainter { 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 1066cc4e962..448ca2fda0b 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -38,6 +38,15 @@ bool debugPaintSizeEnabled = false; /// 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 @@ bool debugAssertAllRenderVarsUnset(String reason, {bool debugCheckIntrinsicSizes 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 f3df3a53702..e8834ccec71 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -1007,6 +1007,11 @@ class RenderParagraph extends RenderBox } } + 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 7df7658ac06..357f46b9bed 100644 --- a/packages/flutter/test/painting/text_painter_test.dart +++ b/packages/flutter/test/painting/text_painter_test.dart @@ -1957,6 +1957,30 @@ void main() { expect(painter.height, 10); }); + test('debugPaintTextLayoutBoxes', () { + const TextSpan span = TextSpan( + text: 'M', + // ascent = 96, descent = 32 + style: TextStyle(fontSize: 128), + children: [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),