Skip to content

mixin_markdown_widget: resolve selection geometry from text leaves - #544

Merged
boyan01 merged 4 commits into
mixin_markdown_widget/step0-fixesfrom
mixin_markdown_widget/selection-leaf-refactor
Oct 21, 2026
Merged

boyan01 merged 4 commits into
mixin_markdown_widget/step0-fixesfrom
mixin_markdown_widget/selection-leaf-refactor

Conversation

@boyan01

@boyan01 boyan01 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #543. Selection geometry moves from one resolver that walks GlobalKey registries and re-implements hit testing per container kind, to text leaves that answer their own geometry. Offsets convert between leaf-local and block display space once, in StructuredBlockSelection.

 SelectableMarkdownBlock (painter unchanged: merge, corner joins, clip, paint order)
-  MarkdownSelectionResolver (2970 lines)
-    resolveList* / resolveQuote* / resolveDetails* / resolveTable* ...
-    walks listItemChildKeys, quoteChildKeys, tableCellKeys, ...
+  MarkdownSelectionLeafScope
+    MarkdownSelectableLeaf(path)          # rectsFor / offsetAt / lineRangeAt / clipRect
+  markdown_selection_geometry.dart       # one container algorithm for list/quote/details/footnote/deflist/table
pointer → container: which leaf? → leaf.offsetAt(local)
        → StructuredBlockSelectionLeaf: local → block display offset (once)
        → controller.setSelection(DocumentPosition)
  • StructuredBlockSelection.forBlock is cached per block (Expando) and is the single source for serializer, clamp, and select-all. The controller always delegates to its serializer; the runtimeType fast path is gone, and custom serializer subclasses still work.
  • Ported rules: gap between child blocks (midpoint within ±24px horizontal overlap), list marker hit order and prefix rect aligned to the first child's first line, table triple-click selects the cell, code/table viewport clipping.
  • AGENTS.md updated; removed the stale TableCellSelection description.
file before after
markdown_selection_resolver.dart → markdown_block_keys_registry.dart 2970 38
markdown_descriptor_extractor.dart 1315 738
markdown_block_builder.dart 2372 1828
selection_controller.dart 272 96
new markdown_selectable_leaf.dart – 782
new markdown_selection_geometry.dart – 845

Net: −4803 / +2233 lines in lib/.

Commits:

  1. pin selection geometry and gestures before the leaf refactor: tests only, on the old implementation.
  2. resolve selection geometry from text leaves: the refactor, no test file changed.

Evidence

  • Commit 1 adds 44 tests (selection_geometry_snapshot_test.dart: painter rect snapshots within 0.5px for paragraphs, nested lists/quotes, details, deflists, footnotes, tables incl. scrolled-in-quote clipping, scrolled code; selection_gesture_edge_test.dart: gaps, markers, line ends, cross-block drags, blank-space drags, multi-click in nested containers, link taps, copy across unmounted ListView rows). Before: 44/44 pass on the old lib/. After: 44/44 pass on the new lib/ with no test file edited.
  • flutter test: 258 passed. flutter analyze: no issues.
  • benchmark/selection_geometry_benchmark.dart (avg per selection update): medium 2056 → 2086–2298 µs, large 1846 → 1568–2129 µs; run-to-run noise, no clear regression. incremental_append_benchmark.dart: 468x → 499x, 1533x → 1603x.
  • Image goldens were not added: CI runs on ubuntu and they would be platform-dependent; rect snapshots pin painter input instead.

Merge Danger

Door: two-way

Internal only; public API and DocumentPosition semantics unchanged. Revert restores the resolver.

Blast Radius: selection

  • Child block GlobalKeys are gone, so a child subtree is not reparented when its structure changes (e.g. a streamed list item going from one child to several); a nested details expanded state or image state may rebuild.
  • Container plainText now comes from the structure index instead of descriptors; they agreed in all tested cases.
  • Small behavior changes: nested custom codeBlockBuilder becomes a placeholder leaf instead of throwing; empty table cells are no longer hit targets; ragged tables no longer risk a RangeError.
  • Selection still rebuilds on change (repaint-only was not attempted: existing tests read SelectableMarkdownBlock.selectionRange after setSelection).
  • Needs GUI verification: nested list/quote/details drag, marker area, triple-click units, horizontal scroll clipping, footnote/deflist copy, MixinSelectionArea cross-widget drag, context menu and Cmd+A/C.

@boyan01
boyan01 added this pull request to stack #545 October 20, 2026 12:01
@boyan01 boyan01 changed the title mixin markdown widget/selection leaf refactor mixin_markdown_widget: resolve selection geometry from text leaves Oct 20, 2026
… leaf refactor

Add safety-net tests that pass on the current renderer so the selection
geometry can be restructured without visual or behavioral drift.

- selection_geometry_snapshot_test.dart records the block-local rects,
  clip rect, and paint order that each block's selection painter receives
  for paragraphs, list items with marker prefixes, nested lists, nested
  quotes with headings, details, definition lists, footnotes, table cells
  across rows, a scrolled table inside a quote, a scrolled code block, and
  inline code/math. Rects are compared within 0.5px.
- selection_gesture_edge_test.dart drives mouse gestures for gaps between
  quote/list children, list markers, pointers past a line end, drags across
  top-level blocks and into blank space, double/triple clicks in nested
  lists, quotes and table cells, link taps with selection enabled, and
  copying a drag selection that spans a block scrolled out of a ListView.
Each selectable text leaf now answers its own selection geometry, and
container blocks only order leaves.

- Cache StructuredBlockSelection and serialized block text per BlockNode.
  The plain text serializer is the single source for copy, clamping, and
  select-all; the selection controller delegates to it, so a serializer
  subclass changes both consistently.
- Add MarkdownSelectableLeaf: text, code, table cell, and placeholder
  leaves register by PathInBlock in a per-block scope and expose rects,
  offsets, line ranges, an optional viewport clip, and their origin.
- Replace the per-container resolvers with one container algorithm for
  quotes, details, lists, footnotes, and tables. Leaf-local and block
  display offsets are converted once, through the structured leaves.
  Container specs now take their text from the structured selection.
- Remove the old selection resolver, descriptor-based unit ranges, and
  the per-child GlobalKey maps for list items, quote/details children,
  and table cells. Block keys, scroll controllers, and viewport keys stay.

Public API and selection semantics are unchanged.
Flutter tooling updated the example project when running it on macOS.
…f a leaf

Double clicking the right half of the last character of a table cell,
quote paragraph, list item, or details summary selected the word of the
next leaf instead. Hit testing turns the pointer into a block display
offset and resolves it downstream, so the end of a leaf lands on the
start of the next leaf, and word selection then looked up the word there.
For a table such as `| Alpha | | Charlie |`, the word was `Charlie` and
copy returned the wrong cell.

selectWord already detects that the pointer is over the character before
the offset; it now resolves the position upstream in that case, so the
word comes from the leaf that ends there.
@boyan01
boyan01 force-pushed the mixin_markdown_widget/selection-leaf-refactor branch from 6e90993 to d49526b Compare October 20, 2026 13:29
@boyan01
boyan01 merged commit 9009859 into main Oct 21, 2026
8 of 11 checks passed
@boyan01
boyan01 deleted the mixin_markdown_widget/selection-leaf-refactor branch October 21, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant