1use 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
68const POPUP_MAX: usize = 6;
71pub(crate) const PPF_FOCUS: u32 = 1; const PPF_AUTO: u32 = 2; const PPF_POINT: u32 = 4; const PPF_QUIET: u32 = 8; const PPF_TWOCOL: u32 = 16; const PPF_SCROLLED: u32 = 32; const FLN_MAX: usize = 8; const CST_LIMIT: usize = 1536; const MENU_MAX: usize = 250;
83const MB_COMPACT_LABEL: &str = "Menü";
85const MB_COMPACT_W: i32 = 44;
86const MB_FILL_MIN: i32 = 64; const MB_MAX: usize = 16; const LAB_MAX: usize = 256;
89pub(crate) const CATCH_ID: u32 = 0x7FFE_0C0C; const CTX_SALT: u32 = 0x00C0_FFEE;
91const BOXID_XOR: u32 = 0x6C00_0000; const FLS_XOR: u32 = 0x0100_0000; const 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#[derive(Clone, Copy, Debug)]
100pub struct Command {
101 pub id: u32,
103 pub icon: u32,
105 pub label: &'static str,
107 pub key: u16,
109 pub mods: u8,
111 pub flags: u8,
113}
114
115impl Command {
116 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#[derive(Clone, Copy, Debug)]
124#[non_exhaustive]
125pub enum Mi {
126 Cmd(u32),
128 Sep,
130 Sub(&'static str, &'static [Mi]),
132 Menu(&'static str, &'static [Mi]),
134 Item(u32, &'static str, u32),
136 List(u32, u16, &'static str),
140}
141
142impl Mi {
143 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 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#[derive(Clone, Copy, Default)]
161struct Pp {
162 id: u32,
163 flags: u32,
165 anchor: [i32; 4],
167 rect: [i32; 4],
169 menu: Option<&'static [Mi]>,
171 ret: u32,
173 kind: u32,
177 key: [i32; 2],
178 nat: [i32; 2],
179 seen: u32,
181 born: u32,
182 hit: u32,
184 hitend: u32,
185 focusid: u32,
187}
188
189#[derive(Clone, Copy, Default)]
191struct Fln {
192 open: bool,
193 idx: u32,
194 layer: u32,
195 dis: u32,
196 level: u32,
197 scroll: bool,
199 clip: [i32; 4],
200}
201
202#[derive(Default)]
204pub(crate) struct S {
205 cst: BTreeMap<u32, u32>,
207 cst_warned: bool,
208 accel_order: u32,
210 pub(crate) lists: Vec<(u32, Vec<String>)>,
212 pub(crate) list_empty: Vec<(u32, String)>,
214 compact: Vec<((usize, usize), &'static [Mi])>,
216}
217
218#[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 tip_part: u64,
242 hot_prev: u32,
243 tip_rect: [i32; 4],
245 tip_seen: u32,
246 tip_anchor: [i32; 4],
248 tip_nat: [i32; 2],
249}
250
251fn 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
285fn 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
318fn 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
327fn 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
340fn 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
347fn 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
352fn is_alnum_vk(vk: u32) -> bool {
354 vk.wrapping_sub(0x30) <= 9 || vk.wrapping_sub(0x41) <= 25
355}
356
357fn strlist_n(list: &[&str]) -> usize {
359 list.iter().position(|s| s.is_empty()).unwrap_or(list.len())
360}
361
362fn 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
367static 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, "["), (VK_OEM_6, "]"),
392 (VK_MULTIPLY, "Num *"),
393 (VK_DIVIDE, "Num /"),
394 (VK_DECIMAL, "Num ,"),
395];
396
397fn 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); }
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 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 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 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 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 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 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() } else {
482 vk == VK_DOWN && m == MOD_ALT
483 };
484 if yes {
485 self.key(vk);
486 }
487 yes
488 }
489
490 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 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 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 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 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 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 fn pp_popup_area(&mut self) -> Option<([i32; 4], bool)> {
544 self.uiw_popup_area()
545 }
546
547 fn pp_noaccel(&self) -> bool {
549 self.m.win.noaccel
550 }
551
552 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 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 pub fn popup_is_open(&self, id: u32) -> bool {
579 self.pp_find(id) >= 0
580 }
581
582 pub fn popup_count(&self) -> u32 {
584 self.wp.popup.pp_n
585 }
586
587 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 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 pub fn popup_close(&mut self) {
619 self.pp_dismiss_all();
620 }
621
622 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 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 #[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 {
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 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 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; 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 let area = self.pp_bounds(fw, fh);
721 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 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 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; }
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 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 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 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 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 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 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 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 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 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 pub fn popup_ext(&self, i: u32) -> Option<[i32; 4]> {
879 self.popup_item(i, false)
880 }
881
882 pub fn popup_rect(&self, i: u32) -> Option<[i32; 4]> {
886 self.popup_item(i, true)
887 }
888
889 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); if anywhere {
927 return Some(r);
928 }
929 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 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 #[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(); }
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 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 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 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 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 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 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, };
1093 self.m.popup.cst.insert(id, flags);
1094 if cur != flags {
1095 self.repass = true;
1096 }
1097 }
1098
1099 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 pub fn cmd_label(&self, id: u32) -> String {
1111 strip_amp(self.cmd_find(id).map_or("", |c| c.label)).0
1112 }
1113
1114 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 pub fn accel_order_set(&mut self, order: u32) {
1124 self.m.popup.accel_order = order;
1125 }
1126
1127 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"); 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 #[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 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 fn mitem(&mut self, id: u32, label: &str, icon: u32, accel: Option<&str>, mflags: u32) -> u32 {
1203 let mut mf = mflags;
1204 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 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 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 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); 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); 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 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 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 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); }
1485 continue;
1486 }
1487 };
1488 if ev & EV_ACT != 0 {
1490 activated = true;
1491 }
1492 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: {
1502 if activated || self.wp.popup.pp_n.wrapping_sub(1) != level {
1503 break 'keys;
1504 }
1505 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 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 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 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; self.menu_flyout(sub, spid, FL_RIGHT);
1554 self.wp.ctx.last_rect = rect;
1555 }
1556 true
1557 }
1558
1559 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 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 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 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 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 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 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 #[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 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 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 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 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; }
1714 let mut toggle = false;
1715 let k = self.key_first();
1716 let vk = k & 0xFFFF;
1717 let allm = k >> 16; let m = allm & MOD3;
1719 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 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 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 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 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); let ev = self.behave(id, 0);
1835 let state = self.wp.ctx.last_state;
1836 if open >= 0 {
1838 let prev = self.layer_set(L_POPUP);
1839 self.hit(id, 0);
1840 self.layer_set(prev);
1841 }
1842 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 self.mb_open(mb, base, i, false);
1855 open = i as i32;
1856 }
1857 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; }
1864 if fi == i as i32 {
1865 f |= 2; }
1867 sp.flags = f;
1868 sp.value = -1; 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 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 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 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); 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 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 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 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 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 #[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 #[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 #[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 pub fn context_target(&self) -> u32 {
2041 self.wp.popup.ctx_target
2042 }
2043
2044 #[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); 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 #[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); 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 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 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; sp.s[2] = evc as i32; 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 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 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 #[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 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); 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 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 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 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 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 #[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 #[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 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 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 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); }
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 pub(crate) fn tip_showing(&self) -> bool {
2377 self.wp.popup.tip_shown != 0
2378 }
2379
2380 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 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 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]; if y + h > area[3] {
2407 y = my - sp.s[2] - h; }
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 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]; self.wp.popup.tip_seen = self.frame;
2428 sp.place([x, y, w, h]);
2429 st.tooltip(self, &sp);
2430 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 pub(crate) fn accel_dispatch(&mut self) {
2446 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 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 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 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}