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