Search code examples
flutterdarttexttext-rendering

What does Paragraph.getBoxesForRange actually get in Flutter


The Paragraph class in Flutter has a method called getBoxesForRange.

The documentation says:

Returns a list of text boxes that enclose the given text range.

However, it's not clear to me if the boxes are the boxes around the lines of text that include the given range or if they are something else.

After doing further research, I found an answer, which I'm adding below.


Solution

  • Short answer

    Paragraph.getBoxesForRange gives you what would essentially be the highlighted area for the given range if it were selected. You get more than one box if the range wraps to a new line (or if it contains mixed bi-directional text).

    Longer answer

    Given the string:

    Hello, world.
    

    If you take the range from 1 to 8 like so:

    final boxes = paragraph.getBoxesForRange(1, 8);
    

    This will give you a single box:

    [
      TextBox.fromLTRBD(22.3, -0.3, 112.0, 35.0, TextDirection.ltr)
    ]
    

    Graphically represented it would look like this:

    enter image description here

    This shows that the boxes are not full lines but only wrap the specified text range.

    If you restrict the width so that the paragraph is forced to line wrap the text, you get the following:

    enter image description here

    [
      TextBox.fromLTRBD(22.3, -0.3, 47.0, 35.0, TextDirection.ltr), 
      TextBox.fromLTRBD(0.0, 34.7, 41.8, 70.0, TextDirection.ltr), 
      TextBox.fromLTRBD(0.0, 69.7, 23.2, 105.0, TextDirection.ltr)
    ]
    

    Full code

    Here is the full code if you want to play around with it yourself.

    import 'package:flutter/material.dart';
    import 'dart:ui' as ui;
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            body: HomeWidget(),
          ),
        );
      }
    }
    
    class HomeWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: CustomPaint(
            size: Size(300, 300),
            painter: MyPainter(),
          ),
        );
      }
    }
    
    class MyPainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final textStyle = ui.TextStyle(
          color: Colors.black,
          fontSize: 30,
        );
        final paragraphStyle = ui.ParagraphStyle(
          textDirection: TextDirection.ltr,
        );
        final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)
          ..pushStyle(textStyle)
          ..addText('Hello, world.');
        final constraints = ui.ParagraphConstraints(width: 300);
        // final constraints = ui.ParagraphConstraints(width: 50);
        final paragraph = paragraphBuilder.build();
        paragraph.layout(constraints);
        canvas.drawParagraph(paragraph, Offset.zero);
    
        final boxes = paragraph.getBoxesForRange(1, 8);
        print(boxes);
        final paint = Paint()
          ..color = Colors.blue
          ..style = PaintingStyle.stroke
          ..strokeWidth = 1;
        for (final box in boxes) {
          final rect = Rect.fromLTRB(box.left, box.top, box.right, box.bottom);
          canvas.drawRect(rect, paint);
        }
      }
    
      @override
      bool shouldRepaint(CustomPainter old) {
        return false;
      }
    }