flutter / flutter/flutter

RenderEditable.getBoxesForSelection gets selection width for longest line width

Open
#180,311 2 comments 0 reactions 0 assignees View on GitHub
a: text input a: typography c: regression P1 team-text-input triaged-text-input
Dominant language
Dart
Stars
179k
Forks
31.1k
PR merge metrics
PR metrics pending

Description

### Steps to reproduce

Run the attached example on Flutter 3.35 or 3.38

### Expected results

This is how it looks on Flutter 3.27
RenderEditable.getBoxesForSelection for Text Selection from 39 to 51 (second line) only returns coordinates of the 2nd line which gets correctly highlighted/underlined.

![Image](https://github.com/user-attachments/assets/37bc1813-3df6-4d27-b0de-5abd97262653)

### Actual results

On Flutter version 3.35 and 3.38, same code returned coordinates for the longest line width, which highlights/underlines more than the actual TextSelection was requested.

![Image](https://github.com/user-attachments/assets/871ed8b9-0f98-490f-9618-d4d9ff1694fc)

### Code sample

Code sample

```dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RenderEditable Bug Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'RenderEditable Bug Demo'),
);
}
}

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

final String title;

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

class _MyHomePageState extends State {
final GlobalKey customPaintKey = GlobalKey();
final ScrollController scrollController = ScrollController();
final TextEditingController controller = TextEditingController();
final List errorLines = [const TextSelection(baseOffset: 39, extentOffset: 51)]; // "Highlight me" position
ErrorLinesPainter? painter;
@override
void initState() {
controller.text = '''
Some long lines of text should go here
Highlight me
Doesn't matter
''';
painter = ErrorLinesPainter(
customPaintKey,
const TextStyle(),
controller,
);
painter!.lineErrorPositions = errorLines;
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: DumbVisitor(
onFound: (ets) {
painter?.setupEditableTextState(ets);
},
child: CustomPaint(
key: customPaintKey,
foregroundPainter: painter,
child: TextField(
controller: controller,
maxLines: null,
expands: true,
readOnly: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
),
);
}
}

class ErrorLinesPainter extends CustomPainter {
ErrorLinesPainter(this.customPaintKey, this.textStyle, Listenable listenable) : super(repaint: listenable);
GlobalKey customPaintKey;
RenderEditable? re;

final TextStyle textStyle;

List lineErrorPositions = [];

@override
void paint(Canvas canvas, Size size) {
if (re case RenderEditable re) {
final ancestor = customPaintKey.currentContext!.findRenderObject();
final offset = re.localToGlobal(Offset.zero, ancestor: ancestor);
for (final e in lineErrorPositions) {
final boxes = re.getBoxesForSelection(e);
if (boxes.isNotEmpty) {
final firstBox = boxes.first.toRect();
final lastBox = boxes.last.toRect();
canvas.drawLine(
Offset(firstBox.left + offset.dx, firstBox.bottom + offset.dy),
Offset(lastBox.right + offset.dx, firstBox.bottom + offset.dy),
Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke
..filterQuality = FilterQuality.low
..strokeCap = StrokeCap.round
..color = Colors.red.shade500);
}
}
}
}

@override
bool shouldRepaint(ErrorLinesPainter oldDelegate) => false;

void setupEditableTextState(EditableTextState ets) {
re = ets.renderEditable;
}
}

class DumbVisitor extends StatelessWidget {
const DumbVisitor({
super.key,
required this.onFound,
required this.child,
});

final void Function(T object) onFound;
final Widget child;

@override
Widget build(BuildContext context) => child;

@override
StatelessElement createElement() => _DumbVisitorElement(this, onFound);
}

class _DumbVisitorElement extends StatelessElement {
_DumbVisitorElement(super.widget, this.onFound);

final void Function(T object) onFound;
Element? oldElement;

@override
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
final element = super.updateChild(child, newWidget, newSlot);
if (oldElement != element) {
oldElement = element;
element?.visitChildren(_visitor);
}
return element;
}

void _visitor(Element child) {
if (child is StatefulElement && child.state is T) {
onFound(child.state as T);
} else if (child.renderObject is T) {
onFound(child.renderObject as T);
}
child.visitChildren(_visitor);
}
}

```

### Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

### Logs

Logs

```console
[Paste your logs here]
```

### Flutter Doctor output

Doctor output

```console
[!] Flutter (Channel [user-branch], 3.38.5, on macOS 14.7.1 23H222 darwin-arm64, locale en-GB)
! Flutter version 3.38.5 on channel [user-branch] at xxxx
Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/setup.
! Upstream repository unknown source is not a standard remote.
Set environment variable "FLUTTER_GIT_URL" to unknown source to dismiss this error.
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
[✓] Chrome - develop for the web
[✓] Connected device (3 available)
[✓] Network resources

! Doctor found issues in 2 categories.
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.