Skip to content

Let the application handle selection state updates - #5

Merged
tilladam merged 4 commits into
tilladam:mainfrom
OlivierJG:main
Aug 8, 2026
Merged

Let the application handle selection state updates#5
tilladam merged 4 commits into
tilladam:mainfrom
OlivierJG:main

Conversation

@OlivierJG

Copy link
Copy Markdown
Contributor

Rather than the node editor directly updating its own state (requiring a two-way sync to application state), only notify of selection change events and let the application decide whether/how to handle them.
wire_selection! provided for examples and simple cases where an application doesn't care to own the selection model.

Olivier de Gaalon and others added 4 commits August 5, 2026 08:35
The editor kept its own selection (two id arrays, a version counter and two
is-*-selected computation callbacks) while every embedder kept a second copy —
two sources of truth kept in step by hand-written sync callbacks.

Now the editor keeps none. It reports gestures — node-selected, select-link,
selection-cleared and the new box-selection-committed, which hands over the
marquee rectangle in world coordinates plus the shift state captured at
press — and renders the `selected` flag it finds on the rows: a new LinkData
field, and whatever field the embedder's node model already binds to
BaseNode.selected. Apply an intent synchronously and the click/drag logic
reads the new flags back within the same event, as it always did.

Rust side: project_selection writes a SelectionManager into model rows
(diff-based, so untouched rows don't re-render), extend_selection covers
shift+marquee, and wire_node_selection! closes the loop for the common case.
NodeEditorSetup's private selection mirror is gone — hand it the application's
selection with with_selection() to keep multi-node drag.

Gone with it: selected-node-ids, selected-link-ids, selection-version,
selection-changed, sync-selection-to-nodes/links, is-node-selected,
is-link-selected, compute-box-selection, compute-link-box-selection,
SelectionManager::sync_to_model/sync_from_model, and a dead handle-link-click.
The editor already held no selection state, but the host still had to keep a
SelectionManager beside the rows and remember to project one into the other.
Two records of one fact, kept in step by hand — and `GraphLogic::commit_drag`
read the wrong one: it walked the model asking an external set about rows that
carry `selected` themselves, so a drag's visuals and its commit could disagree.

Now the rows are the only record. `selection::resolve_click` / `resolve_box`
hold the policy — pure, generic over the id type, absolute sets in and out,
order-stable — and `project_selection` / `selected_rows` move a set between a
set and the rows. `apply_click` / `apply_box` / `clear_selection` compose the
three, which is all `wire_selection!` needs; it stores nothing. A host can keep
selection elsewhere — in its own document or session state — and project after
every write, or let the rows be the store, as the examples now do.

`commit_drag` reads `selected` off the row — the same data the editor renders,
so the two cannot disagree — and moves the dragged node plus anything else
selected. `end-node-drag` carries the dragged id to make that self-contained.
`MovableNode` gains `selected()`: a drag that commits the co-selected set has
to know the co-selected set, and the row already carries it.

That kills the synchrony requirement. The only live read-back of `selected`
after an emitted intent was `start-node-drag`'s `already-selected`; it now comes
from press-time latches (`was-selected-on-press || selection-intent-sent`), so a
late-applied intent costs at most a frame of sibling lag instead of silently
replacing a shift-toggle in flight.

Gone: SelectionManager and NodeEditorSetup::with_selection (the double handoff
that let the two sources diverge), wire_node_selection!, and the editor's
unreachable drag API — start/update/end-node-drag as component functions plus
node-drag-started/-ended, which nothing has ever invoked: BaseNode talks
straight to NodeEditorInternalCallbacks and nothing routed back.

Grid snapping goes with it. Its only reader sat inside that unreachable
end-node-drag, so snap-on-drag has never once run, default-true, with a checkbox
in the advanced example bound to it. Deleting it is the one behavior change
here, and it is example-only: node creation in `advanced` no longer snaps.
Position policy belongs to the host at commit time; it needs no library surface.

wire_selection! takes two arms, nodes or nodes-and-links, so LinkData.selected
finally has a blessed and tested path. level4 now drives real pointer gestures
as well as intents — including the shift-marquee, which pins down that shift is
captured at press and replayed at commit, since the release carries no
modifiers. That test caught a real asymmetry: clicking a link dropped the node
selection, but clicking a node left links selected.
A shift-click on a node cleared the link selection and vice versa, so no
gesture could ever build a mixed set — an artifact of where the shift flag
flowed (into the click resolver, never into the cross-kind decision).
Meanwhile a shift-marquee extends both kinds at once and a delete acts on
whatever is selected, of either kind.

So the cross-kind clear now happens only for a plain click. Shift leaves
the other model alone, in the two-model macro arm and in the advanced
example, which hand-wires the same policy.
@tilladam
tilladam merged commit f304fb1 into tilladam:main Aug 8, 2026
1 check passed
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.

2 participants