Skip to content

TextPainter line metrics not updated on text re-layout #97298

@tgucio

Description

@tgucio

This is a low level issue that causes other problems whenever line metrics from Skia paragraph renderer are used - e.g. vertical runs using keyboad Down/Up buttons when editing text.

It exists on all platforms but it's more easily seen on desktop.

Steps to Reproduce

  1. Execute flutter run on the code sample on destkop
  2. Click on the floating button to read the last text metrics and total paragraph height from TextPainter
  3. Resize the app window so that the number of text lines is different
  4. Click on the floating button again to update the metrics

Expected results: Line metrics change alongside total height

Actual results: Line metrics don't change

Code sample
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey _paragraphKey = GlobalKey();
  List<String> _lineMetricData = <String>[];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Paragraph(
            key: _paragraphKey,
            inlineSpan: const TextSpan(
              text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do '
                'eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim '
                'ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut '
                'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit '
                'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur '
                'sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt '
                'mollit anim id est laborum.',
              style: TextStyle(fontSize: 16.0, color: Colors.black),
            ),
          ),
          const SizedBox(height: 50.0),
          for (String metricData in _lineMetricData) Text(metricData),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.abc),
        onPressed: _getLineMetrics,
      ),
    );
  }

  void _getLineMetrics() {
    RenderParagraph renderParagraph = _paragraphKey.currentContext!.findRenderObject()! as RenderParagraph;
    List<ui.LineMetrics> lineMetrics = renderParagraph.lineMetrics;
    _lineMetricData = lineMetrics.map((metrics) => '#${metrics.lineNumber} baseLine=${metrics.baseline}').toList();
    _lineMetricData.add('Total text height: ${renderParagraph.height}');
    setState(() {  });
  }
}

class Paragraph extends SingleChildRenderObjectWidget {
  const Paragraph({Key? key, required this.inlineSpan}) : super(key: key);

  final InlineSpan inlineSpan;

  @override
  RenderParagraph createRenderObject(BuildContext context) {
    return RenderParagraph(inlineSpan: inlineSpan);
  }
}

class RenderParagraph extends RenderBox {
  RenderParagraph({required this.inlineSpan})
    : _textPainter = TextPainter(
      text: inlineSpan,
      textDirection: TextDirection.ltr,
    );

  final InlineSpan inlineSpan;
  final TextPainter _textPainter;

  List<ui.LineMetrics> get lineMetrics => _textPainter.computeLineMetrics();
  double get height => _textPainter.height;

  @override
  double computeMinIntrinsicWidth(double height) {
    _layoutText();
    return _textPainter.minIntrinsicWidth;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    _layoutText();
    return _textPainter.maxIntrinsicWidth;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
    return Size(constraints.maxWidth, _textPainter.height);
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
    size = Size(math.min(_textPainter.width, constraints.maxWidth), _textPainter.height);
  }

  void _layoutText({double minWidth = 0.0, double maxWidth = double.infinity}) {
    final double availableMaxWidth = math.max(0.0, maxWidth);
    _textPainter.layout(minWidth: availableMaxWidth, maxWidth: availableMaxWidth);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    _textPainter.paint(context.canvas, offset);
  }
}
Logs
% flutter doctor -v
[✓] Flutter (Channel master, 2.9.0-1.0.pre.494, on macOS 12.1 21C52 darwin-x64, locale en-GB)
    • Flutter version 2.9.0-1.0.pre.494 at /Library/Frameworks/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 78de12b82f (23 hours ago), 2022-01-25 09:55:10 -0800
    • Engine revision b5162c93f5
    • Dart version 2.17.0 (build 2.17.0-51.0.dev)
    • DevTools version 2.9.2

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    • Android SDK at /Users/tgucio/Library/Android/sdk
    • Platform android-31, build-tools 30.0.3
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] VS Code (version 1.63.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.32.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-x64     • macOS 12.1 21C52 darwin-x64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 97.0.4692.99

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

Metadata

Metadata

Assignees

No one assigned

    Labels

    r: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions