Skip to content

Wrong TextRange result in TextPainter on web #188874

Closed

Description

@erickzanardo

Steps to reproduce

Consider the following small app:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    const text = 'AAA AA AAA A A A';

    final textPainter = TextPainter(
      text: TextSpan(
        text: text,
        style: TextStyle(
          color: Color(0xFFFFFFFF),
          fontFamily: 'Arial',
          fontSize: 24,
        ),
      ),
      textDirection: TextDirection.ltr,
    );

    textPainter.layout(maxWidth: 184);
    textPainter.computeLineMetrics();

    final rangeAt0 = textPainter.getLineBoundary(TextPosition(offset: 0));

    final rangeAt15 = textPainter.getLineBoundary(TextPosition(offset: 15));

    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Text('Range at 0 offset: S: ${rangeAt0.start} E: ${rangeAt0.end}'),
            Text(
              'Range at 15 offset: S: ${rangeAt15.start} E: ${rangeAt15.end}',
            ),
          ],
        ),
      ),
    );
  }
}

Expected results

Although I understand that I can't exepect the same text range result between different platforms since font rendering and size calculation is affect by the platform, web is showing an inconsistent result compared to the other platforms since the range at offset 15 should start at 15, not end at 15 (unless I understood something wrongly, and if that is so, please let me know)

Actual results

When rendering on web, the range at the offeset 15 will result in a wrong value:

Range at 0 offset: Start 0 End: 15
Range at 15 offset: Start 0 End: 15

When running on MacOS for example we get instead

Range at 0 offset: Start 0 End: 15
Range at 15 offset: Start 15 End: 16
Image

Code sample

Code sample
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    const text = 'AAA AA AAA A A A';

    final textPainter = TextPainter(
      text: TextSpan(
        text: text,
        style: TextStyle(
          color: Color(0xFFFFFFFF),
          fontFamily: 'Arial',
          fontSize: 24,
        ),
      ),
      textDirection: TextDirection.ltr,
    );

    textPainter.layout(maxWidth: 184);
    textPainter.computeLineMetrics();

    final rangeAt0 = textPainter.getLineBoundary(TextPosition(offset: 0));

    final rangeAt15 = textPainter.getLineBoundary(TextPosition(offset: 15));

    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            Text('Range at 0 offset: S: ${rangeAt0.start} E: ${rangeAt0.end}'),
            Text(
              'Range at 15 offset: S: ${rangeAt15.start} E: ${rangeAt15.end}',
            ),
          ],
        ),
      ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
flutter doctor -v
[✓] Flutter (Channel stable, 3.44.4, on macOS 26.3.1 25D771280a darwin-arm64, locale en-BR) [1,884ms]
    • Flutter version 3.44.4 on channel stable at /Users/erickzanardo/flutters/updated
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ad70ec4617 (7 days ago), 2026-06-24 11:07:06 -0700
    • Engine revision a10d8ac38d
    • Dart version 3.12.2
    • DevTools version 2.57.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android,
      enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file,
      enable-lldb-debugging, enable-uiscene-migration

[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [3.7s]
    • Android SDK at /Users/erickzanardo/Library/Android/sdk
    • Emulator version 35.2.10.0 (build_id 12414864) (CL:N/A)
    • Platform android-36, build-tools 36.0.0
    • ANDROID_HOME = /Users/erickzanardo/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.3+-79915917-b509.11)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [2.8s]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17F113
    • CocoaPods version 1.16.2

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

[✓] Connected device (4 available) [8.1s]
    • sdk gphone64 arm64 (mobile) • emulator-5554                        • android-arm64  • Android 15 (API 35) (emulator)
    • iPhone 17 Pro (mobile)      • F720CF45-D57D-4431-B060-6BBF0F299F40 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-26-0 (simulator)
    • macOS (desktop)             • macos                                • darwin-arm64   • macOS 26.3.1 25D771280a
      darwin-arm64
    • Chrome (web)                • chrome                               • web-javascript • Google Chrome 149.0.7827.201
    ! Error: Browsing on the local area network for iPhone de Erick. Ensure the device is unlocked and attached with a cable or
      associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)

[✓] Network resources [329ms]
    • All expected network resources are available.

! Doctor found issues in 1 category.

Note: I would be happy to work on a fix if we confirm this is a real issue!

Activity

  1. added
    in triagePresently being triaged by the triage team
    platform-webWeb applications specifically
    team-webOwned by Web platform team
    and removed
    in triagePresently being triaged by the triage team
    on Jul 2, 2026
  2. flutter-zl commented on Jul 8, 2026

    @flutter-zl
    Contributor

    This issue was fixed by flame-engine/flame#3939.

  3. erickzanardo commented on Jul 8, 2026

    @erickzanardo
    MemberAuthor

    @flutter-zl That PR didn't solved the issue on Flutter. What I did in that PR was to provide an workaround on the flame engine to avoid running in this issue. It still needs to be fixed in the flutter engine since the TextRange will still be reported wrongly.

  4. self-assigned this
    on Jul 8, 2026
  5. added
    P1High-priority issues at the top of the work list
    triaged-webTriaged by Web platform team
    on Jul 8, 2026
  6. GhagSagar23 commented on Jul 15, 2026

    @GhagSagar23
    Contributor

    I traced this down to a divergence between the native dart:ui binding and the CanvasKit web implementation of Paragraph.getLineBoundary.

    Native (engine/src/flutter/lib/ui/text.dart, L3322-L3343) explicitly handles the case where an offset sits exactly on the boundary between two lines: it queries both the current line and offset + 1's line, and if position.affinity == TextAffinity.downstream and the offset equals both the current line's end and the next line's start, it returns the next line's range. TextPosition's default affinity is downstream, so TextPosition(offset: 15) in the repro correctly resolves to line 2 on native platforms.

    CanvasKit's web implementation (engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart, L974-L984) has no affinity handling at all — it's a plain first-match loop:

    for (final metric in metrics) {
      if (offset >= metric.startIndex && offset <= metric.endIndex) {
        return ui.TextRange(start: metric.startIndex.toInt(), end: metric.endIndex.toInt());
      }
    }
    ` ``
    
    Since `metric.endIndex` of line N equals `metric.startIndex` of line N+1 at a wrap point, offset 15 matches line 0 first (inclusive `<=` on both bounds) and the loop never considers affinity or looks ahead — hence it always returns the earlier line's range, which is the bug.
    
    Fix should port the same affinity-aware next-line lookahead from `text.dart`'s native override into `CkParagraph.getLineBoundary`. FWIW the newer pure-Dart `web_paragraph` renderer (`engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/layout.dart`, [L846-L854](https://github.com/flutter/flutter/blob/fc1ad955f16467c959e3cd8079b760d5af0984aa/engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/layout.dart#L846-L854)) happens to dodge this specific case because it uses a half-open `[start, end)` comparison (`end > codepointPosition`), but it also has no explicit affinity check, so it's worth confirming it doesn't have an inverted version of this bug for `TextAffinity.upstream` positions.
  7. removed their assignment
    on Aug 7, 2026
  8. removed
    P1High-priority issues at the top of the work list
    on Aug 7, 2026
  9. flutter-triage-bot commented on Aug 12, 2026

    @flutter-triage-bot

    This issue is missing a priority label. Please set a priority label when adding the triaged-web label.

  10. self-assigned this
    on Aug 12, 2026
  11. added
    triaged-webTriaged by Web platform team
    P1High-priority issues at the top of the work list
    on Aug 12, 2026
  12. added a commit that references this issue on Sep 30, 2026
    468422c
  13. github-actions commented on Oct 14, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  14. locked as resolved and limited conversation to collaborators on Oct 14, 2026
  15. added a commit that references this issue on Oct 29, 2026
    235b1ce
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listplatform-webWeb applications specificallyteam-webOwned by Web platform teamtriaged-webTriaged by Web platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions