feat(ui): drive a multi-select listbox to a known state - #105
Merged
Conversation
listbox_select on a multi listbox toggles, which can only ever say "flip this row". Calling it twice silently undoes itself, so an application that selects a row after every add ends up with the wrong row selected, and there was no way to keep an app's own selection model and the widget's in agreement. The only correct multi-select was the one nobody drove. listbox_set_selected(lb, i, on) writes one row's state and listbox_clear_selection(lb) clears them all. Neither fires on_select: a state write is not a user action, and an app syncing its model would otherwise re-enter its own click handler on every sync and have to guard against the echo. listbox_selected_count(lb) saves every caller writing the same loop over listbox_is_selected. Both writes go through one helper that sets the flag and the row's class together, so a selection can never be half applied, which is the one failure an app cannot debug from outside. listbox_selection_mode(lb, 1) gives the widget the behaviour every editor, file manager and mail client has: plain click replaces, cmd/ctrl-click toggles one row, shift-click extends from the anchor, with the anchor staying put so re-extending does not walk the selection away a row at a time. This cannot be built on top of on_select, which reports a row index and no modifier state. Mode 0, today's toggle-every-click, stays the default because that is what a checklist wants and what listbox_multi has always done. The modifier state comes from a new modifiers(), which reports the keys held right now as the bitmask window_on_key already uses. Reading the live state inside a click callback is what lets a widget tell a plain click from a cmd-click without every click callback growing an argument, which would break every existing caller. Real on all three backends: +modifierFlags on AppKit, the seat's keyboard state on GTK4, GetKeyState on win32. The ListBox struct is hand-sized at each allocation, so both malloc sites grew with the two new fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lection-api # Conflicts: # ci.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
listbox_multicould express "toggle this row" and nothing else. That is notenough to keep an application's own selection model and the widget's in
agreement, and it is not enough to build the selection behaviour every editor,
file manager and mail client has.
Driving the widget to a known state
listbox_selecton a multi listbox toggles, so calling it twice silentlyundoes itself. An app that selects a row after every add ends up with the
previous row selected. There was no write side at all:
listbox_is_selectedreads a row's state and nothing sets it.
listbox_set_selected(lb, i, on)writes one row's state.listbox_clear_selection(lb)clears every row.listbox_selected_count(lb)saves every caller writing the same loop.Neither write fires
on_select. A state write is not a user action, and an appsyncing its model would otherwise re-enter its own click handler on every sync
and have to guard against the echo.
Both writes go through one helper that sets the flag and the row's class
together, so a selection can never be half applied. The model saying selected
while the row does not look it is exactly the failure an app cannot debug from
the outside.
Replace / toggle / extend
listbox_selection_mode(lb, 1)switches a multi listbox to the standardbehaviour: a plain click replaces the selection, cmd/ctrl-click toggles one
row, shift-click extends from the anchor. The anchor stays put across
successive shift-clicks, so re-extending does not walk the selection away one
row at a time.
The issue offered two designs and preferred this one, for the right reason:
the widget already owns
sel_flagsand the hit testing. The alternative, amodifier argument on
on_select, would change the arity of a callback everyexisting caller has already written.
Mode 0, today's toggle-every-click, stays the default. That is what a checklist
wants and what
listbox_multihas always done, so nothing existing changesbehaviour.
modifiers()The widget needs to know whether cmd was held, and a click callback carries
only a row index.
modifiers()reports the keys held right now as the bitmaskwindow_on_keyalready uses (1 shift, 2 ctrl, 4 alt, 8 super/command), whichis the live platform state at the moment the click is being handled. Real on
all three backends:
+modifierFlagson AppKit, the seat's keyboard state onGTK4,
GetKeyStateon win32. It is exported, so an app can use it in its ownclick handlers too.
Verification
aeb .all.aebuilds clean on AppKit, zero warnings.selmode_demoplusspec_selmode_demoassert the property the old API couldnot deliver: pressing "set 1 and 3" twice leaves the same two rows selected
rather than undoing itself, "clear" empties the selection, and
"clear then set" is how an app says "only this one". Wired into ci.sh beside
the existing multi-select spec, so it runs on AppKit and GTK4.
ListBoxstruct is hand-sized at each allocation rather than by thecompiler, so both
mallocsites were grown for the two new fields. Missingone would corrupt the heap silently, so this is called out in the commit.
Closes #99
🤖 Generated with Claude Code