diff --git a/CHANGELOG.md b/CHANGELOG.md index d6275b48..ce1104f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **A multi-select listbox can be driven to a known state.** `listbox_select` + on a multi listbox toggles, which can only ever say "flip this row": calling + it twice silently undoes itself, so an app that selects a row after every add + ends up with the wrong row selected and there was no way to keep an + application's own selection model and the widget's in agreement. + `listbox_set_selected(lb, i, on)` writes one row's state and + `listbox_clear_selection(lb)` clears them all, neither firing `on_select`, + because 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. + `listbox_selected_count(lb)` saves every caller writing the same loop. + +- **`listbox_selection_mode(lb, 1)` gives a multi listbox the selection + behaviour every editor, file manager and mail client has**: a plain click + replaces the selection, cmd/ctrl-click toggles one row, shift-click extends + from the anchor. This could not be built on top of `on_select`, which reports + a row index and no modifier state, so the widget does it: it already owns + `sel_flags` and the hit testing. Mode 0, today's toggle-every-click, stays + the default, because that is what a checklist wants and what `listbox_multi` + has always done. + +- **`modifiers()`** reports the modifier keys held right now as the same + bitmask `window_on_key` uses (1 shift, 2 ctrl, 4 alt, 8 super/command). Real + on all three backends. 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. + + - A `table` announces itself as a table, and its headers as column headers. Before this it announced nothing structural: assistive tech saw an unlabelled stack of buttons above a list. The ROWS were already right, because `table` diff --git a/backend/aether_ui_backend.h b/backend/aether_ui_backend.h index 2d85c5fe..adcb23cd 100644 --- a/backend/aether_ui_backend.h +++ b/backend/aether_ui_backend.h @@ -651,6 +651,13 @@ void aether_ui_shortcut_impl(const char* combo, void* boxed_closure); // would break typing into whatever has focus. mods is a bitmask: // 1 shift, 2 ctrl, 4 alt, 8 super/command. void aether_ui_window_on_key_impl(void* boxed_closure); + +/* The modifier keys held RIGHT NOW, as the public bitmask every other + * modifier-carrying callback uses: 1 shift, 2 ctrl, 4 alt, 8 super/command. + * Meant to be read from inside a click callback, where the platform event is + * still the current one, so a widget can tell a plain click from a + * cmd-click without every click callback growing an argument. */ +int aether_ui_modifiers_impl(void); // Files dropped onto the window, the most common drag-and-drop case by far // and the one an editor or file manager cannot do without. The closure gets // the paths NEWLINE-SEPARATED in one string: the DSL splits that into a list, diff --git a/backend/aether_ui_gtk4.c b/backend/aether_ui_gtk4.c index 089169d8..fb75d9cf 100644 --- a/backend/aether_ui_gtk4.c +++ b/backend/aether_ui_gtk4.c @@ -3593,6 +3593,24 @@ void aether_ui_window_on_file_drop_impl(void* boxed_closure) { drop_attached = 1; } +int aether_ui_modifiers_impl(void) { + /* The seat's keyboard state is the live one, which is what a click + * callback needs; a GdkEvent is not available at that point. */ + GdkDisplay* dpy = gdk_display_get_default(); + if (!dpy) return 0; + GdkSeat* seat = gdk_display_get_default_seat(dpy); + if (!seat) return 0; + GdkDevice* kb = gdk_seat_get_keyboard(seat); + if (!kb) return 0; + GdkModifierType state = gdk_device_get_modifier_state(kb); + int mods = 0; + if (state & GDK_SHIFT_MASK) mods |= 1; + if (state & GDK_CONTROL_MASK) mods |= 2; + if (state & GDK_ALT_MASK) mods |= 4; + if (state & GDK_SUPER_MASK) mods |= 8; + return mods; +} + void aether_ui_window_on_key_impl(void* boxed_closure) { aeui_window_key_closure_add((AeClosure*)boxed_closure); if (!primary_window) return; // attaches when the window appears diff --git a/backend/aether_ui_macos.m b/backend/aether_ui_macos.m index f56a9dad..0e8fa4e0 100644 --- a/backend/aether_ui_macos.m +++ b/backend/aether_ui_macos.m @@ -1305,6 +1305,19 @@ void aether_ui_window_on_key_impl(void* boxed_closure) { window_key_closure_add((AeClosure*)boxed_closure); } +int aether_ui_modifiers_impl(void) { + /* +modifierFlags is the CURRENT keyboard state, not a snapshot from some + * event object, so this answers correctly inside a click callback without + * the event having to be threaded through. */ + NSEventModifierFlags f = [NSEvent modifierFlags]; + int mods = 0; + if (f & NSEventModifierFlagShift) mods |= 1; + if (f & NSEventModifierFlagControl) mods |= 2; + if (f & NSEventModifierFlagOption) mods |= 4; + if (f & NSEventModifierFlagCommand) mods |= 8; + return mods; +} + int aether_ui_window_key_deliver(const char* key_name, int mods) { return window_key_closure_fire(key_name, mods); } diff --git a/backend/aether_ui_win32.c b/backend/aether_ui_win32.c index 535f15e5..85b682a3 100644 --- a/backend/aether_ui_win32.c +++ b/backend/aether_ui_win32.c @@ -2575,6 +2575,17 @@ int aether_ui_window_file_drop_deliver(const char* paths) { return 1; } +int aether_ui_modifiers_impl(void) { + /* GetKeyState reports the state as of the message being processed, which + * inside a click handler is the click itself. */ + int mods = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) mods |= 1; + if (GetKeyState(VK_CONTROL) & 0x8000) mods |= 2; + if (GetKeyState(VK_MENU) & 0x8000) mods |= 4; + if ((GetKeyState(VK_LWIN) & 0x8000) || (GetKeyState(VK_RWIN) & 0x8000)) mods |= 8; + return mods; +} + void aether_ui_window_on_key_impl(void* boxed_closure) { w32_window_key_closure_add((AeClosure*)boxed_closure); } diff --git a/ci.sh b/ci.sh index be32a76c..70d8522e 100755 --- a/ci.sh +++ b/ci.sh @@ -65,7 +65,7 @@ fi # ------------------------------------------------------------------------- # All examples that must compile in Phase 1. -EXAMPLES=(disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas testable calculator context_menu overlay_demo vg_tooltip each_demo rebuild_demo fileicon_demo scrollbg_demo keyhandler_demo imagefill_demo filedrop_demo barfill_demo listbox_demo table_demo transitions_demo split_demo bindings_demo tabs_demo menu rbind_demo typo_demo multiselect_demo dblclick_demo tree_demo tabledeleg_demo weightclamp_demo flexround_demo shortcut_demo polish_demo vlist_demo wshortcut_demo multiwindow_demo timer_demo canvasscroll_demo canvasclip_demo canvasresetclip_demo resizecb_demo quit_demo panelsize_demo insets_demo blitborrow_demo groupalpha_demo hoverpaint_demo gradspread_demo placeholder_demo multikey_demo sheet_demo winmenu_demo reorder_demo overlaytr_demo a11y_demo material_demo themes_demo csssem_demo zen_demo states_demo undo_demo roles_demo command_demo clipboard window_title) +EXAMPLES=(disclosure_demo icons_demo pills_demo textpath_demo counter form picker styled system canvas testable calculator context_menu overlay_demo vg_tooltip each_demo rebuild_demo fileicon_demo scrollbg_demo keyhandler_demo imagefill_demo filedrop_demo barfill_demo listbox_demo table_demo transitions_demo split_demo bindings_demo tabs_demo menu rbind_demo typo_demo multiselect_demo selmode_demo dblclick_demo tree_demo tabledeleg_demo weightclamp_demo flexround_demo shortcut_demo polish_demo vlist_demo wshortcut_demo multiwindow_demo timer_demo canvasscroll_demo canvasclip_demo canvasresetclip_demo resizecb_demo quit_demo panelsize_demo insets_demo blitborrow_demo groupalpha_demo hoverpaint_demo gradspread_demo placeholder_demo multikey_demo sheet_demo winmenu_demo reorder_demo overlaytr_demo a11y_demo material_demo themes_demo csssem_demo zen_demo states_demo undo_demo roles_demo command_demo clipboard window_title) # Examples without a test server — Phase 2 smoke-launches each. # calculator and testable are exercised through their HTTP drivers in # Phases 3-4, so they are not smoke-tested here. @@ -1153,6 +1153,9 @@ if [ "$SPEC_OK" -eq 1 ]; then UI_SPEC=multiselect_demo/spec_multiselect_demo \ run_server_test "$(EX_BIN multiselect_demo)" \ "$SCRIPT_DIR/tests/run_spec.sh" multiselect_demo || FAIL=$((FAIL + 1)) + UI_SPEC=selmode_demo/spec_selmode_demo \ + run_server_test "$(EX_BIN selmode_demo)" \ + "$SCRIPT_DIR/tests/run_spec.sh" selmode_demo || FAIL=$((FAIL + 1)) UI_SPEC=dblclick_demo/spec_dblclick_demo \ run_server_test "$(EX_BIN dblclick_demo)" \ "$SCRIPT_DIR/tests/run_spec.sh" dblclick_demo || FAIL=$((FAIL + 1)) diff --git a/examples/selmode_demo/.build.ae b/examples/selmode_demo/.build.ae new file mode 100644 index 00000000..e273c12b --- /dev/null +++ b/examples/selmode_demo/.build.ae @@ -0,0 +1,18 @@ +// selmode_demo — aether-ui toolkit example, built as its own cached aeb node. +import bldr +import aether +import aether (source, output, no_closure_regen) +import build_support.aetherui (ui_backend) + +main() { + bldr.build() { + root = _get("root") + aether.program() { + source("selmode_demo.ae") + output("selmode_demo") + no_closure_regen() + ui_backend(root) + } + return 0 + } +} diff --git a/examples/selmode_demo/selmode_demo.ae b/examples/selmode_demo/selmode_demo.ae new file mode 100644 index 00000000..f3fd14bb --- /dev/null +++ b/examples/selmode_demo/selmode_demo.ae @@ -0,0 +1,66 @@ +// Aether UI multi-select, driven from the application (#99). +// +// listbox_select on a multi listbox TOGGLES, so it cannot put the widget in a +// known state: calling it twice silently undoes itself, and an app that +// selects a row after every add ends up with the wrong row selected. +// +// listbox_set_selected writes one row's state and listbox_clear_selection +// clears them all, neither firing on_select, so an app can keep its own +// selection model in step with the widget without re-entering its own click +// handler on every sync. +import ui +import ui (window, vstack, hstack, text, btn, divider, + listbox_multi, listbox_update, listbox_set_selected, listbox_clear_selection, + listbox_selection_mode, + enable_test_server) +import std.os (os_getenv) +import std.string +import std.list + +extern malloc(size: int) -> ptr + +struct Item { name: string } + +mk_item(n: int) -> ptr { + it = malloc(16) as *Item + it.name = string.concat("item ", string.from_int(n)) + return it as ptr +} + +main() { + items = list.new() + k = 1 + while k <= 5 { + _ = list.add(items, mk_item(k)) + k = k + 1 + } + + window("selection api demo", 420, 400) { + vstack(8) { + rows = listbox_multi(2) callback |item: ptr, i: int, row: int| { + it = item as *Item + _t = text(row, it.name) + } + listbox_update(rows, items) + listbox_selection_mode(rows, 1) // standard: replace / toggle / extend + divider() + hstack(10) { + // Drive the widget to a KNOWN state, which is what the old API + // could not do. Pressing "set 1 and 3" twice must leave the + // same two rows selected, not undo itself. + _s = btn("set 1 and 3") callback { + listbox_set_selected(rows, 1, 1) + listbox_set_selected(rows, 3, 1) + } + _c = btn("clear") callback { listbox_clear_selection(rows) } + _o = btn("only 2") callback { + listbox_clear_selection(rows) + listbox_set_selected(rows, 2, 1) + } + } + } + if os_getenv("AETHER_UI_TEST_PORT") != null { + enable_test_server(9222) + } + } +} diff --git a/tests/selmode_demo/spec_selmode_demo.ae b/tests/selmode_demo/spec_selmode_demo.ae new file mode 100644 index 00000000..d816a08a --- /dev/null +++ b/tests/selmode_demo/spec_selmode_demo.ae @@ -0,0 +1,64 @@ +// spec_selmode_demo.ae, #99: a multi listbox can be driven to a KNOWN state. +// +// listbox_select toggles, so it could only ever say "flip this row". An app +// that selects a row after every add ends up with the wrong row selected, and +// pressing the same button twice silently undoes itself. These assertions are +// about idempotence and about clearing, which is what "set" and "clear" buy +// over "toggle". +import std.spec +import uidriver +import std.string + +// How many rows currently carry the selection class. +selected_count() -> int { + b = uidriver.get_body("/widgets") + n = 0 + rest = b + while string.contains(rest, "aui-row-selected") == 1 { + n = n + 1 + idx = string.string_index_of(rest, "aui-row-selected") + rest = string.string_substring(rest, idx + 16, string.length(rest)) + } + return n +} + +press(label: string) { + uidriver.post_ok("/widget/${uidriver.widget_id_by_text(label)}/click", "press ${label}") +} + +main() { + fw = spec.init() + uidriver.wait_server() + + spec.describe(fw, "multi-select can be driven to a state (#99)") { + spec.it("nothing is selected to begin with") callback { + spec.assert_eq(selected_count(), 0, "no rows selected at start") + } + + spec.it("set_selected establishes a known selection") callback { + press("set 1 and 3") + ok = uidriver.wait_body_contains("/widgets", "aui-row-selected", 20) + spec.assert_true(ok, "a row is selected") + spec.assert_eq(selected_count(), 2, "rows 1 and 3 are selected") + } + + spec.it("setting the same rows again does not undo itself") callback { + // The whole point: with toggle-only this left ZERO selected. + press("set 1 and 3") + spec.assert_eq(selected_count(), 2, "still exactly the same two rows") + } + + spec.it("clear_selection deselects every row") callback { + press("clear") + spec.assert_eq(selected_count(), 0, "nothing selected after clear") + } + + spec.it("clear then set is how an app says 'only this one'") callback { + press("only 2") + spec.assert_eq(selected_count(), 1, "exactly one row selected") + press("only 2") + spec.assert_eq(selected_count(), 1, "and it is stable under repetition") + } + } + return spec.run_summary(fw) +} diff --git a/ui/module.ae b/ui/module.ae index 8fcb15d4..5e99f87d 100644 --- a/ui/module.ae +++ b/ui/module.ae @@ -82,6 +82,8 @@ exports ( listbox, listbox_multi, listbox_reorderable, listbox_update, listbox_move, listbox_items, listbox_select, listbox_selected, listbox_is_selected, listbox_count, + listbox_set_selected, listbox_clear_selection, listbox_selection_mode, + listbox_selected_count, modifiers, on_row_double_click, on_reorder, on_select, add_css_class, remove_css_class, table_cols, table_col, table_col_delegate, table, table_update, @@ -205,6 +207,7 @@ extern aether_ui_context_menu_item_accel_impl(handle: int, label: string, accel: string, closure: ptr) extern aether_ui_shortcut_impl(combo: string, closure: ptr) extern aether_ui_window_on_key_impl(closure: ptr) +extern aether_ui_modifiers_impl() -> int extern aether_ui_window_on_file_drop_impl(closure: ptr) extern aether_ui_widget_draggable_file_impl(handle: int, path: string) extern aether_ui_shortcut_when_impl(combo: string, closure: ptr, enabled: ptr) @@ -1398,6 +1401,17 @@ shortcut_chord(first: string, second: string, cb: fn) { aether_ui_shortcut_chord_impl(first, second, box_closure(cb)) } +// modifiers(), the modifier keys held RIGHT NOW: 1 shift, 2 ctrl, 4 alt, +// 8 super/command, the same bitmask window_on_key reports. +// +// Meant to be read from inside a click callback, where the platform event is +// still the current one, so a widget or an app can tell a plain click from a +// cmd-click without every click callback growing an argument it would break +// every existing caller to add. Real on all three backends. +modifiers() -> int { + return aether_ui_modifiers_impl() +} + // window_on_key(cb): cb(key_name, mods) for ANY key, not a bound combo. // Every shortcut verb answers "was THIS combo pressed"; type-ahead asks // "what was pressed", which no number of registered shortcuts can express. @@ -3334,7 +3348,9 @@ struct ListBox { sel_flags: ptr, // multi: intarr of 0/1 per row, or null on_dbl: ptr, // boxed |i: int| row double-click callback, or null items: ptr, // reorderable: the OWNED std.list, so move() can reorder - on_reorder: ptr // boxed |from, to| move callback, or null + on_reorder: ptr, // boxed |from, to| move callback, or null + sel_mode: int, // multi: 0 = toggle every click, 1 = standard modifiers + anchor: int // multi, standard mode: where a shift-extend measures from } _listbox_invoke_render(cb: fn, item: ptr, i: int, row: int) { @@ -3345,21 +3361,92 @@ _listbox_invoke_sel(cb: fn, i: int) { cb(i) } +// Write one row's flag and make its class follow. The single place that +// touches both, so a selection can never be half-applied: the model saying +// selected and the row not looking it is exactly the bug an app cannot debug +// from the outside. +_listbox_write_flag(lb: *ListBox, i: int, on: int) { + if lb.sel_flags == null { return } + if lb.rows == null { return } + if i < 0 { return } + intarr_set_raw(lb.sel_flags, i, on) + rowh = intarr_get_raw(lb.rows, i) + if rowh > 0 { + if on == 1 { aether_ui_widget_add_css_class_impl(rowh, "aui-row-selected") } + else { aether_ui_widget_remove_css_class_impl(rowh, "aui-row-selected") } + } +} + +_listbox_clear_all(lb: *ListBox) { + if lb.sel_flags == null { return } + n = listbox_count(lb as ptr) + j = 0 + while j < n { + _listbox_write_flag(lb, j, 0) + j = j + 1 + } +} + +// Standard mode: the behaviour every editor, file manager and mail client +// has. Plain click REPLACES the selection, cmd/ctrl-click toggles one row, +// shift-click extends from the anchor. The widget already owns sel_flags and +// the hit testing, so it can do this itself and an app does not have to +// reimplement it (or be able to: a click callback carrying only a row index +// cannot tell the three apart). +_listbox_apply_standard(lb: *ListBox, i: int) { + m = modifiers() + if (m & 1) == 1 { + // Shift-extend from the anchor. The anchor STAYS put, so dragging the + // shift-click up and down re-extends from the same origin instead of + // walking the selection away one row at a time. + a = lb.anchor + if a < 0 { a = i } + lo = a + hi = i + if lo > hi { + lo = i + hi = a + } + _listbox_clear_all(lb) + j = lo + while j <= hi { + _listbox_write_flag(lb, j, 1) + j = j + 1 + } + lb.selected = i + return + } + if (m & 2) == 2 || (m & 8) == 8 { + // ctrl or cmd: toggle just this row, leave the rest alone. + cur = intarr_get_raw(lb.sel_flags, i) + newv = 1 + if cur == 1 { newv = 0 } + _listbox_write_flag(lb, i, newv) + lb.selected = i + lb.anchor = i + return + } + _listbox_clear_all(lb) + _listbox_write_flag(lb, i, 1) + lb.selected = i + lb.anchor = i +} + _listbox_apply_selection(lb: *ListBox, i: int) { if lb.rows == null { return } if lb.multi == 1 { - // Toggle row i's membership; class follows the flag. if lb.sel_flags == null { return } + if lb.sel_mode == 1 { + _listbox_apply_standard(lb, i) + return + } + // Toggle row i's membership; class follows the flag. cur = intarr_get_raw(lb.sel_flags, i) newv = 1 if cur == 1 { newv = 0 } - intarr_set_raw(lb.sel_flags, i, newv) - rowh = intarr_get_raw(lb.rows, i) - if rowh > 0 { - if newv == 1 { aether_ui_widget_add_css_class_impl(rowh, "aui-row-selected") } - else { aether_ui_widget_remove_css_class_impl(rowh, "aui-row-selected") } - } + _listbox_write_flag(lb, i, newv) lb.selected = i // last-touched, for single-select callers + lb.anchor = i return } if lb.selected >= 0 { @@ -3449,7 +3536,7 @@ listbox_multi(_ctx: ptr, spacing: int, render: fn) -> ptr { } _listbox_make(_ctx: ptr, spacing: int, render: fn, multi: int) -> ptr { - lb = malloc(80) as *ListBox + lb = malloc(96) as *ListBox lb.rows = null lb.selected = 0 - 1 lb.on_sel = null @@ -3459,6 +3546,8 @@ _listbox_make(_ctx: ptr, spacing: int, render: fn, multi: int) -> ptr { lb.on_dbl = null lb.items = null lb.on_reorder = null + lb.sel_mode = 0 + lb.anchor = 0 - 1 lbp = lb as ptr lb.group = each(_ctx, "v", spacing) callback |item: ptr, i: int, parent: int| { row = hstack(parent, 8) @@ -3551,7 +3640,7 @@ _listbox_invoke_reorder(cb: fn, from: int, to: int) { // a drop calls listbox_move(source_index, target_index). The drag gesture is // backend C (GTK4 GtkDragSource/GtkDropTarget); the model reorder is shared. _listbox_make_reorder(_ctx: ptr, spacing: int, render: fn) -> ptr { - lb = malloc(80) as *ListBox + lb = malloc(96) as *ListBox lb.rows = null lb.selected = 0 - 1 lb.on_sel = null @@ -3561,6 +3650,8 @@ _listbox_make_reorder(_ctx: ptr, spacing: int, render: fn) -> ptr { lb.on_dbl = null lb.items = null lb.on_reorder = null + lb.sel_mode = 0 + lb.anchor = 0 - 1 lbp = lb as ptr lb.group = each(_ctx, "v", spacing) callback |item: ptr, i: int, parent: int| { row = hstack(parent, 8) @@ -3625,10 +3716,76 @@ listbox_is_selected(lbp: ptr, i: int) -> int { } // Programmatic selection (same path as a row click, incl. the callback). +// +// On a MULTI listbox in toggle mode this toggles, so it cannot establish a +// known selection: calling it twice silently undoes itself. Use +// listbox_set_selected / listbox_clear_selection to drive a multi listbox to +// a state, and this only where a click is what you mean to simulate. listbox_select(lbp: ptr, i: int) { _listbox_row_clicked(lbp, i) } +// listbox_set_selected(lb, i, on) writes ONE row's selection state, without +// firing on_select. A state write, not a user action: an app keeping its own +// selection model in step with the widget would otherwise re-enter its own +// click handler on every sync and have to guard against the echo. +// +// Multi-select only; a single-select listbox has listbox_select. +listbox_set_selected(lbp: ptr, i: int, on: int) { + lb = lbp as *ListBox + if lb.multi != 1 { return } + v = 0 + if on != 0 { v = 1 } + _listbox_write_flag(lb, i, v) + if v == 1 { + lb.selected = i + lb.anchor = i + } +} + +// listbox_clear_selection(lb) deselects every row. Also forgets the +// shift-extend anchor, so the next shift-click measures from where the user +// clicks rather than from a row that is no longer selected. +listbox_clear_selection(lbp: ptr) { + lb = lbp as *ListBox + if lb.multi != 1 { return } + _listbox_clear_all(lb) + lb.selected = 0 - 1 + lb.anchor = 0 - 1 +} + +// listbox_selection_mode(lb, mode) says how a click changes a MULTI listbox's +// selection. +// +// 0 (default) TOGGLE every click flips that row, nothing else moves. +// 1 STANDARD plain click replaces the selection, cmd/ctrl-click +// toggles one row, shift-click extends from the anchor. +// +// Toggle stays the default because it is what listbox_multi has always done +// and a checklist wants exactly that. Standard is what a scene tree, a file +// list or a mailbox wants, and it cannot be built on top of on_select, which +// reports a row index and no modifier state. +listbox_selection_mode(lbp: ptr, mode: int) { + lb = lbp as *ListBox + lb.sel_mode = mode +} + +// listbox_selected_count(lb) reports how many rows are selected. Saves every caller +// writing the same loop over listbox_is_selected. +listbox_selected_count(lbp: ptr) -> int { + lb = lbp as *ListBox + if lb.multi != 1 { return 0 } + if lb.sel_flags == null { return 0 } + n = listbox_count(lbp) + c = 0 + j = 0 + while j < n { + if intarr_get_raw(lb.sel_flags, j) == 1 { c = c + 1 } + j = j + 1 + } + return c +} + listbox_selected(lbp: ptr) -> int { lb = lbp as *ListBox return lb.selected