RenderEditable.getBoxesForSelection gets selection width for longest line width
- 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.

### 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.

### 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
Assessment
This issue has not been assessed yet.