Skip to content

feat(ui): drive a multi-select listbox to a known state - #105

Merged
nicolas-maman merged 2 commits into
mainfrom
feat/listbox-multi-selection-api
Sep 4, 2026
Merged

feat(ui): drive a multi-select listbox to a known state#105
nicolas-maman merged 2 commits into
mainfrom
feat/listbox-multi-selection-api

Conversation

@nicolas-maman

Copy link
Copy Markdown
Contributor

What

listbox_multi could express "toggle this row" and nothing else. That is not
enough 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_select on a multi listbox toggles, so calling it twice silently
undoes 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_selected
reads 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 app
syncing 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 standard
behaviour: 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_flags and the hit testing. The alternative, a
modifier argument on on_select, would change the arity of a callback every
existing caller has already written.

Mode 0, today's toggle-every-click, stays the default. That is what a checklist
wants and what listbox_multi has always done, so nothing existing changes
behaviour.

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 bitmask
window_on_key already uses (1 shift, 2 ctrl, 4 alt, 8 super/command), which
is the live platform state at the moment the click is being handled. Real on
all three backends: +modifierFlags on AppKit, the seat's keyboard state on
GTK4, GetKeyState on win32. It is exported, so an app can use it in its own
click handlers too.

Verification

  • aeb .all.ae builds clean on AppKit, zero warnings.
  • selmode_demo plus spec_selmode_demo assert the property the old API could
    not 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.
  • The ListBox struct is hand-sized at each allocation rather than by the
    compiler, so both malloc sites were grown for the two new fields. Missing
    one would corrupt the heap silently, so this is called out in the commit.

Closes #99

🤖 Generated with Claude Code

nicolas-maman and others added 2 commits September 4, 2026 17:44
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>
@nicolas-maman
nicolas-maman merged commit 5cc6db1 into main Sep 4, 2026
3 checks 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.

listbox_multi can only toggle: no way to set/clear a row, and on_select reports no modifier

1 participant