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),