Skip to main content

kui_ffi/
menu.rs

1//! Context menus: opening one over a node, and closing it.
2//!
3//! There is no element here and no node to declare. The menu is state the
4//! core holds and draws, so a host asks for one the way it asks for focus
5//! — a call, not a declaration — and hears what was chosen as an ordinary
6//! event on the node it named.
7
8use super::*;
9
10/// One row of a menu ([`kui_open_menu`], [`kui_menu_bar`],
11/// [`kui_select`]), read while the call runs.
12///
13/// `label` may be empty for a standard `role`, which then reads the way
14/// the platform words it. `id` is what the row posts when chosen; NULL
15/// posts the row's text. `accel` is drawn right-aligned and bound to
16/// nothing — the shortcut is the host's, and this only says which one.
17#[repr(C)]
18#[derive(Clone, Copy)]
19pub struct KuiMenuItem {
20    pub label: KuiStr,
21    /// A `KUI_MENU_*` role.
22    pub role: u32,
23    /// Zero disables the row: drawn dimmed, not focusable, not choosable.
24    /// A row present-but-dead rather than absent, so a menu's shape does
25    /// not change under the pointer.
26    pub enabled: u32,
27    pub id: *const KuiValue,
28    pub accel: KuiStr,
29    /// Non-zero draws a checkmark beside the row (and sets the platform's
30    /// own check state where the host renders the menu): a setting the row
31    /// *is*, not a command it runs.
32    pub checked: u32,
33    /// The rows this row opens, `submenu_count` of them in the same
34    /// struct (backlog F128); null / 0 is an ordinary row. ABI 26.
35    pub submenu: *const KuiMenuItem,
36    pub submenu_count: usize,
37}
38
39/// How deep a C menu nests before the read refuses it: far past any menu
40/// a person can use, and the end of a row that names itself as its own
41/// submenu, which would otherwise recurse until the stack ran out.
42const MAX_MENU_DEPTH: usize = 32;
43
44impl KuiMenuItem {
45    /// Reads one item and the submenus under it. `None` for a role this
46    /// build does not know anywhere in it, which is a host built against a
47    /// newer header, and for a menu nested past [`MAX_MENU_DEPTH`].
48    pub(crate) fn to_core(self) -> Option<kui_core::MenuItem> {
49        self.to_core_at(0)
50    }
51
52    fn to_core_at(self, depth: usize) -> Option<kui_core::MenuItem> {
53        let role = *kui_core::MenuRole::ALL.get(self.role as usize)?;
54        let submenu = if self.submenu.is_null() || self.submenu_count == 0 {
55            Vec::new()
56        } else if depth >= MAX_MENU_DEPTH {
57            return None;
58        } else {
59            // SAFETY: the header's contract, `submenu_count` rows behind
60            // `submenu`, read while the call runs.
61            let rows = unsafe { std::slice::from_raw_parts(self.submenu, self.submenu_count) };
62            rows.iter()
63                .map(|r| r.to_core_at(depth + 1))
64                .collect::<Option<Vec<_>>>()?
65        };
66        Some(kui_core::MenuItem {
67            label: kstr(self.label).into_owned(),
68            role,
69            enabled: self.enabled != 0,
70            checked: self.checked != 0,
71            id: unsafe { self.id.as_ref() }.map(|v| v.0.clone()),
72            accel: opt_str(self.accel).map(|s| s.into_owned()),
73            submenu,
74        })
75    }
76}
77
78/// A row's path as the header spells it — `depth` indices behind `path`,
79/// outermost first — or `None` for a NULL `path` with a depth to read.
80///
81/// # Safety
82/// `path`, when not null, points at `depth` readable `size_t`s.
83unsafe fn path_of<'a>(path: *const usize, depth: usize) -> Option<&'a [usize]> {
84    if depth == 0 {
85        return Some(&[]);
86    }
87    if path.is_null() {
88        return None;
89    }
90    // SAFETY: the caller's promise.
91    Some(unsafe { std::slice::from_raw_parts(path, depth) })
92}
93
94/// Opens a context menu at `(x, y)` (logical viewport px) over `key`, with
95/// `count` items read from `items`. The next frame draws it.
96///
97/// Choosing a row posts `{kind:"menu", role, item}` on `key` and closes
98/// the menu; a press outside it or Escape closes it and posts nothing. The
99/// standard roles the core can carry out it carries out — the clipboard
100/// three come back through `kui_take_menu_actions` — and a `KUI_MENU_CUSTOM`
101/// row is the host's to act on.
102///
103/// A key of 0, no items, or an item with an unknown role: nothing opens
104/// and this returns false.
105#[unsafe(no_mangle)]
106pub extern "C" fn kui_open_menu(
107    ptr: *mut KuiCtx,
108    key: u64,
109    x: f32,
110    y: f32,
111    items: *const KuiMenuItem,
112    count: usize,
113) -> bool {
114    guard(false, || {
115        let Some(c) = (unsafe { ctx(ptr) }) else {
116            return false;
117        };
118        if key == 0 || items.is_null() || count == 0 {
119            return false;
120        }
121        let rows = unsafe { std::slice::from_raw_parts(items, count) };
122        let mut parsed = Vec::with_capacity(count);
123        for row in rows {
124            match row.to_core() {
125                Some(item) => parsed.push(item),
126                None => return false,
127            }
128        }
129        c.core().open_menu(kui_core::Menu::new(
130            Key(key),
131            kui_core::Vec2::new(x, y),
132            parsed,
133        ));
134        true
135    })
136}
137
138/// Tells the core this host can show the platform's definition panel
139/// (macOS's Look Up). The standard Look Up row is then offered where it
140/// means something, and a force click over text asks for one; without
141/// this the core neither offers nor asks, because a row that does nothing
142/// is worse than a row that is not there.
143#[unsafe(no_mangle)]
144pub extern "C" fn kui_set_lookup_available(ptr: *mut KuiCtx, on: bool) {
145    guard((), || {
146        if let Some(c) = unsafe { ctx(ptr) } {
147            c.core().set_lookup_available(on);
148        }
149    });
150}
151
152/// Tells the core that this host shows menus itself — the platform's own,
153/// however it draws them. The core then keeps the open menu as state and
154/// draws none of it: read it with `kui_menu_items`, show it, and report
155/// back with `kui_activate_menu_item` or `kui_close_menu`.
156#[unsafe(no_mangle)]
157pub extern "C" fn kui_set_native_menus(ptr: *mut KuiCtx, on: bool) {
158    guard((), || {
159        if let Some(c) = unsafe { ctx(ptr) } {
160            c.core().set_native_menus(on);
161        }
162    });
163}
164
165/// Reports that the host's menu chose row `index`: the same path a press
166/// on the drawn menu's row takes. An index past the end closes the menu
167/// and posts nothing. Returns false when nothing was taken: no menu was
168/// open, or the row cannot be chosen — disabled, or a separator — in
169/// which case the menu stays open and nothing is posted.
170#[unsafe(no_mangle)]
171pub extern "C" fn kui_activate_menu_item(ptr: *mut KuiCtx, index: usize) -> bool {
172    guard(false, || {
173        let Some(c) = (unsafe { ctx(ptr) }) else {
174            return false;
175        };
176        let Some(events) = c.core().activate_menu_item(index) else {
177            return false;
178        };
179        c.absorb(events);
180        true
181    })
182}
183
184/// Asks for the selection as text.
185///
186/// `KUI_COPY_READY` writes the selection into `out` — borrowed until the
187/// next call on this context. `KUI_COPY_ASKED` means the selection reaches
188/// rows a virtual list never built: a `{kind:"selectionrange", from, to}`
189/// event is waiting in the queue, the rows behind that gap are the host's,
190/// and the host answers with `kui_answer_selection_range`, whose text then
191/// arrives as a `KUI_MENU_ACTION_SET_CLIPBOARD`. `KUI_COPY_NOTHING` is
192/// nothing selected.
193#[unsafe(no_mangle)]
194pub extern "C" fn kui_request_copy(ptr: *mut KuiCtx, out: *mut KuiStr) -> u32 {
195    guard(2, || {
196        let Some(c) = (unsafe { ctx(ptr) }) else {
197            return 2;
198        };
199        match c.core().request_copy() {
200            kui_core::CopyRequest::Ready(text) => {
201                c.copy_text = text;
202                if !out.is_null() {
203                    unsafe {
204                        out.write(KuiStr {
205                            ptr: c.copy_text.as_ptr(),
206                            len: c.copy_text.len(),
207                        })
208                    };
209                }
210                KUI_COPY_READY
211            }
212            kui_core::CopyRequest::Asked => KUI_COPY_ASKED,
213            kui_core::CopyRequest::Nothing => KUI_COPY_NOTHING,
214        }
215    })
216}
217
218/// Answers a `selectionrange` ask with the text for the range it named,
219/// whole. False when nothing asked — a late answer cannot overwrite what
220/// has been copied since.
221#[unsafe(no_mangle)]
222pub extern "C" fn kui_answer_selection_range(ptr: *mut KuiCtx, text: KuiStr) -> bool {
223    guard(false, || {
224        let Some(c) = (unsafe { ctx(ptr) }) else {
225            return false;
226        };
227        let text = kstr(text).into_owned();
228        c.core().answer_selection_range(&text)
229    })
230}
231
232/// Puts `text` on the system clipboard, as a `KUI_MENU_ACTION_SET_CLIPBOARD`
233/// the host drains: the action a menu's Copy queues, callable from an
234/// `on_key` sink that hears the raw `Ctrl-c`. `html` is a second flavour
235/// beside the text,
236/// never in place of it; an empty `html` is none. Under `kui_run` the
237/// runner applies it after every input and every frame.
238#[unsafe(no_mangle)]
239pub extern "C" fn kui_set_clipboard(ptr: *mut KuiCtx, text: KuiStr, html: KuiStr) {
240    guard((), || {
241        if let Some(c) = unsafe { ctx(ptr) } {
242            let text = kstr(text).into_owned();
243            let html = kstr(html).into_owned();
244            c.core()
245                .set_clipboard(text, (!html.is_empty()).then_some(html));
246        }
247    });
248}
249
250/// Puts a secret on the system clipboard, as a
251/// `KUI_MENU_ACTION_SET_CLIPBOARD_SECRET` the host drains and writes
252/// marked concealed and transient, the way a password manager does:
253/// `org.nspasteboard.ConcealedType` and `TransientType`
254/// on macOS, the exclusion formats on Windows — so no clipboard manager
255/// shows or keeps it. Under `kui_run` the runner writes it.
256#[unsafe(no_mangle)]
257pub extern "C" fn kui_set_clipboard_secret(ptr: *mut KuiCtx, text: KuiStr) {
258    guard((), || {
259        if let Some(c) = unsafe { ctx(ptr) } {
260            let text = kstr(text).into_owned();
261            c.core().set_clipboard_secret(text);
262        }
263    });
264}
265
266/// Asks for what is on the clipboard, as a `KUI_MENU_ACTION_PASTE` the
267/// host drains and answers with [`kui_input_paste`] (the text and the
268/// pasteboard's `KUI_PASTE_*` markers) or [`kui_input_commit`]: a focused
269/// editor takes
270/// the text as typing, a focused `on_key` sink hears it as
271/// `{kind:"text", text, tag}` — so an app that owns its text inserts a
272/// paste the way it inserts a committed IME string, and the clipboard is
273/// read on the host's side, where the permission lives. Under `kui_run`
274/// the runner does both halves.
275#[unsafe(no_mangle)]
276pub extern "C" fn kui_request_paste(ptr: *mut KuiCtx) {
277    guard((), || {
278        if let Some(c) = unsafe { ctx(ptr) } {
279            c.core().request_paste();
280        }
281    });
282}
283
284/// Whether a paste asked for is still unanswered: `kui_request_paste`
285/// queues one ask at a time, and the `kui_input_commit` that answers it
286/// (an empty one for an empty clipboard) is what lets the next through.
287#[unsafe(no_mangle)]
288pub extern "C" fn kui_awaiting_paste(ptr: *mut KuiCtx) -> bool {
289    guard(false, || {
290        unsafe { ctx(ptr) }.is_some_and(|c| c.core().awaiting_paste())
291    })
292}
293
294/// Closes whatever menu is open; true when there was one.
295#[unsafe(no_mangle)]
296pub extern "C" fn kui_close_menu(ptr: *mut KuiCtx) -> bool {
297    guard(false, || {
298        unsafe { ctx(ptr) }.is_some_and(|c| c.core().close_menu())
299    })
300}
301
302/// Drains what choosing a menu row left for the host: the clipboard, which
303/// is the host's in this library. `KUI_MENU_ACTION_SET_CLIPBOARD` carries
304/// the text to put there — the core worked out *what*, which is the half
305/// only it can do — and `KUI_MENU_ACTION_PASTE` asks for what is there,
306/// which a host delivers back with `kui_input_commit` (an editor takes it
307/// as typing; a sink hears it as `{kind:"text"}`).
308///
309/// Writes one action into `out` and returns true; false when the queue is
310/// empty, and false with the action left queued when `out` is NULL or its
311/// `size` too small. `text` is borrowed until the next call on this context.
312#[unsafe(no_mangle)]
313pub extern "C" fn kui_take_menu_action(ptr: *mut KuiCtx, out: *mut KuiMenuAction) -> bool {
314    guard(false, || {
315        let Some(c) = (unsafe { ctx(ptr) }) else {
316            return false;
317        };
318        if c.menu_actions.is_empty() {
319            c.menu_actions = c.core().take_menu_actions().into();
320        }
321        // Checked before the action leaves the queue, as kui_poll_event
322        // does: an `out` this library cannot write keeps it for the next
323        // call rather than dropping a clipboard write.
324        if !out_accepts(out) {
325            return false;
326        }
327        let Some(action) = c.menu_actions.pop_front() else {
328            return false;
329        };
330        let mut at = kui_core::Vec2::new(0.0, 0.0);
331        let (kind, text, html) = match action {
332            kui_core::MenuAction::SetClipboard { text, html } => {
333                (KUI_MENU_ACTION_SET_CLIPBOARD, text, html)
334            }
335            kui_core::MenuAction::SetClipboardSecret { text } => {
336                (KUI_MENU_ACTION_SET_CLIPBOARD_SECRET, text, None)
337            }
338            kui_core::MenuAction::Paste => (KUI_MENU_ACTION_PASTE, String::new(), None),
339            // The core only asks for a panel a host said it can show
340            // (`kui_set_lookup_available`), so this arrives exactly where
341            // a host is ready for it.
342            kui_core::MenuAction::LookUp { text, at: point } => {
343                at = point;
344                (KUI_MENU_ACTION_LOOK_UP, text, None)
345            }
346        };
347        c.menu_text = text;
348        c.menu_html = html.unwrap_or_default();
349        let written = KuiMenuAction {
350            size: std::mem::size_of::<KuiMenuAction>() as u32,
351            kind,
352            text: KuiStr {
353                ptr: c.menu_text.as_ptr(),
354                len: c.menu_text.len(),
355            },
356            html: KuiStr {
357                ptr: c.menu_html.as_ptr(),
358                len: c.menu_html.len(),
359            },
360            x: at.x,
361            y: at.y,
362        };
363        write_out(out, written)
364    })
365}
366
367// -- The application menu bar -----------------------------------------------
368
369/// One menu of the application menu bar ([`kui_menu_bar`]), read while
370/// the call runs; the core copies what it needs.
371///
372/// `items` is `count` rows in the same `KuiMenuItem` a context menu takes,
373/// which is the point: an Edit menu's Copy is the same row the right-click
374/// Copy is, and the core performs it the same way.
375#[repr(C)]
376#[derive(Clone, Copy)]
377pub struct KuiMenu {
378    pub label: KuiStr,
379    pub items: *const KuiMenuItem,
380    pub count: usize,
381    /// Zero disables the whole menu: dimmed, and it opens nothing.
382    pub enabled: u32,
383}
384
385/// The application menu for this frame: `count` menus read from `menus`,
386/// in bar order, declared and, where the platform has no menu bar of its
387/// own, drawn into the frame right here as a row of titles that drop
388/// their menus.
389///
390/// One call and not two, because what the menu is and where its strip goes
391/// are one decision. Where the platform owns the bar
392/// (`kui_set_native_menu_bar`) nothing is drawn and the declaration still
393/// stands, so a host calls this unconditionally and is portable; a host
394/// with a bar of its own reads the declaration back with
395/// `kui_menu_bar_menu_count` / `_menu` / `_item` and reports a choice with
396/// `kui_activate_menu_bar_item`.
397///
398/// Sticky and diffed, like `kui_window_title`: a frame that does not call
399/// this leaves the last declaration in force, the same declaration again
400/// changes nothing, and `count == 0` takes the menu away. Choosing an item
401/// posts `{kind:"menu", role, item}` — the same event the context menu
402/// posts — on the bar's own node, or on the root where the platform drew
403/// it. Returns false, declaring and drawing nothing, for an item with a
404/// role this build does not know. Call between `kui_frame_begin` and
405/// `kui_frame_finish`.
406#[unsafe(no_mangle)]
407pub extern "C" fn kui_menu_bar(ptr: *mut KuiCtx, menus: *const KuiMenu, count: usize) -> bool {
408    guard(false, || {
409        let Some(c) = (unsafe { ctx(ptr) }) else {
410            return false;
411        };
412        let mut out = Vec::with_capacity(count);
413        if !menus.is_null() {
414            for menu in unsafe { std::slice::from_raw_parts(menus, count) } {
415                let mut items = Vec::with_capacity(menu.count);
416                if !menu.items.is_null() {
417                    for row in unsafe { std::slice::from_raw_parts(menu.items, menu.count) } {
418                        match row.to_core() {
419                            Some(item) => items.push(item),
420                            None => return false,
421                        }
422                    }
423                }
424                out.push(kui_core::BarMenu {
425                    label: kstr(menu.label).into_owned(),
426                    items,
427                    enabled: menu.enabled != 0,
428                });
429            }
430        }
431        let mut ui = kui_core::Ui::wrap(c.core());
432        kui_core::widgets::menu_bar(&mut ui, kui_core::MenuBar::new(out));
433        true
434    })
435}
436
437/// Tells the core that the platform owns the menu bar, so `kui_menu_bar`
438/// draws nothing and the host is the one that hands the declaration over
439/// (`kui_menu_bar_menu_count` / `kui_menu_bar_item` read it back) and
440/// reports what was chosen with `kui_activate_menu_bar_item`.
441///
442/// Off by default: a host that says nothing draws its own bar.
443#[unsafe(no_mangle)]
444pub extern "C" fn kui_set_native_menu_bar(ptr: *mut KuiCtx, on: bool) {
445    guard((), || {
446        if let Some(c) = unsafe { ctx(ptr) } {
447            c.core().set_native_menu_bar(on);
448        }
449    });
450}
451
452/// How many menus the declaration in force has, and a revision that
453/// changes only when the declaration does — a host with a native bar
454/// keeps the last number it built and rebuilds nothing until it moves.
455/// Writes the revision into `revision` when it is not NULL.
456#[unsafe(no_mangle)]
457pub extern "C" fn kui_menu_bar_menu_count(ptr: *mut KuiCtx, revision: *mut u64) -> usize {
458    guard(0, || {
459        let Some(c) = (unsafe { ctx(ptr) }) else {
460            return 0;
461        };
462        if !revision.is_null() {
463            unsafe { revision.write(c.core().menu_bar_revision()) };
464        }
465        c.core().menu_bar().map_or(0, |b| b.menus.len())
466    })
467}
468
469/// Reads one menu of the declaration back: its title into `label` and how
470/// many rows it has. Both borrowed until the next call on this context.
471/// Returns false for a menu past the end.
472#[unsafe(no_mangle)]
473pub extern "C" fn kui_menu_bar_menu(
474    ptr: *mut KuiCtx,
475    menu: usize,
476    label: *mut KuiStr,
477    enabled: *mut bool,
478) -> usize {
479    guard(0, || {
480        let Some(c) = (unsafe { ctx(ptr) }) else {
481            return 0;
482        };
483        let Some(m) = c.core().menu_bar().and_then(|b| b.menus.get(menu)) else {
484            return 0;
485        };
486        let (text, on, count) = (m.label.clone(), m.enabled, m.items.len());
487        c.row_text = text;
488        if !label.is_null() {
489            unsafe {
490                label.write(KuiStr {
491                    ptr: c.row_text.as_ptr(),
492                    len: c.row_text.len(),
493                })
494            };
495        }
496        if !enabled.is_null() {
497            unsafe { enabled.write(on) };
498        }
499        count
500    })
501}
502
503/// Reads one row of one menu: its text into `label`, its accelerator into
504/// `accel` (empty when it has none), and its role and flags through the
505/// out pointers. Both strings are borrowed until the next call on this
506/// context. False for a row that is not there.
507#[unsafe(no_mangle)]
508pub extern "C" fn kui_menu_bar_item(
509    ptr: *mut KuiCtx,
510    menu: usize,
511    item: usize,
512    label: *mut KuiStr,
513    accel: *mut KuiStr,
514    role: *mut u32,
515    flags: *mut u32,
516) -> bool {
517    guard(false, || {
518        let Some(c) = (unsafe { ctx(ptr) }) else {
519            return false;
520        };
521        let Some(row) = c.core().menu_bar().and_then(|b| b.item(menu, item)) else {
522            return false;
523        };
524        let row = row.clone();
525        write_row(c, &row, label, accel, role, flags);
526        true
527    })
528}
529
530/// One row of either menu, spelled the one way a host reads a row: what
531/// the drawn menu would show (`MenuItem::text`, `MenuItem::accel_text` —
532/// the role's default where the row declared none), the `KUI_MENU_*` role
533/// and the `KUI_MENU_ITEM_*` flags. Shared by `kui_menu_bar_item` and
534/// `kui_menu_item` so the bar and the context menu cannot read one item
535/// two ways. Any out pointer may be NULL; the strings are borrowed until
536/// the next call on this context.
537fn write_row(
538    c: &mut KuiCtx,
539    row: &kui_core::MenuItem,
540    label: *mut KuiStr,
541    accel: *mut KuiStr,
542    role: *mut u32,
543    flags: *mut u32,
544) {
545    c.row_text = row.text().to_string();
546    c.menu_accel = row.accel_text().unwrap_or_default().to_string();
547    if !label.is_null() {
548        unsafe {
549            label.write(KuiStr {
550                ptr: c.row_text.as_ptr(),
551                len: c.row_text.len(),
552            })
553        };
554    }
555    if !accel.is_null() {
556        unsafe {
557            accel.write(KuiStr {
558                ptr: c.menu_accel.as_ptr(),
559                len: c.menu_accel.len(),
560            })
561        };
562    }
563    if !role.is_null() {
564        unsafe { role.write(role_code(row.role)) };
565    }
566    if !flags.is_null() {
567        let mut bits = 0;
568        if row.enabled {
569            bits |= KUI_MENU_ITEM_ENABLED;
570        }
571        if row.checked {
572            bits |= KUI_MENU_ITEM_CHECKED;
573        }
574        if row.has_submenu() {
575            bits |= KUI_MENU_ITEM_SUBMENU;
576        }
577        unsafe { flags.write(bits) };
578    }
579}
580
581/// The menu this window has open, for a host that said it shows menus
582/// itself (`kui_set_native_menus`): how many rows it has, writing the node
583/// it is about into `target` and where it opened (logical viewport px)
584/// into `x` / `y` — any of the three may be NULL. Zero when none is open,
585/// which is unambiguous because a menu never opens with no rows. What
586/// Node's `menu()` and `Core::menu` answer, so a C host is not the one
587/// binding told to read what is open and given nothing to read it with.
588#[unsafe(no_mangle)]
589pub extern "C" fn kui_menu_item_count(
590    ptr: *mut KuiCtx,
591    target: *mut u64,
592    x: *mut f32,
593    y: *mut f32,
594) -> usize {
595    guard(0, || {
596        let Some(c) = (unsafe { ctx(ptr) }) else {
597            return 0;
598        };
599        let Some(menu) = c.core().menu() else {
600            return 0;
601        };
602        if !target.is_null() {
603            unsafe { target.write(menu.target.0) };
604        }
605        if !x.is_null() {
606            unsafe { x.write(menu.at.x) };
607        }
608        if !y.is_null() {
609            unsafe { y.write(menu.at.y) };
610        }
611        menu.items.len()
612    })
613}
614
615/// Reads row `item` of the open menu, spelled exactly as `kui_menu_bar_item`
616/// spells a bar's row. False for a row that is not there, including when
617/// no menu is open. Answer with `kui_activate_menu_item` or
618/// `kui_close_menu`.
619#[unsafe(no_mangle)]
620pub extern "C" fn kui_menu_item(
621    ptr: *mut KuiCtx,
622    item: usize,
623    label: *mut KuiStr,
624    accel: *mut KuiStr,
625    role: *mut u32,
626    flags: *mut u32,
627) -> bool {
628    guard(false, || {
629        let Some(c) = (unsafe { ctx(ptr) }) else {
630            return false;
631        };
632        let Some(row) = c.core().menu().and_then(|m| m.items.get(item)) else {
633            return false;
634        };
635        let row = row.clone();
636        write_row(c, &row, label, accel, role, flags);
637        true
638    })
639}
640
641/// Reports that the platform's menu bar chose row `item` of menu `menu`:
642/// the same path a press on the drawn bar's row takes. Out of range does
643/// nothing. Returns whether an item was performed.
644#[unsafe(no_mangle)]
645pub extern "C" fn kui_activate_menu_bar_item(ptr: *mut KuiCtx, menu: usize, item: usize) -> bool {
646    guard(false, || {
647        let Some(c) = (unsafe { ctx(ptr) }) else {
648            return false;
649        };
650        let events = c.core().activate_menu_bar_item(menu, item);
651        let any = !events.is_empty();
652        c.absorb(events);
653        any
654    })
655}
656
657/// How many rows the open menu's row at `path` opens (`depth` indices,
658/// outermost first): 0 for a row with no submenu or one not there, and at
659/// a depth of 0 the menu's own rows, as `kui_menu_item_count` counts them.
660#[unsafe(no_mangle)]
661pub extern "C" fn kui_menu_submenu_count(
662    ptr: *mut KuiCtx,
663    path: *const usize,
664    depth: usize,
665) -> usize {
666    guard(0, || {
667        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
668            return 0;
669        };
670        let Some(menu) = c.core().menu() else {
671            return 0;
672        };
673        if path.is_empty() {
674            return menu.items.len();
675        }
676        kui_core::MenuItem::at_path(&menu.items, path).map_or(0, |r| r.submenu.len())
677    })
678}
679
680/// Reads the open menu's row at `path`, spelled as `kui_menu_item` spells
681/// one; `KUI_MENU_ITEM_SUBMENU` in its flags when it opens rows of its own.
682/// False for a row that is not there, and for an empty path.
683#[unsafe(no_mangle)]
684pub extern "C" fn kui_menu_item_path(
685    ptr: *mut KuiCtx,
686    path: *const usize,
687    depth: usize,
688    label: *mut KuiStr,
689    accel: *mut KuiStr,
690    role: *mut u32,
691    flags: *mut u32,
692) -> bool {
693    guard(false, || {
694        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
695            return false;
696        };
697        let Some(row) = c
698            .core()
699            .menu()
700            .and_then(|m| kui_core::MenuItem::at_path(&m.items, path))
701            .cloned()
702        else {
703            return false;
704        };
705        write_row(c, &row, label, accel, role, flags);
706        true
707    })
708}
709
710/// Reports that the host's own menu chose the row at `path`, inside its
711/// submenus: `Core::activate_menu_path`. False when nothing was taken — no
712/// menu open, an empty path, a row not there, or one that cannot be
713/// chosen (dead, a separator, or one that opens a submenu, which the host
714/// opens), the menu then left open and nothing posted.
715#[unsafe(no_mangle)]
716pub extern "C" fn kui_activate_menu_path(
717    ptr: *mut KuiCtx,
718    path: *const usize,
719    depth: usize,
720) -> bool {
721    guard(false, || {
722        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
723            return false;
724        };
725        if path.is_empty() {
726            return false;
727        }
728        let Some(events) = c.core().activate_menu_path(path) else {
729            return false;
730        };
731        c.absorb(events);
732        true
733    })
734}
735
736/// How many rows the bar's row at `path` in menu `menu` opens: 0 for a row
737/// with no submenu or one not there, and at a depth of 0 the menu's own
738/// rows, as `kui_menu_bar_menu` counts them.
739#[unsafe(no_mangle)]
740pub extern "C" fn kui_menu_bar_submenu_count(
741    ptr: *mut KuiCtx,
742    menu: usize,
743    path: *const usize,
744    depth: usize,
745) -> usize {
746    guard(0, || {
747        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
748            return 0;
749        };
750        let Some(bar) = c.core().menu_bar() else {
751            return 0;
752        };
753        if path.is_empty() {
754            return bar.menus.get(menu).map_or(0, |m| m.items.len());
755        }
756        bar.item_at(menu, path).map_or(0, |r| r.submenu.len())
757    })
758}
759
760/// Reads the bar's row at `path` in menu `menu`, spelled as
761/// `kui_menu_bar_item` spells one. False for a row that is not there, and
762/// for an empty path.
763#[unsafe(no_mangle)]
764pub extern "C" fn kui_menu_bar_item_path(
765    ptr: *mut KuiCtx,
766    menu: usize,
767    path: *const usize,
768    depth: usize,
769    label: *mut KuiStr,
770    accel: *mut KuiStr,
771    role: *mut u32,
772    flags: *mut u32,
773) -> bool {
774    guard(false, || {
775        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
776            return false;
777        };
778        let Some(row) = c
779            .core()
780            .menu_bar()
781            .and_then(|b| b.item_at(menu, path))
782            .cloned()
783        else {
784            return false;
785        };
786        write_row(c, &row, label, accel, role, flags);
787        true
788    })
789}
790
791/// Reports that the platform's menu bar chose the row at `path` in menu
792/// `menu`, inside its submenus: `Core::activate_menu_bar_path`. Returns
793/// whether a row was performed; a row that opens a submenu, a dead one and
794/// one not there are not.
795#[unsafe(no_mangle)]
796pub extern "C" fn kui_activate_menu_bar_path(
797    ptr: *mut KuiCtx,
798    menu: usize,
799    path: *const usize,
800    depth: usize,
801) -> bool {
802    guard(false, || {
803        let (Some(c), Some(path)) = (unsafe { ctx(ptr) }, unsafe { path_of(path, depth) }) else {
804            return false;
805        };
806        if path.is_empty() {
807            return false;
808        }
809        let events = c.core().activate_menu_bar_path(menu, path);
810        let any = !events.is_empty();
811        c.absorb(events);
812        any
813    })
814}
815
816/// The `KUI_MENU_*` code for a role, the inverse of `KuiMenuItem::to_core`.
817fn role_code(role: kui_core::MenuRole) -> u32 {
818    kui_core::MenuRole::ALL
819        .iter()
820        .position(|r| *r == role)
821        .expect("every role is in MenuRole::ALL") as u32
822}