From 1e3124d90cfcb99ef61340a87e55ffd6f16e4eae Mon Sep 17 00:00:00 2001 From: VAUsoltsev Date: Thu, 20 Aug 2026 17:26:13 +0300 Subject: [PATCH] Measure the line grid with the strut the lines are laid out with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_preferredLineHeight` is the grid the editor places lines on. It was measured by a `TextPainter` without the strut, while paragraphs are laid out with `forceStrutHeight: true`. Those are different numbers — for Consolas at 13.09 logical pixels, 21 against 18 — so lines were spaced three points further apart than the height they were actually given. Two visible consequences: * a selection dragged across several lines looked striped: the boxes could not reach each other across the gap; * the three points per line accumulated into the correction the layout applies to the scroll offset, pushing the offset above the top of the document. The physics sprang it back, so scrolling up by a page ended in a bounce. Two places measure that grid — `_CodeParagraphProvider.updateBaseStyle` and `_CodeFieldRender._calculatePreferredLineHeight` — and both now use the same strut as the paragraphs. Selection boxes are snapped to the grid as well. `BoxHeightStyle.max` measures the glyph runs, which are shorter than the line; an empty line made the difference plain, since `getRangeRects` already returns a box of a full `preferredLineHeight` for it. --- example/pubspec.lock | 26 +++++++++++++------------- lib/src/_code_field.dart | 15 +++++++++++++++ lib/src/_code_paragraph.dart | 34 +++++++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 53164fc..af91d09 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,18 +13,18 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" flutter: dependency: "direct main" description: flutter @@ -66,18 +66,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.18.0" path: dependency: transitive description: @@ -92,7 +92,7 @@ packages: path: ".." relative: true source: path - version: "0.9.0" + version: "0.10.0" re_highlight: dependency: "direct main" description: @@ -105,7 +105,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" stream_channel: dependency: transitive description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=1.17.0" diff --git a/lib/src/_code_field.dart b/lib/src/_code_field.dart index f5d4ff9..934d4ea 100644 --- a/lib/src/_code_field.dart +++ b/lib/src/_code_field.dart @@ -1183,8 +1183,23 @@ class _CodeFieldRender extends RenderBox implements MouseTrackerAnnotation { } void _calculatePreferredLineHeight() { + // Measured with the same strut the paragraphs are laid out with, see + // `_CodeParagraphProvider.updateBaseStyle`. + // + // This number is the grid the editor places lines on, so it has to be the + // height a line actually gets. Measured without the strut it was three + // points taller here, and every line scrolled past added that much to the + // correction the layout applies to the scroll offset. Scrolling up by a + // page then pushed the offset above the top of the document and the + // physics sprang it back — a bounce nobody asked for. final TextPainter painter = TextPainter( textDirection: TextDirection.ltr, + strutStyle: StrutStyle( + fontSize: _textStyle.fontSize, + fontFamily: _textStyle.fontFamily, + height: _textStyle.height, + forceStrutHeight: true, + ), ); painter.text = TextSpan( text: '0', diff --git a/lib/src/_code_paragraph.dart b/lib/src/_code_paragraph.dart index d975fed..bddedb9 100644 --- a/lib/src/_code_paragraph.dart +++ b/lib/src/_code_paragraph.dart @@ -128,7 +128,20 @@ class _ParagraphImpl extends IParagraph { if (range.isCollapsed) { return const []; } - return paragraph.getBoxesForRange(range.start, range.end, boxHeightStyle: ui.BoxHeightStyle.max).map((e) => e.toRect()).toList(); + // Selection boxes span the whole line of the grid rather than the ascent + // and descent of the glyphs. + // + // `BoxHeightStyle.max` measures the runs, and a run is shorter than the + // line it sits on, so adjacent boxes did not touch and a selection dragged + // over several lines looked striped. An empty line made it obvious: the + // branch above returns a box of a full `preferredLineHeight` for it, so + // the gap changed depending on whether a line had any text on it. + return paragraph.getBoxesForRange(range.start, range.end, boxHeightStyle: ui.BoxHeightStyle.max).map((e) { + final Rect rect = e.toRect(); + final int line = max(0, (rect.center.dy / _preferredLineHeight).floor()); + final double top = line * _preferredLineHeight; + return Rect.fromLTRB(rect.left, top, rect.right, top + _preferredLineHeight); + }).toList(); } Offset? _getOffsetDownstream(int position) { @@ -210,19 +223,26 @@ class _CodeParagraphProvider { if (uiStyle == _style) { return; } + // One strut for both the paragraph layout and the line height measured + // below. Measured without the strut, `_preferredLineHeight` is a different + // number than the height a line is actually given: for Consolas 13.09 it + // is 21 against 18. Lines are then placed on a grid that is taller than + // they are, which shows up as a stripe of background between them. + final StrutStyle strutStyle = StrutStyle( + fontSize: style.fontSize, + fontFamily: style.fontFamily, + height: style.height, + forceStrutHeight: true, + ); _paragraphStyle = style.getParagraphStyle( textAlign: TextAlign.left, textDirection: TextDirection.ltr, - strutStyle: StrutStyle( - fontSize: style.fontSize, - fontFamily: style.fontFamily, - height: style.height, - forceStrutHeight: true, - ) + strutStyle: strutStyle ); _style = uiStyle; final TextPainter painter = TextPainter( textDirection: TextDirection.ltr, + strutStyle: strutStyle, ); painter.text = TextSpan( text: '0',