Skip to main content

ui/
popup.rs

1// popup -- popups, flyouts, menus, menu bar, command table and accelerators, drop-down / split buttons, combo boxes, tooltips
2//                                                                  [src/ui/ui_popup.s]
3//
4// Popups (a stack of up to 6 levels per window, drawn on L_POPUP):
5//   popup_open(id)                      open popup `id` (inside a flyout: as its child)
6//   popup_close()                       close every popup; the focus returns to the opener
7//   popup_is_open(id) -> bool   popup_count() -> n
8//   flyout_begin(id, FL_*) -> bool      draw popup `id` when it is open: a flyout surface
9//                                       anchored to the last widget, content box BOX_V|PAD_4
10//   flyout_end()                        always paired with flyout_begin
11//   A press outside every popup closes them all and is consumed; Esc closes the top one;
12//   Up / Down move the focus inside the top one.
13//   popup_ext(i) / popup_rect(i) -> rect (U3, §6: popups that leave their window; the area
14//   they may use is the monitor's work area -- they flip above / to the left where it ends)
15// Commands (the table AppDesc::cmds, Command records):
16//   cmd_find(id) -> &Command   cmd_state(id, CF_*)   cmd_flags(id) -> CF_*
17//   cmd_label(id) -> label without '&'   accel_text(id) -> "Strg+Umschalt+S"
18//   accel_order_set(1): "Strg+Alt+Umschalt+S" (ui_accel_order)
19//   cmd_button(id, ST_*) -> EV          button from the table (icon, label, tooltip with
20//                                       the accelerator, disabled / checked); queues it
21// Menus (Mi records; Mi::List rows come from menu_list(base, rows), row k = id base + k; a
22// checked row with an icon gives the flyout a check column and an icon column):
23//   menubar(None | Some(bar)) -> EV_OPEN   WinUI MenuBar (None = AppDesc::menubar): items,
24//                                       flyouts, submenus, keys (Alt / F10, Alt+mnemonic,
25//                                       arrows, Enter, Esc)
26//   menu_popup(menu)                    open a menu at the pointer (drawn by the runtime)
27//   context_menu(menu) -> EV_OPEN       post-modifier: right click / menu key / Shift+F10 on
28//                                       the last widget opens `menu`
29//   context_target() -> id              the widget whose context menu is open
30//   menu_item(id, label) -> EV          row inside a flyout (next_icon, next_flags
31//                                       ST_DISABLED / ST_CHECKED); activation closes popups
32//   menu_item_keys(id, label, keys) -> EV   the same with the text of its keys at the row's
33//                                       end, for keys the app's own widgets handle (IDE1)
34//   key_text(vk, mods) -> "Strg+Umschalt+S"   a key as the accelerator texts write it
35//   menu_cmd(cmd) -> EV   menu_sep()    a row for a command (queues it), a separator
36//   menu_list(base, rows)               the rows of the Mi::List with that base (the asm's
37//                                       app-owned STRLIST buffer)
38// Drop-down controls:
39//   dropdown_button(id, label) -> EV    button with a chevron; opens popup `id` (EV_OPEN)
40//   split_button(id, label) -> EV       EV_ACT from the main part; the chevron part
41//                                       (id | 0x80000000) opens popup `id`
42//   combobox(id, &mut sel, list) -> EV  EV_ACT when sel changed, EV_OPEN while open
43//   (drop-downs open with Alt+Down, and with F4 unless the command table has an F4 accelerator)
44// Tooltips:
45//   tooltip(s)                          post-modifier: tooltip for the last widget after
46//                                       500 ms of hover (timer UIT_TOOLTIP wakes the loop)
47//   tooltip_part(s, part)               the same for one part of the last widget (an item of
48//                                       a canvas, part != 0): the delay starts again when the
49//                                       part changes, a press hides it until another part
50// Runtime: accel_dispatch() at the start of uiw_pass_end: runtime-drawn menus, stale
51// popups, command accelerators.
52//
53// Ids: a menu's popup id is a hash of the menu's address (menu_pid), the menu bar's item
54// ids a hash of the bar's address -- like the asm, whose dumps therefore show other
55// numbers for them.
56
57use alloc::collections::BTreeMap;
58use alloc::string::String;
59use alloc::vec::Vec;
60
61use crate::consts::*;
62use crate::keys::*;
63use crate::{Sp, Ui};
64use plat::{PA_ABOVE, PA_BELOW, PA_FREE, PA_POINT, PA_SIDE};
65
66// popup stack: fixed (menus and submenus inside each other; the platform shows a few popup
67// windows, plat::PPW_MAX); a popup opened deeper replaces the deepest one (limit_hit)
68const POPUP_MAX: usize = 6;
69pub(crate) const PPF_FOCUS: u32 = 1; // focus the first focusable row once drawn
70const PPF_AUTO: u32 = 2; // drawn by the runtime (menu_popup)
71const PPF_POINT: u32 = 4; // anchored to a fixed point
72const PPF_QUIET: u32 = 8; // focusid without the focus visual
73const PPF_TWOCOL: u32 = 16; // a checked row with an icon: check column + icon column (U1)
74const PPF_SCROLLED: u32 = 32; // (U3) a scrolling combo list was scrolled to its selection
75
76const FLN_MAX: usize = 8; // flyouts inside each other (fixed: nesting; deeper: limit_hit)
77const CST_LIMIT: usize = 1536; // at most this many commands with a state
78// rows of a menu (fixed: a row's record index is a byte; more: limit_hit), all of them
79// reachable by keys and mnemonics
80const MENU_MAX: usize = 250;
81const MB_MAX: usize = 16; // menus per menu bar (fixed: wider than any window; more: limit_hit)
82const LAB_MAX: usize = 256;
83pub(crate) const CATCH_ID: u32 = 0x7FFE_0C0C; // hit box under the popups (light dismiss)
84const CTX_SALT: u32 = 0x00C0_FFEE;
85const BOXID_XOR: u32 = 0x6C00_0000; // flyout content box id = popup id ^ BOXID_XOR
86const FLS_XOR: u32 = 0x0100_0000; // (U3) scroll view of a tall flyout = content box id ^ FLS_XOR
87const TIP_DELAY: u32 = 500;
88const TIP_QUICK: u32 = 300;
89const BTN_MASK: u32 = (1 << BTN_LEFT) | (1 << BTN_RIGHT) | (1 << BTN_MIDDLE);
90const MOD3: u32 = MOD_SHIFT | MOD_CTRL | MOD_ALT;
91
92/// CMD record: one row of the app's command table (AppDesc::cmds)
93#[derive(Clone, Copy, Debug)]
94pub struct Command {
95    /// command id (!= 0); also the widget id of cmd_button
96    pub id: u32,
97    /// IC_* code point or 0
98    pub icon: u32,
99    /// UTF-8 label, '&' marks the mnemonic ("&&" = '&')
100    pub label: &'static str,
101    /// accelerator key VK_* (0 = none)
102    pub key: u16,
103    /// MOD_SHIFT | MOD_CTRL | MOD_ALT
104    pub mods: u8,
105    /// initial CF_* state
106    pub flags: u8,
107}
108
109impl Command {
110    /// CMD id, "&Label", icon, mods, key, flags (the order of the asm macro)
111    pub const fn new(id: u32, label: &'static str, icon: u32, mods: u32, key: u32, flags: u32) -> Command {
112        Command { id, icon, label, key: key as u16, mods: mods as u8, flags: flags as u8 }
113    }
114}
115
116/// menu record (MENUBAR / MENU / MI_* of ui.inc); menus are `static` slices of them
117#[derive(Clone, Copy, Debug)]
118#[non_exhaustive]
119pub enum Mi {
120    /// MI_CMD cmd: a row from the command table
121    Cmd(u32),
122    /// MI_SEP
123    Sep,
124    /// MI_SUB "Label", submenu
125    Sub(&'static str, &'static [Mi]),
126    /// MENU "Label", menu: a top-level menu of a menu bar
127    Menu(&'static str, &'static [Mi]),
128    /// MI_ITEM id, "Label", icon: a plain row, runs App::command(id, 0)
129    Item(u32, &'static str, u32),
130    /// MI_LIST base, max, "placeholder": one row per string of the rows set with
131    /// Ui::menu_list(base, ..) (at most max); row k runs App::command(base + k, 0) and has
132    /// the widget id base + k; no rows: the placeholder as a disabled row ("" = none)  (U1)
133    List(u32, u16, &'static str),
134}
135
136impl Mi {
137    // MI_LABEL: the label of MENU / MI_SUB / MI_ITEM, the placeholder of MI_LIST
138    fn label(&self) -> Option<&'static str> {
139        match *self {
140            Mi::Sub(l, _) | Mi::Menu(l, _) | Mi::Item(_, l, _) | Mi::List(_, _, l) => Some(l),
141            Mi::Cmd(_) | Mi::Sep => None,
142        }
143    }
144    // MI_ARG: the command of MI_CMD, the id of MI_ITEM, the base of MI_LIST
145    fn arg(&self) -> u32 {
146        match *self {
147            Mi::Cmd(a) | Mi::Item(a, _, _) | Mi::List(a, _, _) => a,
148            _ => 0,
149        }
150    }
151}
152
153// popup stack entry  [PP_*]
154#[derive(Clone, Copy, Default)]
155struct Pp {
156    id: u32,
157    /// PPF_*
158    flags: u32,
159    /// anchor x, y, w, h (px)
160    anchor: [i32; 4],
161    /// flyout rectangle when last drawn (px)
162    rect: [i32; 4],
163    /// menu of a runtime-drawn popup
164    menu: Option<&'static [Mi]>,
165    /// focus to restore when it closes
166    ret: u32,
167    /// (§6) how it hangs at the anchor (PA_*); its natural place before the platform's
168    /// placement moved it (px, logical): as computed (the key of the placement) and where its
169    /// rectangle lay (plat::popup_anchor)
170    kind: u32,
171    key: [i32; 2],
172    nat: [i32; 2],
173    /// pass that drew it, pass that opened it
174    seen: u32,
175    born: u32,
176    /// hit list range of its content (this pass)
177    hit: u32,
178    hitend: u32,
179    /// widget to focus once drawn (0 = none)
180    focusid: u32,
181}
182
183// flyout nesting record (flyout_begin .. flyout_end)  [FLN_*]
184#[derive(Clone, Copy, Default)]
185struct Fln {
186    open: bool,
187    idx: u32,
188    layer: u32,
189    dis: u32,
190    level: u32,
191    /// (U3) the content scrolls (taller than the area)
192    scroll: bool,
193    clip: [i32; 4],
194}
195
196/// state shared by all windows
197#[derive(Default)]
198pub(crate) struct S {
199    /// command states (ui_cmd_state; the asm's hash table of up to CST_LIMIT entries)
200    cst: BTreeMap<u32, u32>,
201    cst_warned: bool,
202    /// ui_accel_order: 0 = Ctrl, Shift, Alt (default), 1 = Ctrl, Alt, Shift
203    accel_order: u32,
204    /// rows of the Mi::List menus by base id (the app's STRLIST buffers)
205    pub(crate) lists: Vec<(u32, Vec<String>)>,
206    /// what an empty Mi::List says instead of its placeholder (by base id; "": the placeholder)
207    pub(crate) list_empty: Vec<(u32, String)>,
208}
209
210/// per-window state (ui_popup_wb0 .. ui_popup_wb1: popup stack, flyouts, menu bar, tooltip)
211#[derive(Default)]
212pub(crate) struct W {
213    pp: [Pp; POPUP_MAX],
214    pp_n: u32,
215    fln: [Fln; FLN_MAX],
216    fl_sp: u32,
217    fl_over: u32,
218    fl_level: u32,
219    fl_offy: i32,
220    catch_frame: u32,
221    mb_mode: bool,
222    mb_prev: u32,
223    alt_armed: bool,
224    ctx_target: u32,
225    tip_id: u32,
226    tip_t0: u32,
227    tip_block: u32,
228    tip_shown: u32,
229    tip_hid_t: u32,
230    tip_hid_ok: bool,
231    tip_suppress: bool,
232    /// the part of tip_id the tooltip is for (tooltip_part; 0: the whole widget)
233    tip_part: u64,
234    hot_prev: u32,
235    /// (U3) tooltip rectangle of its last pass (px), the pass that drew it
236    tip_rect: [i32; 4],
237    tip_seen: u32,
238    /// (§6) the pointer it hangs below, its natural place (px, logical)
239    tip_anchor: [i32; 4],
240    tip_nat: [i32; 2],
241}
242
243// ------------------------------------------------------------------ small helpers
244
245/// strip_amp(src) -> (src without the '&' markers ("&&" = '&'; at most LAB_MAX - 1 bytes),
246/// mnemonic (upper-case ASCII, 0 = none), byte offset of the mnemonic in the result or -1)
247fn strip_amp(src: &str) -> (String, u32, i32) {
248    let s = src.as_bytes();
249    let mut out: Vec<u8> = Vec::with_capacity(s.len().min(LAB_MAX));
250    let mut m = 0u32;
251    let mut pos = -1i32;
252    let mut i = 0;
253    while out.len() < LAB_MAX - 1 && i < s.len() {
254        let mut c = s[i];
255        if c == 0 {
256            break;
257        }
258        i += 1;
259        if c == b'&' {
260            if i >= s.len() || s[i] == 0 {
261                break;
262            }
263            c = s[i];
264            i += 1;
265            if c != b'&' && m == 0 {
266                pos = out.len() as i32;
267                m = c as u32;
268                if c.is_ascii_lowercase() {
269                    m -= 32;
270                }
271            }
272        }
273        out.push(c);
274    }
275    (utf8_string(out), m, pos)
276}
277
278// bytes -> String, cut at the last complete UTF-8 sequence (a label cut at LAB_MAX)
279fn utf8_string(mut b: Vec<u8>) -> String {
280    match core::str::from_utf8(&b) {
281        Ok(_) => {}
282        Err(e) => b.truncate(e.valid_up_to()),
283    }
284    String::from_utf8(b).unwrap_or_default()
285}
286
287// str_cat(dst, src, cap): append src while the total stays below cap (never inside a
288// UTF-8 sequence)
289fn cat(dst: &mut String, src: &str, cap: usize) {
290    if dst.len() >= cap {
291        return;
292    }
293    let mut n = (cap - dst.len() - 1).min(src.len());
294    while !src.is_char_boundary(n) {
295        n -= 1;
296    }
297    dst.push_str(&src[..n]);
298}
299
300/// menu_pid(menu, salt) -> popup id of a menu (low byte 0: rows are id + 1 + index)
301fn menu_pid(menu: &'static [Mi], salt: u32) -> u32 {
302    let a = (menu.as_ptr() as usize as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
303    let e = ((a >> 32) as u32) ^ salt.wrapping_mul(0x045D_9F3B);
304    (e & 0x0FFF_FF00) | 0xB000_0000
305}
306
307// mnem_find(mnemonics, vk) -> index or -1
308fn mnem_find(mn: &[u8], vk: u32) -> i32 {
309    mn.iter().position(|&m| m != 0 && m as u32 == vk).map_or(-1, |i| i as i32)
310}
311
312// is_alnum_vk(vk): VK '0'..'9' / 'A'..'Z'
313fn is_alnum_vk(vk: u32) -> bool {
314    vk.wrapping_sub(0x30) <= 9 || vk.wrapping_sub(0x41) <= 25
315}
316
317/// the strings of a STRLIST (the list ends at the first empty string)
318fn strlist_n(list: &[&str]) -> usize {
319    list.iter().position(|s| s.is_empty()).unwrap_or(list.len())
320}
321
322// strlist_at(list, i) -> string i of a STRLIST or None
323fn strlist_at<'a>(list: &[&'a str], i: i32) -> Option<&'a str> {
324    if i < 0 || i as usize >= strlist_n(list) { None } else { Some(list[i as usize]) }
325}
326
327// key names of the accelerator texts (German; i18n::tr translates them)  [key_table]
328static KEY_TABLE: [(u32, &str); 26] = [
329    (VK_DELETE, "Entf"),
330    (VK_INSERT, "Einfg"),
331    (VK_RETURN, "Eingabe"),
332    (VK_ESCAPE, "Esc"),
333    (VK_BACK, "Rücktaste"),
334    (VK_TAB, "Tab"),
335    (VK_SPACE, "Leertaste"),
336    (VK_HOME, "Pos1"),
337    (VK_END, "Ende"),
338    (VK_PRIOR, "Bild auf"),
339    (VK_NEXT, "Bild ab"),
340    (VK_LEFT, "Nach-links"),
341    (VK_RIGHT, "Nach-rechts"),
342    (VK_UP, "Nach-oben"),
343    (VK_DOWN, "Nach-unten"),
344    (VK_OEM_PLUS, "Plus"),
345    (VK_OEM_MINUS, "Minus"),
346    (VK_ADD, "Num +"),
347    (VK_SUBTRACT, "Num -"),
348    (VK_OEM_COMMA, ","),
349    (VK_OEM_PERIOD, "."),
350    (VK_OEM_4, "["), // (U1)
351    (VK_OEM_6, "]"),
352    (VK_MULTIPLY, "Num *"),
353    (VK_DIVIDE, "Num /"),
354    (VK_DECIMAL, "Num ,"),
355];
356
357// key_name(vk): letters / digits, "F1".."F24", "Num 0".."Num 9", the table, else "?"
358fn key_name(vk: u32) -> String {
359    if is_alnum_vk(vk) {
360        let mut s = String::new();
361        s.push(vk as u8 as char);
362        return s;
363    }
364    if vk.wrapping_sub(VK_F1) <= 23 {
365        return alloc::format!("F{}", vk - VK_F1 + 1);
366    }
367    if vk.wrapping_sub(VK_NUMPAD0) <= 9 {
368        return alloc::format!("Num {}", vk - VK_NUMPAD0); // (U1)
369    }
370    let name = KEY_TABLE.iter().find(|k| k.0 == vk).map_or("?", |k| crate::i18n::tr(k.1));
371    String::from(name)
372}
373
374impl Ui {
375    // ------------------------------------------------------------------ popup stack helpers
376
377    // pp_find(id) -> index in the popup stack or -1
378    fn pp_find(&self, id: u32) -> i32 {
379        let w = &self.wp.popup;
380        (0..w.pp_n as usize).rev().find(|&i| w.pp[i].id == id).map_or(-1, |i| i as i32)
381    }
382
383    // pp_flag(id, flags): or PPF_* flags into the entry of popup id (if open)
384    pub(crate) fn pp_flag(&mut self, id: u32, f: u32) {
385        let i = self.pp_find(id);
386        if i >= 0 {
387            self.wp.popup.pp[i as usize].flags |= f;
388        }
389    }
390
391    // pp_push(id, level) -> index: close popups >= level, open `id` at level
392    fn pp_push(&mut self, id: u32, level: u32) -> usize {
393        let mut l = level.min(self.wp.popup.pp_n);
394        if l > POPUP_MAX as u32 - 1 {
395            l = POPUP_MAX as u32 - 1;
396            self.limit_hit(super::ctx::LIM_POPUPS);
397        }
398        let w = &mut self.wp.popup;
399        w.pp_n = l + 1;
400        w.pp[l as usize] = Pp {
401            id,
402            ret: self.wp.ctx.focus,
403            born: self.frame,
404            anchor: self.wp.ctx.last_rect,
405            ..Pp::default()
406        };
407        self.repass = true;
408        l as usize
409    }
410
411    // pp_close_to(level): close the popups >= level; the focus returns to what it was when
412    // `level` opened
413    fn pp_close_to(&mut self, level: u32) {
414        let w = &mut self.wp.popup;
415        if level >= w.pp_n {
416            return;
417        }
418        w.pp_n = level;
419        self.wp.ctx.focus = w.pp[level as usize].ret;
420        self.repass = true;
421    }
422
423    // pp_dismiss_all: close every popup and leave menu bar mode
424    fn pp_dismiss_all(&mut self) {
425        self.pp_close_to(0);
426        if self.wp.popup.mb_mode {
427            self.wp.popup.mb_mode = false;
428            self.wp.ctx.focus = self.wp.popup.mb_prev;
429            self.repass = true;
430        }
431    }
432
433    // open_key -> Alt+Down or F4 was pressed (consumed): opens drop-downs.  F4 is left
434    // alone when the command table has an F4 accelerator (paint.net uses it).
435    pub(crate) fn open_key(&mut self) -> bool {
436        let k = self.key_first();
437        let vk = k & 0xFFFF;
438        let m = (k >> 16) & MOD3;
439        let yes = if vk == VK_F4 && m == 0 {
440            self.accel_find(VK_F4, 0).is_none() // (U1) an F4 accelerator wins
441        } else {
442            vk == VK_DOWN && m == MOD_ALT
443        };
444        if yes {
445            self.key(vk);
446        }
447        yes
448    }
449
450    // accel_find(vk, mods) -> the command whose accelerator is vk + mods
451    pub(crate) fn accel_find(&self, vk: u32, mods: u32) -> Option<&'static Command> {
452        let t: &'static [Command] = self.desc.cmds;
453        t.iter().take_while(|c| c.id != 0).find(|c| c.key as u32 == vk && c.mods as u32 & MOD3 == mods)
454    }
455
456    // last_widget -> (id, rect) of the last semantic record with an id (0 = none); the rect
457    // logical again (sems are physical right-to-left; the mirror is its own inverse)
458    fn last_widget(&self) -> (u32, [i32; 4]) {
459        self.sems.iter().rev().find(|r| r.id != 0).map_or((0, [0; 4]), |r| (r.id, self.phys_rect(r.rect)))
460    }
461
462    // focus_is_text: the focused widget is a text input (this pass)
463    pub(crate) fn focus_is_text(&self) -> bool {
464        let c = &self.wp.ctx;
465        if c.focus == 0 {
466            return false;
467        }
468        c.hits[c.hit_cur].iter().rev().find(|h| h.id == c.focus).is_some_and(|h| h.flags as u32 & HB_TEXT != 0)
469    }
470
471    // focus_was_text: the focused widget was a text input in the last pass (for questions
472    // asked before this pass drew it, as ui.undo_keys at the start of a frame)
473    pub(crate) fn focus_was_text(&self) -> bool {
474        let c = &self.wp.ctx;
475        c.focus != 0 && c.hits[c.hit_cur ^ 1].iter().rev().find(|h| h.id == c.focus).is_some_and(|h| h.flags as u32 & HB_TEXT != 0)
476    }
477
478    // uiw_popup_area (ui_win.s): the area the live window's popups may use when they may
479    // leave it (the screen / virtual desktop; known false: Wayland)  (ui-win)
480    fn pp_popup_area(&mut self) -> Option<([i32; 4], bool)> {
481        self.uiw_popup_area()
482    }
483
484    // g_uiw_noaccel (ui_win.s): the live window is a dialog window, no accelerators  (ui-win)
485    fn pp_noaccel(&self) -> bool {
486        self.m.win.noaccel
487    }
488
489    // pp_bounds(w, h) -> the area a popup of w x h px of the live window may use: x0, y0, x1, y1
490    // (px, window coordinates): the screen / virtual desktop around the window (headless
491    // without a virtual screen: the main window keeps its popups); where the screen is not
492    // known (Wayland) the window while the popup fits into it, else a wide area that the
493    // compositor keeps on screen
494    fn pp_bounds(&mut self, w: i32, h: i32) -> [i32; 4] {
495        let win = [0, 0, self.wp.rt.fb.w, self.wp.rt.fb.h];
496        match self.pp_popup_area() {
497            Some((r, true)) => r,
498            Some((r, false)) if w > win[2] || h > win[3] => r,
499            _ => win,
500        }
501    }
502
503    // pp_nudge(item, rect) -> (dx, dy): how far the platform moved popup item `item` from its
504    // natural place, computed as rect (px, logical) -- Wayland's compositor flips / slides popups
505    // that leave the window (uiw_popup_nudge)
506    fn pp_nudge(&self, item: u32, r: [i32; 4]) -> (i32, i32) {
507        let p = self.phys_rect(r);
508        let (dx, dy) = self.uiw_popup_nudge(item, [p[0], p[1]]);
509        (if self.rtl { -dx } else { dx }, dy)
510    }
511
512    // ------------------------------------------------------------------ popup stack API
513
514    /// ui_popup_is_open(id): popup `id` is open
515    pub fn popup_is_open(&self, id: u32) -> bool {
516        self.pp_find(id) >= 0
517    }
518
519    /// ui_popup_count -> open popups of the live window (U3)
520    pub fn popup_count(&self) -> u32 {
521        self.wp.popup.pp_n
522    }
523
524    /// ui_popup_open(id): open popup `id` (inside a flyout: as a child of that flyout,
525    /// otherwise it replaces every open popup); an open popup only loses its children
526    pub fn popup_open(&mut self, id: u32) {
527        let i = self.pp_find(id);
528        if i >= 0 {
529            let n = i as u32 + 1;
530            if n < self.wp.popup.pp_n {
531                self.wp.popup.pp_n = n;
532                self.repass = true;
533            }
534            return;
535        }
536        let l = self.wp.popup.fl_level;
537        self.pp_push(id, l);
538    }
539
540    /// ui_popup_close: close every popup (the focus returns to where it was)
541    pub fn popup_close(&mut self) {
542        self.pp_dismiss_all();
543    }
544
545    // popup_catcher -> a press outside every popup dismissed them.  Once per pass (while
546    // popups are open) a window-sized hit box goes under the popups, so the base layer gets
547    // no pointer input and a press there closes them.
548    fn popup_catcher(&mut self) -> bool {
549        if self.wp.popup.pp_n == 0 || self.wp.popup.catch_frame == self.frame {
550            return false;
551        }
552        self.wp.popup.catch_frame = self.frame;
553        let c = &mut self.wp.ctx;
554        if c.hot == CATCH_ID && c.pressed & BTN_MASK != 0 {
555            c.pressed &= !BTN_MASK;
556            self.pp_dismiss_all();
557            return true;
558        }
559        let (lr, clip) = (c.last_rect, c.uiclip);
560        let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
561        self.wp.ctx.last_rect = [0, 0, w, h];
562        self.wp.ctx.uiclip = [0, 0, w, h];
563        let prev = self.layer_set(L_POPUP);
564        self.hit(CATCH_ID, 0);
565        self.layer_set(prev);
566        self.wp.ctx.last_rect = lr;
567        self.wp.ctx.uiclip = clip;
568        false
569    }
570
571    // ------------------------------------------------------------------ flyouts
572
573    /// ui_flyout_begin(id, FL_*) -> popup `id` is open: then its content follows (a vertical
574    /// box with 4 DIP padding) until flyout_end, which is called in any case.  The flyout is
575    /// anchored to the last placed widget (FL_POINT: to the pointer position where it
576    /// opened) and kept inside the window.
577    #[cfg_attr(jfgui_dev, track_caller)]
578    pub fn flyout_begin(&mut self, id: u32, flags: u32) -> bool {
579        let _src = self.src_scope();
580        let sp_i = self.wp.popup.fl_sp as usize;
581        if sp_i >= FLN_MAX {
582            self.wp.popup.fl_over += 1;
583            self.limit_hit(super::ctx::LIM_FLYOUTS);
584            return false;
585        }
586        self.wp.popup.fl_sp += 1;
587        self.wp.popup.fln[sp_i].open = false;
588        let i = self.pp_find(id);
589        if i < 0 {
590            return false;
591        }
592        let idx = i as usize;
593        // anchor: the last widget, or a fixed point
594        {
595            let (mx, my, lr) = (self.wp.ctx.mx, self.wp.ctx.my, self.wp.ctx.last_rect);
596            let e = &mut self.wp.popup.pp[idx];
597            if e.flags & PPF_POINT == 0 {
598                if flags & FL_POINT != 0 {
599                    e.flags |= PPF_POINT;
600                    e.anchor = [mx, my, 0, 0];
601                } else {
602                    e.anchor = lr;
603                }
604            }
605        }
606        if self.popup_catcher() {
607            return false;
608        }
609        // Esc closes the top popup
610        if self.wp.popup.pp_n.wrapping_sub(1) == idx as u32 && self.key(VK_ESCAPE) {
611            self.pp_close_to(idx as u32);
612            self.wp.ctx.focus_visible = true;
613            return false;
614        }
615        // size from the previous pass
616        let boxid = id ^ BOXID_XOR;
617        let (mut fw, mut fh) = {
618            let s = self.slot(boxid);
619            (s.measw, s.meash)
620        };
621        let anchor = self.wp.popup.pp[idx].anchor;
622        let mut forced = 0; // forced width px (0 = natural)
623        if flags & FL_MINW != 0 && fw < anchor[2] {
624            fw = anchor[2];
625            forced = fw;
626        }
627        let st = self.style;
628        let gap = st.flyout_pad(self);
629        // the area the flyout may use (px, window coordinates): the window, or the screen for
630        // popups that become their own OS windows (pp_bounds, U3, §6)
631        let area = self.pp_bounds(fw, fh);
632        // taller than the area: it scrolls (U3).  Where the screen is not known (Wayland) the
633        // area is wide, but no popup gets taller than the screen: a long list scrolls instead
634        // of hanging off it
635        let mut room = area[3] - area[1];
636        if fh > self.wp.rt.fb.h {
637            if let Some(h) = self.uiw_popup_screen_h() {
638                room = room.min(h);
639            }
640        }
641        let mut scroll = false;
642        if fh > room {
643            fh = room;
644            scroll = true;
645        }
646        // position (x, y) in px
647        let mut x = anchor[0];
648        let mut y = anchor[1];
649        let point = self.wp.popup.pp[idx].flags & PPF_POINT != 0;
650        if point {
651            if x + fw > area[2] {
652                x -= fw;
653            }
654            if y + fh > area[3] {
655                y -= fh;
656            }
657        } else if flags & FL_RIGHT != 0 {
658            // submenu: right of the row, first row level with it; flips to the left
659            x += anchor[2];
660            y -= gap;
661            if x + fw > area[2] {
662                x = anchor[0] - fw;
663            }
664        } else if flags & FL_OVER != 0 {
665            if !scroll {
666                y -= self.wp.popup.fl_offy; // scrolling list: from the area top
667            }
668        } else {
669            if flags & FL_ALIGN_END != 0 {
670                x += anchor[2] - fw;
671            }
672            if flags & FL_ABOVE != 0 {
673                y -= gap + fh;
674                if y < area[1] {
675                    y = anchor[1] + anchor[3] + gap;
676                }
677            } else {
678                // below; above when it does not fit
679                y += anchor[3] + gap;
680                if y + fh > area[3] {
681                    let a = anchor[1] - gap - fh;
682                    if a >= area[1] {
683                        y = a;
684                    }
685                }
686            }
687        }
688        // kept inside the area
689        if x > area[2] - fw {
690            x = area[2] - fw;
691        }
692        if x < area[0] {
693            x = area[0];
694        }
695        if y > area[3] - fh {
696            y = area[3] - fh;
697        }
698        if y < area[1] {
699            y = area[1];
700        }
701        // (§6) how it hangs at its anchor; where the platform put it (Wayland)
702        let kind = if point {
703            PA_POINT
704        } else if flags & FL_RIGHT != 0 {
705            PA_SIDE
706        } else if flags & FL_OVER != 0 {
707            PA_FREE
708        } else if y < anchor[1] {
709            PA_ABOVE
710        } else {
711            PA_BELOW
712        };
713        let key = [x, y];
714        let (dx, dy) = self.pp_nudge(idx as u32, [x, y, fw, fh]);
715        x += dx;
716        y += dy;
717        let e = &mut self.wp.popup.pp[idx];
718        e.kind = kind;
719        e.key = key;
720        // absolute placement of the content box (DIP relative to the current box)
721        let br = self.box_rect();
722        let xd = self.px2dip(x - br[0]);
723        let yd = self.px2dip(y - br[1]);
724        let wd = if forced != 0 { self.px2dip(forced) } else { 0 };
725        let hd = if scroll { self.px2dip(fh) } else { 0 };
726        self.next_rect(xd, yd, wd, hd);
727        // popup layer, clipping to the area, not disabled
728        let layer = self.layer_set(L_POPUP);
729        let w = &mut self.wp.popup;
730        let f = &mut w.fln[sp_i];
731        f.layer = layer;
732        f.clip = self.wp.ctx.uiclip;
733        f.dis = self.wp.ctx.disabled_depth;
734        self.wp.ctx.disabled_depth = 0;
735        self.wp.ctx.uiclip = area;
736        f.level = w.fl_level;
737        f.idx = idx as u32;
738        w.fl_level = idx as u32 + 1;
739        f.open = true;
740        f.scroll = scroll;
741        let frame = self.frame;
742        if !scroll {
743            // content box; its rectangle is the flyout
744            let bf = st.flyout_box(self);
745            self.box_begin(boxid, bf, 0);
746            let e = &mut self.wp.popup.pp[idx];
747            e.rect = self.wp.ctx.last_rect;
748            e.nat = [e.rect[0] - dx, e.rect[1] - dy];
749            e.seen = frame;
750            self.fl_surface(idx, boxid);
751            return true;
752        }
753        // (U3) too tall: the surface at the height of the area, a scroll view with the
754        // content box inside (the shadow stays outside the clip of the view)
755        let e = &mut self.wp.popup.pp[idx];
756        e.rect = [x, y, fw, fh];
757        e.nat = key;
758        e.seen = frame;
759        self.fl_surface(idx, boxid);
760        self.fl_scroll_begin(boxid ^ FLS_XOR);
761        // a combo box list opens with its selected row in view
762        let e = &mut self.wp.popup.pp[idx];
763        if e.flags & PPF_SCROLLED == 0 {
764            e.flags |= PPF_SCROLLED;
765            if flags & FL_OVER != 0 {
766                let oy = self.px2dip(self.wp.popup.fl_offy);
767                self.fl_scroll_to(boxid ^ FLS_XOR, oy);
768            }
769        }
770        let bf = st.flyout_box(self);
771        self.box_begin(boxid, bf, 0);
772        true
773    }
774
775    // (U3) flyouts taller than the area scroll (ui-list's scroll view)
776    fn fl_scroll_begin(&mut self, id: u32) {
777        self.scroll_begin(id, SC_V);
778    }
779    fn fl_scroll_to(&mut self, id: u32, y_dip: i32) {
780        self.scroll_to(id, -1, y_dip);
781    }
782    fn fl_scroll_end(&mut self) {
783        self.scroll_end();
784    }
785
786    /// ui_popup_ext(i) -> rect x, y, w, h (px, window coordinates) of item i of the live
787    /// window when it was drawn in the last pass partly outside its framebuffer.  Items
788    /// 0 .. 5 are the open popups, 6 the tooltip.  (U3, OS popup windows)
789    pub fn popup_ext(&self, i: u32) -> Option<[i32; 4]> {
790        self.popup_item(i, false)
791    }
792
793    /// ui_popup_rect(i) -> rect (px) of item i of the live window when it was drawn in the
794    /// last pass (anywhere).  The main window gives its popups own OS windows with it while
795    /// tool windows are shown, so menus stay above them.
796    pub fn popup_rect(&self, i: u32) -> Option<[i32; 4]> {
797        self.popup_item(i, true)
798    }
799
800    /// (§6) how item i of the live window hangs at its anchor (physical px): the anchor
801    /// rectangle, the PA_* kind, its natural place x, y (plat::popup_anchor) and the key of the
802    /// placement (the natural place as computed: uiw_popup_nudge)
803    pub(crate) fn popup_place(&self, i: u32) -> Option<([i32; 4], u32, [i32; 2], [i32; 2])> {
804        let w = &self.wp.popup;
805        let (anchor, kind, nat, key, r) = if i == POPUP_MAX as u32 {
806            (w.tip_anchor, PA_BELOW, w.tip_nat, w.tip_nat, w.tip_rect)
807        } else {
808            let e = w.pp.get(i as usize).filter(|_| i < w.pp_n)?;
809            (e.anchor, e.kind, e.nat, e.key, e.rect)
810        };
811        let n = self.phys_rect([nat[0], nat[1], r[2], r[3]]);
812        let k = self.phys_rect([key[0], key[1], r[2], r[3]]);
813        Some((self.phys_rect(anchor), kind, [n[0], n[1]], [k[0], k[1]]))
814    }
815
816    fn popup_item(&self, i: u32, anywhere: bool) -> Option<[i32; 4]> {
817        let w = &self.wp.popup;
818        let r = if i == POPUP_MAX as u32 {
819            if w.tip_shown == 0 || w.tip_seen != self.frame {
820                return None;
821            }
822            w.tip_rect
823        } else {
824            if i > POPUP_MAX as u32 || i >= w.pp_n {
825                return None;
826            }
827            let e = &w.pp[i as usize];
828            if e.seen != self.frame {
829                return None;
830            }
831            e.rect
832        };
833        if r[2] <= 0 || r[3] <= 0 {
834            return None;
835        }
836        let r = self.phys_rect(r); // window px (right-to-left: mirrored)
837        if anywhere {
838            return Some(r);
839        }
840        // inside the framebuffer: drawn there, no own window
841        let fb = &self.wp.rt.fb;
842        if r[0] < 0 || r[1] < 0 || r[2] + r[0] > fb.w || r[1] + r[3] > fb.h { Some(r) } else { None }
843    }
844
845    // fl_surface(entry, box id): shadow, fill, 1 px stroke, 8 DIP corners of the flyout at
846    // entry.rect; its body blocks the pointer (hit box before the content)
847    fn fl_surface(&mut self, idx: usize, boxid: u32) {
848        let n = self.wp.ctx.hits[self.wp.ctx.hit_cur].len() as u32;
849        let e = &mut self.wp.popup.pp[idx];
850        e.hit = n;
851        e.hitend = n;
852        let r = e.rect;
853        if r[2] <= 0 || r[3] <= 0 {
854            return;
855        }
856        let st = self.style;
857        let mut sp = Sp::default();
858        sp.place(r);
859        st.flyout_frame(self, &sp);
860        self.hit(boxid, HB_BLOCK);
861    }
862
863    /// ui_flyout_end: close the content of flyout_begin (also when it returned false)
864    #[cfg_attr(jfgui_dev, track_caller)]
865    pub fn flyout_end(&mut self) {
866        let _src = self.src_scope();
867        let w = &mut self.wp.popup;
868        if w.fl_over != 0 {
869            w.fl_over -= 1;
870            return;
871        }
872        if w.fl_sp == 0 {
873            return;
874        }
875        w.fl_sp -= 1;
876        let f = w.fln[w.fl_sp as usize];
877        if !f.open {
878            return;
879        }
880        self.box_end();
881        if f.scroll {
882            self.fl_scroll_end(); // (U3) tall flyout
883        }
884        self.layer_set(f.layer);
885        self.wp.ctx.uiclip = f.clip;
886        self.wp.ctx.disabled_depth = f.dis;
887        self.wp.popup.fl_level = f.level;
888        // the content may have closed it
889        let i = f.idx;
890        if i >= self.wp.popup.pp_n {
891            return;
892        }
893        let idx = i as usize;
894        self.wp.popup.pp[idx].hitend = self.wp.ctx.hits[self.wp.ctx.hit_cur].len() as u32;
895        // focus requests
896        let fid = self.wp.popup.pp[idx].focusid;
897        if fid != 0 {
898            let e = &mut self.wp.popup.pp[idx];
899            e.focusid = 0;
900            if e.flags & PPF_QUIET != 0 {
901                e.flags &= !PPF_QUIET;
902                self.wp.ctx.focus = fid;
903                self.repass = true;
904            } else {
905                self.focus(fid);
906            }
907        } else if self.wp.popup.pp[idx].flags & PPF_FOCUS != 0 {
908            self.wp.popup.pp[idx].flags &= !PPF_FOCUS;
909            let t = self.pp_nav(idx, 0);
910            if t != 0 {
911                self.focus(t);
912            }
913        }
914        // Up / Down move the focus through the rows of the top popup
915        if self.wp.popup.pp_n.wrapping_sub(1) == i {
916            let nav = if self.key_peek(VK_DOWN) {
917                Some((1, VK_DOWN))
918            } else if self.key_peek(VK_UP) {
919                Some((-1, VK_UP))
920            } else {
921                None
922            };
923            if let Some((dir, vk)) = nav {
924                let t = self.pp_nav(idx, dir);
925                if t != 0 {
926                    self.key(vk);
927                    self.focus(t);
928                }
929            }
930        }
931        self.wp.ctx.last_rect = self.wp.popup.pp[idx].rect;
932    }
933
934    // pp_nav(entry, dir) -> id of the focusable row to focus (0 = none): dir 0 first, +1
935    // after the focused one, -1 before it (wrapping)
936    fn pp_nav(&self, idx: usize, dir: i32) -> u32 {
937        let e = &self.wp.popup.pp[idx];
938        let c = &self.wp.ctx;
939        let list = &c.hits[c.hit_cur];
940        let (a, b) = ((e.hit as usize).min(list.len()), (e.hitend as usize).min(list.len()));
941        let (mut first, mut last, mut before, mut after, mut found, mut prev) = (0, 0, 0, 0, false, 0);
942        for h in &list[a..b.max(a)] {
943            if h.flags as u32 & (HB_FOCUS | HB_DISABLED) != HB_FOCUS || h.layer as u32 != L_POPUP {
944                continue;
945            }
946            let id = h.id;
947            if first == 0 {
948                first = id;
949            }
950            last = id;
951            if found {
952                if after == 0 {
953                    after = id;
954                }
955            } else if id == c.focus {
956                found = true;
957                before = prev;
958            }
959            prev = id;
960        }
961        if dir == 0 {
962            return first;
963        }
964        if !found {
965            return if dir > 0 { first } else { last };
966        }
967        if dir > 0 {
968            if after != 0 { after } else { first }
969        } else if before != 0 {
970            before
971        } else {
972            last
973        }
974    }
975
976    // ------------------------------------------------------------------ commands
977
978    /// ui_cmd_find(id) -> the command record of the app's command table
979    pub fn cmd_find(&self, id: u32) -> Option<&'static Command> {
980        let t: &'static [Command] = self.desc.cmds;
981        t.iter().take_while(|c| c.id != 0).find(|c| c.id == id)
982    }
983
984    /// ui_cmd_state(id, flags): set the CF_* state of a command; it stays until changed
985    /// (calling it every frame with the same flags is fine) and overrides the table flags.
986    /// Up to 1536 commands have a state; more print one UIWARN and are ignored.
987    pub fn cmd_state(&mut self, id: u32, flags: u32) {
988        if id == 0 {
989            return;
990        }
991        let s = &mut self.m.popup;
992        let cur = match s.cst.get(&id) {
993            Some(&f) => f,
994            None if s.cst.len() >= CST_LIMIT => {
995                if !s.cst_warned {
996                    s.cst_warned = true;
997                    let m = alloc::format!("ui_cmd_state: more than {} command states (id {} ignored)", CST_LIMIT, id);
998                    self.warn(&m);
999                }
1000                return;
1001            }
1002            None => u32::MAX, // a new entry starts at -1
1003        };
1004        self.m.popup.cst.insert(id, flags);
1005        if cur != flags {
1006            self.repass = true;
1007        }
1008    }
1009
1010    /// ui_cmd_flags(id) -> CF_* state (cmd_state, else the table flags)
1011    pub fn cmd_flags(&self, id: u32) -> u32 {
1012        if id != 0 {
1013            if let Some(&f) = self.m.popup.cst.get(&id) {
1014                return f;
1015            }
1016        }
1017        self.cmd_find(id).map_or(0, |c| c.flags as u32)
1018    }
1019
1020    /// ui_cmd_label(id) -> label of a command without '&' ("" if unknown)
1021    pub fn cmd_label(&self, id: u32) -> String {
1022        strip_amp(self.cmd_find(id).map_or("", |c| c.label)).0
1023    }
1024
1025    /// ui_accel_text(id) -> accelerator of a command as text, e.g. "Strg+Umschalt+S"
1026    pub fn accel_text(&self, id: u32) -> String {
1027        match self.cmd_find(id) {
1028            Some(c) => self.accel_fmt(c, 64),
1029            None => String::new(),
1030        }
1031    }
1032
1033    /// ui_accel_order: 0 = "Strg+Umschalt+Alt+X" (default), 1 = "Strg+Alt+Umschalt+X"  (U1)
1034    pub fn accel_order_set(&mut self, order: u32) {
1035        self.m.popup.accel_order = order;
1036    }
1037
1038    // accel_fmt(rec, cap) -> "Strg+Umschalt+Alt+Key" (key names of the UI language, i18n::tr;
1039    // accel_order 1: "Strg+Alt+Umschalt+Key")
1040    fn accel_fmt(&self, c: &Command, cap: usize) -> String {
1041        let mut s = String::new();
1042        if c.key == 0 {
1043            return s;
1044        }
1045        let modifier = |s: &mut String, name: &'static str| {
1046            cat(s, crate::i18n::tr(name), cap);
1047            cat(s, "+", cap);
1048        };
1049        let mut m = c.mods as u32;
1050        if m & MOD_CTRL != 0 {
1051            modifier(&mut s, "Strg");
1052        }
1053        if self.m.popup.accel_order != 0 && m & MOD_ALT != 0 {
1054            modifier(&mut s, "Alt"); // (U1) Ctrl, Alt, Shift
1055            m &= !MOD_ALT;
1056        }
1057        if m & MOD_SHIFT != 0 {
1058            modifier(&mut s, "Umschalt");
1059        }
1060        if m & MOD_ALT != 0 {
1061            modifier(&mut s, "Alt");
1062        }
1063        cat(&mut s, &key_name(c.key as u32), cap);
1064        s
1065    }
1066
1067    /// ui_cmd_button(id, ST_*) -> EV_*: button for a command: icon and label from the table
1068    /// (ST_ICON_ONLY: icon only), tooltip "Label (Strg+S)", ST_DISABLED / ST_CHECKED from the
1069    /// command state; a click queues the command.  Hidden commands draw nothing.
1070    #[cfg_attr(jfgui_dev, track_caller)]
1071    pub fn cmd_button(&mut self, id: u32, flags: u32) -> u32 {
1072        let _src = self.src_scope();
1073        let Some(c) = self.cmd_find(id) else { return 0 };
1074        let cf = self.cmd_flags(id);
1075        if cf & CF_HIDDEN != 0 {
1076            return 0;
1077        }
1078        let mut f = flags;
1079        if cf & CF_DISABLED != 0 {
1080            f |= ST_DISABLED;
1081        }
1082        if cf & CF_CHECKED != 0 {
1083            f |= ST_CHECKED;
1084        }
1085        self.next_flags(f);
1086        if c.icon != 0 {
1087            self.next_icon(c.icon);
1088        }
1089        let lab = strip_amp(c.label).0;
1090        self.wp.popup.tip_suppress = true;
1091        let ev = self.button(id, &lab);
1092        self.wp.popup.tip_suppress = false;
1093        if ev & EV_ACT != 0 {
1094            self.cmd_run(id, 0);
1095        }
1096        // tooltip: label (accelerator)
1097        let mut tip = String::new();
1098        cat(&mut tip, &lab, LAB_MAX);
1099        let acc = self.accel_fmt(c, 64);
1100        if !acc.is_empty() {
1101            cat(&mut tip, " (", LAB_MAX);
1102            cat(&mut tip, &acc, LAB_MAX);
1103            cat(&mut tip, ")", LAB_MAX);
1104        }
1105        self.tip_core(&tip);
1106        ev
1107    }
1108
1109    // ------------------------------------------------------------------ menu rows
1110
1111    // mitem(id, label, icon, accel, mflags) -> EV: one menu / list row (32 DIP): icon or
1112    // check column, label, accelerator right-aligned, submenu chevron
1113    fn mitem(&mut self, id: u32, label: &str, icon: u32, accel: Option<&str>, mflags: u32) -> u32 {
1114        let mut mf = mflags;
1115        // (U1) WinUI: a flyout with a checked row that has an icon shows a check column and
1116        // an icon column (MIF_TWOCOL); the flag lives in the flyout's popup entry
1117        let lv = self.wp.popup.fl_level.wrapping_sub(1);
1118        if (lv as i32) >= 0 && mf & MIF_LIST == 0 && lv < self.wp.popup.pp_n {
1119            let e = &mut self.wp.popup.pp[lv as usize];
1120            if mf & MIF_CHECKED != 0 && icon != 0 && e.flags & PPF_TWOCOL == 0 {
1121                e.flags |= PPF_TWOCOL;
1122                self.repass = true;
1123            }
1124            if e.flags & PPF_TWOCOL != 0 {
1125                mf |= MIF_TWOCOL;
1126            }
1127        }
1128        let lab = strip_amp(label).0;
1129        let st = self.style;
1130        let mut sp = Sp { label: Some(&lab), label2: accel, flags: mf, ..Sp::default() };
1131        let (w, h) = st.menu_item_size(self, &mut sp);
1132        let r = self.place(w, h);
1133        let mut hb = HB_FOCUS;
1134        if mf & MIF_DISABLED != 0 {
1135            hb |= HB_DISABLED;
1136        }
1137        let ev = self.behave(id, hb);
1138        sp.place(r);
1139        sp.ev = ev;
1140        sp.flags = mf;
1141        sp.icon = icon;
1142        st.menu_item(self, &sp);
1143        let mut s = self.wp.ctx.last_state;
1144        if mf & MIF_CHECKED != 0 {
1145            s |= SS_CHECKED as u32;
1146        }
1147        if mf & MIF_OPEN != 0 {
1148            s |= SS_OPEN as u32;
1149        }
1150        if mf & MIF_SELECTED != 0 {
1151            s |= SS_SELECTED as u32;
1152        }
1153        self.wp.ctx.last_state = s;
1154        let kind = if mf & MIF_LIST != 0 { K_LISTITEM } else { K_MENUITEM };
1155        self.sem(kind, id, Some(&lab), 0);
1156        ev
1157    }
1158
1159    // mcmd(cmd, id) -> EV: menu row `id` for a command; activation queues it and closes
1160    // every popup.  Hidden commands draw nothing (0).
1161    fn mcmd(&mut self, cmd: u32, id: u32) -> u32 {
1162        let c = self.cmd_find(cmd);
1163        let cf = self.cmd_flags(cmd);
1164        if cf & CF_HIDDEN != 0 {
1165            return 0;
1166        }
1167        let acc = match c {
1168            Some(c) => self.accel_fmt(c, 64),
1169            None => String::new(),
1170        };
1171        let mut mf = 0;
1172        if cf & CF_DISABLED != 0 {
1173            mf |= MIF_DISABLED;
1174        }
1175        if cf & CF_CHECKED != 0 {
1176            mf |= MIF_CHECKED;
1177        }
1178        let (label, icon) = match c {
1179            Some(c) => (c.label, c.icon),
1180            None => ("?", 0),
1181        };
1182        let ev = self.mitem(id, label, icon, Some(&acc), mf);
1183        if ev & EV_ACT != 0 {
1184            self.cmd_run(cmd, 0);
1185            self.pp_dismiss_all();
1186        }
1187        ev
1188    }
1189
1190    /// ui_menu_item(id, label) -> EV_*: menu row inside a flyout (next_icon; next_flags
1191    /// ST_DISABLED / ST_CHECKED); activation closes every popup
1192    #[cfg_attr(jfgui_dev, track_caller)]
1193    pub fn menu_item(&mut self, id: u32, label: &str) -> u32 {
1194        let _src = self.src_scope();
1195        let f = self.take_flags();
1196        let mut mf = 0;
1197        if f & ST_DISABLED != 0 {
1198            mf |= MIF_DISABLED;
1199        }
1200        if f & ST_CHECKED != 0 {
1201            mf |= MIF_CHECKED;
1202        }
1203        let icon = self.take_icon();
1204        let ev = self.mitem(id, label, icon, None, mf);
1205        if ev & EV_ACT != 0 {
1206            self.pp_dismiss_all();
1207        }
1208        ev
1209    }
1210
1211    /// menu_item with `keys` (key_text) at the end of the row: a row whose keys the app
1212    /// handles itself (no command of the table)
1213    #[cfg_attr(jfgui_dev, track_caller)]
1214    pub fn menu_item_keys(&mut self, id: u32, label: &str, keys: &str) -> u32 {
1215        let _src = self.src_scope();
1216        let f = self.take_flags();
1217        let mut mf = 0;
1218        if f & ST_DISABLED != 0 {
1219            mf |= MIF_DISABLED;
1220        }
1221        if f & ST_CHECKED != 0 {
1222            mf |= MIF_CHECKED;
1223        }
1224        let icon = self.take_icon();
1225        let ev = self.mitem(id, label, icon, if keys.is_empty() { None } else { Some(keys) }, mf);
1226        if ev & EV_ACT != 0 {
1227            self.pp_dismiss_all();
1228        }
1229        ev
1230    }
1231
1232    /// a key with modifiers (MOD_*) as the accelerator texts write it: "Strg+Umschalt+S",
1233    /// "F5" (the UI language's names; accel_order_set holds)
1234    pub fn key_text(&self, vk: u32, mods: u32) -> String {
1235        self.accel_fmt(&Command::new(0, "", 0, mods, vk, 0), 64)
1236    }
1237
1238    /// ui_menu_cmd(cmd) -> EV_*: menu row for a command (widget id = command id)
1239    #[cfg_attr(jfgui_dev, track_caller)]
1240    pub fn menu_cmd(&mut self, cmd: u32) -> u32 {
1241        let _src = self.src_scope();
1242        self.mcmd(cmd, cmd)
1243    }
1244
1245    /// ui_menu_sep -> 0: separator line across the flyout
1246    #[cfg_attr(jfgui_dev, track_caller)]
1247    pub fn menu_sep(&mut self) -> u32 {
1248        let _src = self.src_scope();
1249        let st = self.style;
1250        let h = st.menu_sep_size(self);
1251        let r = self.place(0, h);
1252        let mut sp = Sp::default();
1253        sp.place(r);
1254        st.menu_sep(self, &sp);
1255        0
1256    }
1257
1258    /// the disabled row an empty Mi::List with this base id shows instead of its placeholder
1259    /// ("": the placeholder again), for a list that can be empty for different reasons
1260    #[cfg_attr(jfgui_dev, track_caller)]
1261    pub fn menu_list_empty(&mut self, base: u32, text: &str) {
1262        let _src = self.src_scope();
1263        let l = &mut self.m.popup.list_empty;
1264        match l.iter_mut().find(|e| e.0 == base) {
1265            Some(e) => {
1266                if e.1 != text {
1267                    e.1 = String::from(text);
1268                }
1269            }
1270            None => l.push((base, String::from(text))),
1271        }
1272    }
1273
1274    /// the rows of the Mi::List menus with this base id (row k: id base + k; the asm app's
1275    /// STRLIST buffer); the list ends at the first empty string
1276    #[cfg_attr(jfgui_dev, track_caller)]
1277    pub fn menu_list<T: AsRef<str>>(&mut self, base: u32, rows: &[T]) {
1278        let _src = self.src_scope();
1279        let v: Vec<String> = rows.iter().map(|s| String::from(s.as_ref())).collect();
1280        let l = &mut self.m.popup.lists;
1281        match l.iter_mut().find(|e| e.0 == base) {
1282            Some(e) => e.1 = v,
1283            None => l.push((base, v)),
1284        }
1285    }
1286
1287    // row_mnemonic(rec) -> mnemonic of a menu record's label (0 = none)
1288    fn row_mnemonic(&self, rec: &Mi) -> u32 {
1289        let label = match rec {
1290            Mi::Cmd(c) => self.cmd_find(*c).map(|c| c.label),
1291            r => r.label(),
1292        };
1293        strip_amp(label.unwrap_or("")).1
1294    }
1295
1296    // menu_flyout(menu, pid, flags) -> open: the flyout of a menu with its submenus (drawn
1297    // next to their rows) and keyboard handling
1298    fn menu_flyout(&mut self, menu: &'static [Mi], pid: u32, flags: u32) -> bool {
1299        let mut rows = 0usize;
1300        let mut ids = [0u32; MENU_MAX];
1301        let mut recs = [0u8; MENU_MAX];
1302        let mut activated = false;
1303        // the open submenu with its row: built after this menu, so it lies on top of the later
1304        // rows (a phone's submenu overlaps its menu) and outside the scroll view of a tall menu
1305        let mut open_sub: Option<(&'static [Mi], u32, [i32; 4])> = None;
1306        let open = self.flyout_begin(pid, flags);
1307        if !open {
1308            self.flyout_end();
1309            return false;
1310        }
1311        let level = self.pp_find(pid) as u32;
1312        // .mf_remember: add a row to the keyboard list
1313        let remember = |rows: &mut usize, ids: &mut [u32; MENU_MAX], recs: &mut [u8; MENU_MAX], id: u32, ri: usize| {
1314            if *rows < MENU_MAX {
1315                ids[*rows] = id;
1316                recs[*rows] = ri as u8;
1317                *rows += 1;
1318            }
1319        };
1320        for (ri, rec) in menu.iter().enumerate() {
1321            if ri >= MENU_MAX {
1322                self.limit_hit(super::ctx::LIM_MENU_ROWS);
1323                break;
1324            }
1325            let rid = pid.wrapping_add(ri as u32 + 1); // row id
1326            let ev = match *rec {
1327                Mi::Sep => {
1328                    self.menu_sep();
1329                    continue;
1330                }
1331                Mi::Cmd(c) => {
1332                    if self.cmd_flags(c) & CF_HIDDEN != 0 {
1333                        continue;
1334                    }
1335                    self.mcmd(c, rid)
1336                }
1337                Mi::Item(id, label, icon) => {
1338                    let ev = self.mitem(rid, label, icon, None, 0);
1339                    if ev & EV_ACT != 0 {
1340                        self.cmd_run(id, 0);
1341                        self.pp_dismiss_all();
1342                    }
1343                    ev
1344                }
1345                Mi::Sub(label, sub) => {
1346                    let spid = menu_pid(sub, rid); // submenu popup id
1347                    let sopen = self.pp_find(spid) >= 0;
1348                    let mf = if sopen { MIF_SUB | MIF_OPEN } else { MIF_SUB };
1349                    let ev = self.mitem(rid, label, 0, None, mf);
1350                    remember(&mut rows, &mut ids, &mut recs, rid, ri);
1351                    if !sopen {
1352                        // entering the row, a click or Enter opens it (Enter: focus on its
1353                        // first row)
1354                        let entered = ev & EV_HOVER != 0 && self.wp.ctx.hot != self.wp.popup.hot_prev;
1355                        if ev & EV_ACT == 0 && !entered {
1356                            continue;
1357                        }
1358                        self.popup_open(spid);
1359                        if ev & EV_ACT != 0 && self.wp.ctx.focus_visible {
1360                            self.pp_flag(spid, PPF_FOCUS);
1361                        }
1362                    }
1363                    open_sub = Some((sub, spid, self.wp.ctx.last_rect));
1364                    continue;
1365                }
1366                Mi::List(base, max, empty) => {
1367                    // (U1) one row per string of the list (widget id = command id base + k);
1368                    // an empty list shows its placeholder, disabled
1369                    let items: Vec<String> =
1370                        self.m.popup.lists.iter().find(|e| e.0 == base).map_or(Vec::new(), |e| e.1.clone());
1371                    let mut k = 0u32;
1372                    for s in items.iter() {
1373                        if s.is_empty() || k >= max as u32 {
1374                            break;
1375                        }
1376                        let id = base.wrapping_add(k);
1377                        let ev = self.mitem(id, s, 0, None, 0);
1378                        if ev & EV_ACT != 0 {
1379                            activated = true;
1380                            self.cmd_run(id, 0);
1381                            self.pp_dismiss_all();
1382                        } else if ev & EV_HOVER != 0
1383                            && self.wp.ctx.hot != self.wp.popup.hot_prev
1384                            && level + 1 < self.wp.popup.pp_n
1385                        {
1386                            // entering a row closes the open submenu of this menu
1387                            self.pp_close_to(level + 1);
1388                        }
1389                        k += 1;
1390                    }
1391                    let reason = self.m.popup.list_empty.iter().find(|e| e.0 == base && !e.1.is_empty()).map(|e| e.1.clone());
1392                    let empty = reason.as_deref().unwrap_or(empty);
1393                    if k == 0 && !empty.is_empty() {
1394                        self.mitem(rid, empty, 0, None, MIF_DISABLED); // (P1) "": no row
1395                    }
1396                    continue;
1397                }
1398                Mi::Menu(..) => continue,
1399            };
1400            // .mf_row: Cmd / Item rows
1401            if ev & EV_ACT != 0 {
1402                activated = true;
1403            }
1404            // entering a row closes the open submenu of this menu
1405            if ev & EV_HOVER != 0 && self.wp.ctx.hot != self.wp.popup.hot_prev && level + 1 < self.wp.popup.pp_n {
1406                self.pp_close_to(level + 1);
1407            }
1408            if self.wp.ctx.last_state & SS_DISABLED as u32 == 0 {
1409                remember(&mut rows, &mut ids, &mut recs, rid, ri);
1410            }
1411        }
1412        // keys go to the top popup only
1413        'keys: {
1414            if activated || self.wp.popup.pp_n.wrapping_sub(1) != level {
1415                break 'keys;
1416            }
1417            // the focused row
1418            let focus = self.wp.ctx.focus;
1419            let sel = (0..rows).rev().find(|&j| ids[j] == focus).map(|j| (ids[j], recs[j] as usize));
1420            // Right on a submenu row opens it with the focus inside
1421            if let Some((rid, ri)) = sel {
1422                if let Mi::Sub(_, sub) = menu[ri] {
1423                    if self.key(VK_RIGHT) {
1424                        self.mf_open_sub(sub, rid);
1425                        break 'keys;
1426                    }
1427                }
1428            }
1429            // Left closes a submenu (the focus returns to its row)
1430            if level != 0 && self.key(VK_LEFT) {
1431                self.pp_close_to(level);
1432                self.wp.ctx.focus_visible = true;
1433                break 'keys;
1434            }
1435            // a letter chooses the row with that mnemonic
1436            let k = self.key_first();
1437            if k == 0 || k & (MOD_CTRL << 16) != 0 {
1438                break 'keys;
1439            }
1440            let vk = k & 0xFFFF;
1441            if !is_alnum_vk(vk) {
1442                break 'keys;
1443            }
1444            for j in 0..rows {
1445                let rec = menu[recs[j] as usize];
1446                if self.row_mnemonic(&rec) != vk {
1447                    continue;
1448                }
1449                self.key(vk);
1450                let rid = ids[j];
1451                if let Mi::Sub(_, sub) = rec {
1452                    self.focus(rid);
1453                    self.mf_open_sub(sub, rid);
1454                } else {
1455                    self.cmd_run(rec.arg(), 0);
1456                    self.pp_dismiss_all();
1457                }
1458                break;
1459            }
1460        }
1461        self.flyout_end();
1462        if let Some((sub, spid, row)) = open_sub {
1463            let rect = self.wp.ctx.last_rect;
1464            self.wp.ctx.last_rect = row; // what flyout_begin anchors it to
1465            self.menu_flyout(sub, spid, FL_RIGHT);
1466            self.wp.ctx.last_rect = rect;
1467        }
1468        true
1469    }
1470
1471    // .mf_open_sub(submenu, row id): open it as a child with the focus on its first row
1472    fn mf_open_sub(&mut self, sub: &'static [Mi], rid: u32) {
1473        let pid = menu_pid(sub, rid);
1474        self.popup_open(pid);
1475        self.pp_flag(pid, PPF_FOCUS);
1476    }
1477
1478    // ------------------------------------------------------------------ menu bar
1479
1480    // the menus of a menu bar (its MENU records)
1481    fn mb_menu(mb: &'static [Mi], i: usize) -> &'static [Mi] {
1482        match mb[i] {
1483            Mi::Menu(_, m) | Mi::Sub(_, m) => m,
1484            _ => &[],
1485        }
1486    }
1487
1488    // mb_open_index(menubar, base, n) -> index of the menu of the bar that is open (-1)
1489    fn mb_open_index(&self, mb: &'static [Mi], base: u32, n: usize) -> i32 {
1490        if self.wp.popup.pp_n == 0 {
1491            return -1;
1492        }
1493        let top = self.wp.popup.pp[0].id;
1494        (0..n).find(|&i| menu_pid(Self::mb_menu(mb, i), base.wrapping_add(i as u32)) == top).map_or(-1, |i| i as i32)
1495    }
1496
1497    // mb_open(menubar, base, i, focus): open menu i of the bar (replacing every popup)
1498    fn mb_open(&mut self, mb: &'static [Mi], base: u32, i: usize, focus: bool) {
1499        let pid = menu_pid(Self::mb_menu(mb, i), base.wrapping_add(i as u32));
1500        // the focus to return to: the bar item in menu bar mode, else the previous one
1501        let w = &mut self.wp.popup;
1502        let mut ret = self.wp.ctx.focus;
1503        if w.pp_n != 0 {
1504            ret = w.pp[0].ret;
1505        }
1506        if w.mb_mode {
1507            ret = base.wrapping_add(i as u32);
1508        }
1509        w.pp_n = 0;
1510        let e = self.pp_push(pid, 0);
1511        let e = &mut self.wp.popup.pp[e];
1512        e.ret = ret;
1513        if focus {
1514            e.flags |= PPF_FOCUS;
1515        }
1516    }
1517
1518    /// ui_menubar(menubar) -> EV_OPEN while one of its menus is open (None = the one of
1519    /// AppDesc::menubar).  WinUI MenuBar: subtle 32 DIP items, a flyout per menu.
1520    /// Keyboard: Alt (tap) or F10 enter / leave menu bar mode, Alt+mnemonic opens a menu,
1521    /// Left / Right move between the menus, Down / Enter open one, Esc steps back.
1522    #[cfg_attr(jfgui_dev, track_caller)]
1523    pub fn menubar(&mut self, menubar: Option<&'static [Mi]>) -> u32 {
1524        let _src = self.src_scope();
1525        let mb: &'static [Mi] = match menubar {
1526            Some(m) => m,
1527            None => self.desc.menubar,
1528        };
1529        if mb.is_empty() {
1530            return 0;
1531        }
1532        let a = (mb.as_ptr() as usize as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1533        let base = ((a >> 36) as u32 & 0x0FFF_FF00) | 0xC000_0000;
1534        // count the menus, collect the mnemonics
1535        let mut mn = [0u8; MB_MAX];
1536        let mut mpos = [0u8; MB_MAX];
1537        let mut n = 0usize;
1538        while n < mb.len() && n < MB_MAX {
1539            let Mi::Menu(label, _) = mb[n] else { break };
1540            let (_, m, pos) = strip_amp(label);
1541            mn[n] = m as u8;
1542            mpos[n] = pos as u8;
1543            n += 1;
1544        }
1545        if n == MB_MAX && matches!(mb.get(n), Some(Mi::Menu(..))) {
1546            self.limit_hit(super::ctx::LIM_MENUBAR);
1547        }
1548        if n == 0 {
1549            return 0;
1550        }
1551        let mut open = self.mb_open_index(mb, base, n);
1552        let fi0 = self.wp.ctx.focus.wrapping_sub(base);
1553        let mut fi: i32 = if (fi0 as usize) < n { fi0 as i32 } else { -1 };
1554        // menu bar mode ends when the focus went elsewhere
1555        if self.wp.popup.mb_mode && fi < 0 && self.wp.popup.pp_n == 0 {
1556            self.wp.popup.mb_mode = false;
1557        }
1558        'keys: {
1559            if self.modal_active() != 0 {
1560                break 'keys; // no menu keys while a dialog is open
1561            }
1562            let mut toggle = false;
1563            let k = self.key_first();
1564            let vk = k & 0xFFFF;
1565            let allm = k >> 16; // all modifiers (AltGr too)
1566            let m = allm & MOD3;
1567            // ---- Alt tap: Alt pressed and released without anything in between
1568            if vk == VK_MENU {
1569                self.wp.popup.alt_armed = allm & (MOD_CTRL | MOD_SHIFT | MOD_ALTGR) == 0;
1570            } else if vk != 0 || self.wp.ctx.pressed & BTN_MASK != 0 {
1571                self.wp.popup.alt_armed = false;
1572            } else if self.wp.popup.alt_armed && !self.key_down(VK_MENU) {
1573                self.wp.popup.alt_armed = false;
1574                if self.headless || plat::window_state() & plat::WST_ACTIVE != 0 {
1575                    toggle = true;
1576                }
1577            }
1578            if vk == VK_F10 && m == 0 {
1579                self.key(VK_F10);
1580                toggle = true;
1581            }
1582            if toggle {
1583                if !self.wp.popup.mb_mode && open < 0 {
1584                    // enter menu bar mode: the focus goes to the first menu
1585                    let w = &mut self.wp.popup;
1586                    let mut f = self.wp.ctx.focus;
1587                    if w.pp_n != 0 {
1588                        f = w.pp[0].ret;
1589                        w.pp_n = 0;
1590                    }
1591                    w.mb_prev = f;
1592                    w.mb_mode = true;
1593                    self.focus(base);
1594                    fi = 0;
1595                    open = -1;
1596                } else {
1597                    self.pp_dismiss_all();
1598                    open = -1;
1599                    fi = -1;
1600                }
1601                break 'keys;
1602            }
1603            // ---- Alt+mnemonic opens a menu
1604            if m & MOD_ALT != 0 {
1605                if m & MOD_CTRL != 0 {
1606                    break 'keys;
1607                }
1608                let i = mnem_find(&mn[..n], vk);
1609                if i < 0 {
1610                    break 'keys;
1611                }
1612                self.key(vk);
1613                if !self.wp.popup.mb_mode {
1614                    let w = &mut self.wp.popup;
1615                    let mut f = self.wp.ctx.focus;
1616                    if w.pp_n != 0 {
1617                        f = w.pp[0].ret;
1618                    }
1619                    w.mb_prev = f;
1620                    w.mb_mode = true;
1621                }
1622                self.mb_open(mb, base, i as usize, true);
1623                open = i;
1624                break 'keys;
1625            }
1626            // ---- menu bar mode without an open menu
1627            if fi < 0 || open >= 0 || vk == 0 || m & (MOD_CTRL | MOD_ALT) != 0 {
1628                break 'keys;
1629            }
1630            let mut i = fi;
1631            match vk {
1632                VK_LEFT | VK_RIGHT => {
1633                    self.key(vk);
1634                    if vk == VK_LEFT {
1635                        i = if fi - 1 < 0 { n as i32 - 1 } else { fi - 1 };
1636                    } else {
1637                        i = if fi + 1 < n as i32 { fi + 1 } else { 0 };
1638                    }
1639                    fi = i;
1640                    self.focus(base.wrapping_add(i as u32));
1641                    break 'keys;
1642                }
1643                VK_ESCAPE => {
1644                    self.key(vk);
1645                    self.wp.popup.mb_mode = false;
1646                    self.wp.ctx.focus = self.wp.popup.mb_prev;
1647                    self.repass();
1648                    fi = -1;
1649                    break 'keys;
1650                }
1651                VK_DOWN | VK_UP | VK_RETURN | VK_SPACE => {}
1652                _ => {
1653                    i = mnem_find(&mn[..n], vk);
1654                    if i < 0 {
1655                        break 'keys;
1656                    }
1657                }
1658            }
1659            self.key(vk);
1660            self.mb_open(mb, base, i as usize, true);
1661            open = i;
1662        }
1663        // a press outside the open menu closes it
1664        if open >= 0 && self.popup_catcher() {
1665            open = -1;
1666            fi = -1;
1667        }
1668        self.box_begin(base | 0xFF, BOX_H | BOX_XCENTER | PAD_4, 4);
1669        let st = self.style;
1670        let mut rects = [[0i32; 4]; MB_MAX];
1671        for i in 0..n {
1672            let Mi::Menu(label, _) = mb[i] else { break };
1673            let lab = strip_amp(label).0;
1674            let mut sp = Sp { label: Some(&lab), ..Sp::default() };
1675            let (w, h) = st.menubar_item_size(self, &mut sp);
1676            let r = self.place(w, h);
1677            rects[i] = r;
1678            let id = base.wrapping_add(i as u32); // item id
1679            let ev = self.behave(id, 0);
1680            let state = self.wp.ctx.last_state;
1681            // while a menu is open the items stay reachable above the popup catcher
1682            if open >= 0 {
1683                let prev = self.layer_set(L_POPUP);
1684                self.hit(id, 0);
1685                self.layer_set(prev);
1686            }
1687            // press: open / close; hover: switch menus while one is open
1688            let c = &self.wp.ctx;
1689            if ev & EV_HELD != 0 && c.pressed & (1 << BTN_LEFT) != 0 && c.hot == id {
1690                if open == i as i32 {
1691                    self.pp_dismiss_all();
1692                    open = -1;
1693                } else {
1694                    self.mb_open(mb, base, i, false);
1695                    open = i as i32;
1696                }
1697            } else if ev & EV_HOVER != 0 && open >= 0 && open != i as i32 && c.hot != self.wp.popup.hot_prev {
1698                // the pointer just entered it
1699                self.mb_open(mb, base, i, false);
1700                open = i as i32;
1701            }
1702            // paint (style)
1703            sp.place(self.wp.ctx.last_rect);
1704            sp.ev = ev;
1705            let mut f = 0;
1706            if open == i as i32 {
1707                f |= 1; // its menu is open
1708            }
1709            if fi == i as i32 {
1710                f |= 2; // the menu bar's current item
1711            }
1712            sp.flags = f;
1713            sp.value = -1; // mnemonic underline: byte index or -1
1714            if self.wp.popup.mb_mode && mpos[i] != 0xFF {
1715                sp.value = mpos[i] as i32;
1716            }
1717            st.menubar_item(self, &sp);
1718            if fi == i as i32 && open < 0 {
1719                self.focus_ring_draw(ev);
1720            }
1721            let mut s = state;
1722            if open == i as i32 {
1723                s |= SS_OPEN as u32;
1724            }
1725            self.wp.ctx.last_state = s;
1726            self.sem(K_MENU, id, Some(&lab), 0);
1727        }
1728        self.box_end();
1729        // the open menu's flyout
1730        if open >= 0 {
1731            let oi = open as usize;
1732            self.wp.ctx.last_rect = rects[oi];
1733            let menu = Self::mb_menu(mb, oi);
1734            let pid = menu_pid(menu, base.wrapping_add(oi as u32));
1735            self.menu_flyout(menu, pid, FL_BELOW);
1736            // Left / Right not used by the menu switch to the neighbour menu
1737            if self.wp.popup.pp_n == 1 && self.wp.popup.pp[0].id == pid {
1738                let d = if self.key(VK_LEFT) {
1739                    -1
1740                } else if self.key(VK_RIGHT) {
1741                    1
1742                } else {
1743                    0
1744                };
1745                if d != 0 {
1746                    let mut j = open + d;
1747                    if j < 0 {
1748                        j = n as i32 - 1;
1749                    }
1750                    if j >= n as i32 {
1751                        j = 0;
1752                    }
1753                    open = j;
1754                    self.mb_open(mb, base, j as usize, true);
1755                }
1756            }
1757        }
1758        if open >= 0 { EV_OPEN } else { 0 }
1759    }
1760
1761    // ------------------------------------------------------------------ context menus
1762
1763    // ctx_open(menu, x, y, focus): open `menu` as a runtime-drawn popup at (x, y) px
1764    fn ctx_open(&mut self, menu: &'static [Mi], x: i32, y: i32, focus: bool) {
1765        let pid = menu_pid(menu, CTX_SALT);
1766        let w = &mut self.wp.popup;
1767        let mut ret = self.wp.ctx.focus;
1768        if w.pp_n != 0 {
1769            ret = w.pp[0].ret;
1770            w.pp_n = 0;
1771        }
1772        let i = self.pp_push(pid, 0);
1773        let e = &mut self.wp.popup.pp[i];
1774        e.ret = ret;
1775        e.menu = Some(menu);
1776        e.flags |= PPF_AUTO | PPF_POINT | if focus { PPF_FOCUS } else { 0 };
1777        e.anchor = [x, y, 0, 0];
1778    }
1779
1780    /// ui_menu_popup(menu): open a menu at the pointer (the runtime draws it); for the
1781    /// menu key / Shift+F10 (an EV_RCLICK kbnav.rs gave the focused widget in this pass) at
1782    /// the widget's lower left corner with its first row focused
1783    #[cfg_attr(jfgui_dev, track_caller)]
1784    pub fn menu_popup(&mut self, menu: &'static [Mi]) {
1785        let _src = self.src_scope();
1786        self.wp.popup.ctx_target = 0;
1787        if let Some(r) = self.kb_ctx_rect() {
1788            self.ctx_open(menu, r[0], r[1] + r[3], true);
1789            return;
1790        }
1791        let (x, y) = (self.wp.ctx.mx, self.wp.ctx.my);
1792        self.ctx_open(menu, x, y, false);
1793    }
1794
1795    /// ui_context_menu(menu) -> EV_OPEN while `menu` is open for the last widget.  Opens on a
1796    /// right click (released over the widget), or with the menu key / Shift+F10 while the
1797    /// widget has the focus -- a list row: while the list has it and the row is selected --
1798    /// (then at its lower left corner, first row focused).
1799    #[cfg_attr(jfgui_dev, track_caller)]
1800    pub fn context_menu(&mut self, menu: &'static [Mi]) -> u32 {
1801        let _src = self.src_scope();
1802        let (id, r) = self.last_widget();
1803        if id == 0 {
1804            return 0;
1805        }
1806        let c = &self.wp.ctx;
1807        if c.hot == id && c.released & (1 << BTN_RIGHT) != 0 {
1808            let (x, y) = (c.mx, c.my);
1809            self.ctx_open(menu, x, y, false);
1810            self.wp.popup.ctx_target = id;
1811        } else if self.wp.ctx.focus == id || self.list_row_focused(id) {
1812            let mut open = self.key(VK_APPS);
1813            if !open {
1814                let k = self.key_first();
1815                if k & 0xFFFF == VK_F10 && (k >> 16) & MOD3 == MOD_SHIFT {
1816                    self.key(VK_F10);
1817                    open = true;
1818                }
1819            }
1820            if open {
1821                self.ctx_open(menu, r[0], r[1] + r[3], true);
1822                self.wp.popup.ctx_target = id;
1823            }
1824        }
1825        if self.pp_find(menu_pid(menu, CTX_SALT)) < 0 || self.wp.popup.ctx_target != id {
1826            return 0;
1827        }
1828        EV_OPEN
1829    }
1830
1831    /// ui_context_target -> id of the widget whose context menu is (or was last) open
1832    pub fn context_target(&self) -> u32 {
1833        self.wp.popup.ctx_target
1834    }
1835
1836    // ------------------------------------------------------------------ drop-down controls
1837
1838    /// ui_dropdown_button(id, label) -> EV_* | EV_OPEN while popup `id` is open (a click,
1839    /// Enter / Space, Alt+Down or F4 opens it; the app draws the flyout with
1840    /// flyout_begin(id, ..) right after).  next_flags ST_SUBTLE: flat tool bar look,
1841    /// ST_ICON_ONLY: icon + chevron, the label as tooltip (U1)
1842    #[cfg_attr(jfgui_dev, track_caller)]
1843    pub fn dropdown_button(&mut self, id: u32, label: &str) -> u32 {
1844        let _src = self.src_scope();
1845        let flags = self.take_flags();
1846        let icon = self.take_icon();
1847        let st = self.style;
1848        let mut sp = Sp { flags, icon, label: Some(label), ..Sp::default() };
1849        let (w, h) = st.dropdown_size(self, &mut sp);
1850        let (w, h) = self.ctrl_size(w, h, flags, false); // (theme) control height
1851        self.place(w, h);
1852        let mut hb = if flags & ST_NOFOCUS != 0 { 0 } else { HB_FOCUS };
1853        if flags & ST_DISABLED != 0 {
1854            hb |= HB_DISABLED;
1855        }
1856        let ev = self.behave(id, hb);
1857        let state = self.wp.ctx.last_state;
1858        let mut open = self.popup_is_open(id);
1859        if !open {
1860            let mut go = ev & EV_ACT != 0;
1861            if !go && ev & EV_FOCUS != 0 && self.open_key() {
1862                self.wp.ctx.focus_visible = true;
1863                go = true;
1864            }
1865            if go {
1866                self.popup_open(id);
1867                open = true;
1868                if self.wp.ctx.focus_visible {
1869                    self.pp_flag(id, PPF_FOCUS);
1870                }
1871            }
1872        }
1873        sp.place(self.wp.ctx.last_rect);
1874        sp.ev = ev;
1875        sp.value = open as i32;
1876        sp.value2 = (state == SS_DISABLED as u32) as i32;
1877        st.dropdown(self, &sp);
1878        self.focus_ring_draw(ev);
1879        self.wp.ctx.last_state = state | if open { SS_OPEN as u32 } else { 0 };
1880        self.sem(K_BUTTON, id, Some(label), 0);
1881        if flags & ST_ICON_ONLY != 0 && !open {
1882            self.tooltip(label);
1883        }
1884        if open { ev | EV_OPEN } else { ev }
1885    }
1886
1887    /// ui_split_button(id, label) -> EV_* of the main part | EV_OPEN while popup `id` is
1888    /// open.  The chevron part (widget id | 0x80000000; Alt+Down / F4 on the main part)
1889    /// opens it.
1890    #[cfg_attr(jfgui_dev, track_caller)]
1891    pub fn split_button(&mut self, id: u32, label: &str) -> u32 {
1892        let _src = self.src_scope();
1893        let flags = self.take_flags();
1894        let icon = self.take_icon();
1895        let st = self.style;
1896        let mut sp = Sp { icon, label: Some(label), ..Sp::default() };
1897        let (w, h) = st.split_size(self, &mut sp);
1898        let (w, h) = self.ctrl_size(w, h, flags, false); // (theme) control height
1899        let (mw, cw) = (sp.s[0], sp.s[1]);
1900        let whole = self.place(w, h);
1901        let dis = if flags & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
1902        // chevron part
1903        self.wp.ctx.last_rect[0] += mw;
1904        self.wp.ctx.last_rect[2] = cw;
1905        let evc = self.behave(id | 0x8000_0000, dis);
1906        let stc = self.wp.ctx.last_state;
1907        // main part
1908        self.wp.ctx.last_rect[0] = whole[0];
1909        self.wp.ctx.last_rect[2] = mw;
1910        let evm = self.behave(id, dis | HB_FOCUS);
1911        let stm = self.wp.ctx.last_state;
1912        let mut open = self.popup_is_open(id);
1913        if !open {
1914            if evc & EV_ACT != 0 {
1915                self.popup_open(id);
1916                open = true;
1917            } else if evm & EV_FOCUS != 0 && self.open_key() {
1918                self.wp.ctx.focus_visible = true;
1919                self.popup_open(id);
1920                self.pp_flag(id, PPF_FOCUS);
1921                open = true;
1922            }
1923        }
1924        sp.place(whole);
1925        sp.ev = evm; // main part
1926        sp.s[2] = evc as i32; // chevron part
1927        sp.value = open as i32;
1928        sp.value2 = (stm == SS_DISABLED as u32) as i32 | ((stc == SS_DISABLED as u32) as i32) << 1;
1929        sp.s[0] = mw;
1930        sp.s[1] = cw;
1931        st.split(self, &sp);
1932        let os = if open { SS_OPEN as u32 } else { 0 };
1933        // semantic records: chevron first, so the main part is the "last widget"
1934        self.wp.ctx.last_rect[0] = whole[0] + mw;
1935        self.wp.ctx.last_rect[2] = cw;
1936        self.wp.ctx.last_state = stc | os;
1937        self.sem(K_BUTTON, id | 0x8000_0000, None, 0);
1938        // focus ring and record of the main part
1939        self.wp.ctx.last_rect = whole;
1940        self.focus_ring_draw(evm);
1941        self.wp.ctx.last_rect[2] = mw;
1942        self.wp.ctx.last_state = stm | os;
1943        self.sem(K_BUTTON, id, Some(label), 0);
1944        self.wp.ctx.last_rect = whole;
1945        if open { evm | EV_OPEN } else { evm }
1946    }
1947
1948    /// ui_combobox(id, &mut sel, list) -> EV_*: EV_ACT when sel changed, EV_OPEN while the
1949    /// list is open.  WinUI ComboBox: 32 DIP, control fill, chevron; the list opens over the
1950    /// box with the selected row (accent pill) on it.  Keyboard: Up / Down change the
1951    /// selection, Enter / Space / Alt+Down / F4 open the list.  `list` is a STRLIST (it ends
1952    /// at the first empty string).
1953    #[cfg_attr(jfgui_dev, track_caller)]
1954    pub fn combobox(&mut self, id: u32, sel: &mut i32, list: &[&str]) -> u32 {
1955        let _src = self.src_scope();
1956        let flags = self.take_flags();
1957        let mut changed = false;
1958        // rows and the widest text
1959        self.ts_font(TS_BODY);
1960        let n = strlist_n(list);
1961        let mut widest = 0;
1962        for s in &list[..n] {
1963            let w = self.text.width(s);
1964            if w > widest {
1965                widest = w;
1966            }
1967        }
1968        let st = self.style;
1969        let mut sp = Sp { flags, value: widest, ..Sp::default() };
1970        let (w, h) = st.combo_size(self, &mut sp);
1971        let (w, h) = self.ctrl_size(w, h, flags, false); // (theme) control height
1972        let rect = self.place(w, h);
1973        let mut hb = HB_FOCUS;
1974        if flags & ST_DISABLED != 0 {
1975            hb |= HB_DISABLED;
1976        }
1977        let ev = self.behave(id, hb);
1978        let state = self.wp.ctx.last_state;
1979        let mut open = self.popup_is_open(id);
1980        let rowbase = (id.wrapping_mul(0x9E37_79B1) & 0x0FFF_FF00) | 0xD000_0000;
1981        if !open {
1982            let mut go = ev & EV_ACT != 0;
1983            if !go && ev & EV_FOCUS != 0 {
1984                if self.open_key() {
1985                    self.wp.ctx.focus_visible = true;
1986                    go = true;
1987                } else if self.key(VK_DOWN) {
1988                    let v = sel.wrapping_add(1);
1989                    if v < n as i32 {
1990                        *sel = v;
1991                        changed = true;
1992                        self.wp.ctx.focus_visible = true;
1993                    }
1994                } else if self.key(VK_UP) {
1995                    let v = sel.wrapping_sub(1);
1996                    if v >= 0 {
1997                        *sel = v;
1998                        changed = true;
1999                        self.wp.ctx.focus_visible = true;
2000                    }
2001                }
2002            }
2003            if go {
2004                self.popup_open(id);
2005                open = true;
2006                // focus the selected row once the list is drawn
2007                let i = self.pp_find(id);
2008                if i >= 0 {
2009                    let c = *sel;
2010                    if c >= 0 && c < n as i32 {
2011                        let fv = self.wp.ctx.focus_visible;
2012                        let e = &mut self.wp.popup.pp[i as usize];
2013                        e.focusid = rowbase.wrapping_add(c as u32 + 1);
2014                        if !fv {
2015                            e.flags |= PPF_QUIET;
2016                        }
2017                    }
2018                }
2019            }
2020        }
2021        // paint (style)
2022        let mut sp = Sp { flags, label: strlist_at(list, *sel), ev, value2: (state == SS_DISABLED as u32) as i32, ..Sp::default() };
2023        sp.place(rect);
2024        st.combo(self, &sp);
2025        self.focus_ring_draw(ev);
2026        // the list
2027        if open {
2028            let r = if *sel < 0 || *sel >= n as i32 { 0 } else { *sel };
2029            let ih = st.menu_item_h(self);
2030            let pad = st.flyout_pad(self);
2031            self.wp.popup.fl_offy = r.wrapping_mul(ih) + pad;
2032            // anchor: the box widened by the flyout padding so the texts line up
2033            self.wp.ctx.last_rect = [rect[0] - pad, rect[1], rect[2] + pad * 2, rect[3]];
2034            if self.flyout_begin(id, FL_OVER | FL_MINW) {
2035                for (k, s) in list[..n].iter().enumerate() {
2036                    let k = k as i32;
2037                    let mut mf = MIF_LIST;
2038                    if k == *sel {
2039                        mf |= MIF_SELECTED;
2040                    }
2041                    let e = self.mitem(rowbase.wrapping_add(k as u32 + 1), s, 0, None, mf);
2042                    if e & EV_ACT != 0 {
2043                        if k != *sel {
2044                            *sel = k;
2045                            changed = true;
2046                        }
2047                        let i = self.pp_find(id);
2048                        if i >= 0 {
2049                            self.pp_close_to(i as u32);
2050                        }
2051                    }
2052                }
2053            }
2054            self.flyout_end();
2055            self.wp.ctx.last_rect = rect;
2056        }
2057        let lab = strlist_at(list, *sel);
2058        let open = self.popup_is_open(id);
2059        self.wp.ctx.last_state = state | if open { SS_OPEN as u32 } else { 0 };
2060        let v = *sel;
2061        self.sem(K_COMBO, id, lab, v).max = n as i32;
2062        let mut r = ev & !EV_ACT;
2063        if changed {
2064            r |= EV_ACT;
2065            self.repass();
2066        }
2067        if open {
2068            r |= EV_OPEN;
2069        }
2070        r
2071    }
2072
2073    // ------------------------------------------------------------------ tooltips
2074
2075    /// ui_tooltip(s): post-modifier: show s as the tooltip of the last widget after 500 ms of
2076    /// hover (at once when another tooltip hid less than 300 ms ago); hidden on press, key or
2077    /// leave.  Icon-only buttons call it with their label.
2078    #[cfg_attr(jfgui_dev, track_caller)]
2079    pub fn tooltip(&mut self, s: &str) {
2080        let _src = self.src_scope();
2081        if self.wp.popup.tip_suppress {
2082            self.wp.popup.tip_suppress = false;
2083            return;
2084        }
2085        self.tip_core(s);
2086    }
2087
2088    /// ui_tooltip_part(s, part): post-modifier like tooltip(s) for one part of the last widget
2089    /// (an item on a canvas: a cable, a port).  The delay starts again when `part` changes (at
2090    /// once when a tooltip hid less than 300 ms ago, as between toolbar buttons); a press hides
2091    /// it until the pointer is over another part; part 0 shows nothing.
2092    #[cfg_attr(jfgui_dev, track_caller)]
2093    pub fn tooltip_part(&mut self, s: &str, part: u64) {
2094        let _src = self.src_scope();
2095        let (id, _) = self.last_widget();
2096        if id == 0 {
2097            return;
2098        }
2099        if self.wp.ctx.hot != id || part == 0 {
2100            if self.wp.popup.tip_id == id {
2101                self.tip_reset();
2102            }
2103            self.wp.popup.tip_part = 0;
2104            return;
2105        }
2106        if self.wp.popup.tip_part != part {
2107            // another part: its tooltip starts over, a press on the last one no longer counts
2108            let w = &mut self.wp.popup;
2109            w.tip_part = part;
2110            if w.tip_block == id {
2111                w.tip_block = 0;
2112            }
2113            if w.tip_id == id {
2114                self.tip_reset();
2115            }
2116        }
2117        self.tip_core(s);
2118    }
2119
2120    // tip_core(s)
2121    fn tip_core(&mut self, s: &str) {
2122        let (id, _) = self.last_widget();
2123        if id == 0 {
2124            return;
2125        }
2126        let w = &mut self.wp.popup;
2127        if self.wp.ctx.hot != id {
2128            if w.tip_block == id {
2129                w.tip_block = 0;
2130            }
2131            if w.tip_id == id {
2132                self.tip_reset();
2133            }
2134            return;
2135        }
2136        if w.tip_block == id {
2137            return;
2138        }
2139        // (a finger's long press is the right button: the tooltip of a resting finger stays)
2140        let press = if self.m.touch.on { 1 << BTN_LEFT } else { BTN_MASK };
2141        if self.wp.ctx.pressed & press != 0 || self.key_first() != 0 {
2142            let w = &mut self.wp.popup;
2143            w.tip_block = id;
2144            w.tip_id = 0;
2145            w.tip_shown = 0;
2146            return;
2147        }
2148        let now = self.now_ms;
2149        let w = &mut self.wp.popup;
2150        if w.tip_id != id {
2151            w.tip_id = id;
2152            w.tip_t0 = now;
2153            if w.tip_shown != 0 || (w.tip_hid_ok && now.wrapping_sub(w.tip_hid_t) < TIP_QUICK) {
2154                w.tip_t0 = now.wrapping_sub(TIP_DELAY); // another tooltip is (just) up: at once
2155            }
2156        }
2157        let el = now.wrapping_sub(w.tip_t0);
2158        if el < TIP_DELAY {
2159            self.timer(UIT_TOOLTIP, TIP_DELAY - el);
2160            return;
2161        }
2162        w.tip_shown = id;
2163        w.tip_hid_ok = false;
2164        self.tip_draw(s);
2165    }
2166
2167    /// a tooltip is up in the live window (touch.rs: it stays a while after the finger lifted)
2168    pub(crate) fn tip_showing(&self) -> bool {
2169        self.wp.popup.tip_shown != 0
2170    }
2171
2172    // tip_reset: stop tracking the hovered widget (remember when a shown tooltip hid)
2173    fn tip_reset(&mut self) {
2174        let now = self.now_ms;
2175        let w = &mut self.wp.popup;
2176        w.tip_id = 0;
2177        if w.tip_shown != 0 {
2178            w.tip_shown = 0;
2179            w.tip_hid_t = now;
2180            w.tip_hid_ok = true;
2181        }
2182    }
2183
2184    // tip_draw(s): the tooltip on L_TIP: below the pointer (above when there is no room),
2185    // flyout colours, 4 DIP corners, caption text (max 300 DIP)
2186    fn tip_draw(&mut self, s: &str) {
2187        let (lr, ls) = (self.wp.ctx.last_rect, self.wp.ctx.last_state);
2188        let prev = self.layer_set(L_TIP);
2189        let st = self.style;
2190        let mut sp = Sp { label: Some(s), ..Sp::default() };
2191        let (w, h) = st.tooltip_size(self, &mut sp);
2192        // x centred on the pointer, y below it; inside the popup area (U3, §6: beyond the
2193        // edges of the window, pp_bounds)
2194        let area = self.pp_bounds(w, h);
2195        let (mx, my) = (self.wp.ctx.mx, self.wp.ctx.my);
2196        let mut x = mx.wrapping_sub(((w as u32) >> 1) as i32);
2197        let mut y = my + sp.s[1]; // gap below the pointer
2198        if y + h > area[3] {
2199            y = my - sp.s[2] - h; // gap above it
2200        }
2201        if x > area[2] - w {
2202            x = area[2] - w;
2203        }
2204        if x < area[0] {
2205            x = area[0];
2206        }
2207        if y > area[3] - h {
2208            y = area[3] - h;
2209        }
2210        if y < area[1] {
2211            y = area[1];
2212        }
2213        // (§6) where the platform put it (Wayland)
2214        self.wp.popup.tip_anchor = [mx, my, 1, 1];
2215        self.wp.popup.tip_nat = [x, y];
2216        let (dx, dy) = self.pp_nudge(POPUP_MAX as u32, [x, y, w, h]);
2217        let (x, y) = (x + dx, y + dy);
2218        self.wp.popup.tip_rect = [x, y, w, h]; // (U3) for an OS popup window
2219        self.wp.popup.tip_seen = self.frame;
2220        sp.place([x, y, w, h]);
2221        st.tooltip(self, &sp);
2222        // semantic record (id 0) for the dump and scripts
2223        self.wp.ctx.last_rect = [x, y, w, h];
2224        self.wp.ctx.last_state = 0;
2225        self.sem(K_TEXT, 0, Some(s), 0);
2226        self.layer_set(prev);
2227        self.wp.ctx.last_rect = lr;
2228        self.wp.ctx.last_state = ls;
2229    }
2230
2231    // ------------------------------------------------------------------ runtime hook
2232
2233    /// ui_accel_dispatch: (start of uiw_pass_end) draws runtime-owned menus (menu_popup /
2234    /// context_menu), closes popups nobody drew in this pass, ends the tooltip tracking when
2235    /// the pointer left, and turns an unconsumed key chord that matches a command
2236    /// accelerator into cmd_run(id, 0)
2237    pub(crate) fn accel_dispatch(&mut self) {
2238        // runtime-drawn context menu (level 0)
2239        let frame = self.frame;
2240        let w = &self.wp.popup;
2241        if w.pp_n != 0 && w.pp[0].flags & PPF_AUTO != 0 && w.pp[0].seen != frame {
2242            let (menu, id) = (w.pp[0].menu.unwrap_or(&[]), w.pp[0].id);
2243            self.menu_flyout(menu, id, FL_POINT);
2244        }
2245        // popups not drawn in this pass (and not opened in it) are gone
2246        for i in 0..self.wp.popup.pp_n {
2247            let e = &self.wp.popup.pp[i as usize];
2248            if e.seen != frame && e.born != frame {
2249                self.pp_close_to(i);
2250                break;
2251            }
2252        }
2253        self.wp.popup.hot_prev = self.wp.ctx.hot;
2254        let t = self.wp.popup.tip_id;
2255        if t != 0 && t != self.wp.ctx.hot {
2256            self.tip_reset();
2257        }
2258        // accelerators (not in dialog windows)
2259        if self.pp_noaccel() {
2260            return;
2261        }
2262        let k = self.key_first();
2263        if k == 0 {
2264            return;
2265        }
2266        let vk = k & 0xFFFF;
2267        let mods = (k >> 16) & MOD3;
2268        if vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU {
2269            return;
2270        }
2271        // plain keys belong to a focused text box
2272        if mods & (MOD_CTRL | MOD_ALT) == 0 && vk.wrapping_sub(VK_F1) > 23 && self.focus_is_text() {
2273            return;
2274        }
2275        let Some(c) = self.accel_find(vk, mods) else { return };
2276        self.key(vk);
2277        if self.cmd_flags(c.id) & (CF_DISABLED | CF_HIDDEN) != 0 {
2278            return;
2279        }
2280        self.pp_dismiss_all();
2281        self.cmd_run(c.id, 0);
2282    }
2283}