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}