Skip to main content

ui/
kbnav.rs

1// kbnav -- keyboard navigation between widgets: the Tab order with focus groups, panes and
2// windows (F6), splitters (F8), the menu key on every widget, the focus after its widget went
3// away, the first control of a new dialog window
4//
5// App-facing:
6//   focus_group_begin(id) / focus_group_end()   the focusable widgets in between are one Tab
7//        stop (a tool bar, a row of segments, a grid of buttons or swatches): the arrows move
8//        inside (one row or column: Left / Up the previous, Right / Down the next, wrapping;
9//        a grid: the nearest member in the arrow's direction), Home / End the first / last.
10//        Tab enters a group at its current member (HB_CURRENT: the checked tool button, the
11//        selected swatch), else at the member focused last, else at its first.  Radios with
12//        one variable, tool buttons with one variable and the swatches of one box are groups
13//        by themselves (radios: the selection follows the arrows).  Text boxes never join.
14//   splitter_keys(ev, along_x) -> DIP            the arrows on a focused splitter of the app
15//   group_arrived(id) -> bool                    the arrows just focused this member (once):
16//        selection-follows-focus widgets (tabs, radios) select it
17//   focus_pane_begin(id) / focus_pane_end()     a pane of the window: F6 / Shift+F6 go to the
18//        next / previous pane (to the widget focused there last, else its first Tab stop),
19//        past the last one on to the next window -- tool windows too, which then get the keys
20//        until Esc, F6 or a click elsewhere; Ctrl+F6 / Ctrl+Shift+F6 switch the windows
21//        directly.  The document area and every group of a dock area and both sides of a
22//        split view are panes by themselves.
23// Built in: Tab / Shift+Tab (Ctrl+Tab stays the app's); F8 / Shift+F8 the window's splitters
24// (HB_SPLITTER: the arrows move the focused one by 8 DIP, Shift+arrows by 1, Home / End to the
25// ends; Enter, Esc or F8 past the last return to the widget focused before); the menu key /
26// Shift+F10 that no widget took: a right click of the focused widget (EV_RCLICK, a focused
27// list passes it to its selected row; menu_popup then opens at the widget with its first row
28// focused); Tab after the focused widget went away goes on from its place; a dialog window
29// shown starts with the focus on its first control (the ring appears with the first key).
30// An app accelerator on F6 / F8 wins over these keys.
31// Tool windows that borrow their owner's keys (win.rs, U3) keep the navigation keys and hand
32// the others on (kb_take_rest).
33
34use alloc::vec::Vec;
35
36use super::consts::*;
37use super::ctx::Hit;
38use super::keys::*;
39use super::Ui;
40
41const MEM_MAX: usize = 64; // remembered groups / panes per window (a cache: the oldest goes)
42const GRIP_STEP: i32 = 8; // DIP per arrow on a splitter
43const MOD3: u32 = MOD_SHIFT | MOD_CTRL | MOD_ALT;
44
45/// per-window state (ctx::W): the groups and panes open in this pass, the memory of the
46/// member / widget focused last in a group / pane, where the focus was among the Tab stops
47#[derive(Default)]
48pub(crate) struct W {
49    groups: Vec<u32>,
50    panes: Vec<u32>,
51    /// the automatic group of the next focusable box (radio, tool button, swatch)
52    next: u32,
53    /// > 0: focusable boxes are no Tab stops (the rows of a list that are not selected)
54    notab: u32,
55    auto_addr: usize,
56    auto_serial: u32,
57    auto_seen: u64,
58    gmem: Vec<(u32, u32)>,
59    pmem: Vec<(u32, u32)>,
60    /// the focus, the Tab stop after it and the one before it (last pass that had it)
61    anchor: [u32; 3],
62    /// a dialog window was shown: its first control gets the focus; that control while the
63    /// focus stays there (Esc in a text box closes the dialog at once, edit.rs)
64    pub(crate) autofocus: bool,
65    pub(crate) soft: u32,
66    /// F8: the widget focused before the splitters
67    split_ret: u32,
68}
69
70impl W {
71    /// the member focused last in each group (the inspector's Tab order: inspect.rs)
72    pub(crate) fn gmem(&self) -> &[(u32, u32)] {
73        &self.gmem
74    }
75
76    /// a pass of the window begins
77    pub(crate) fn pass_begin(&mut self) {
78        self.groups.clear();
79        self.panes.clear();
80        self.next = 0;
81        self.notab = 0;
82        self.auto_addr = 0;
83        self.auto_serial = 0;
84        self.auto_seen = 0;
85    }
86
87    /// hit_keys(hbflags) -> (group, pane, HB_NOTAB or 0) of a box hit() registers
88    pub(crate) fn hit_keys(&mut self, hb: u32) -> (u32, u32, u32) {
89        let pane = self.panes.last().copied().unwrap_or(0);
90        if hb & HB_FOCUS == 0 {
91            return (0, pane, 0);
92        }
93        let g = if self.next != 0 {
94            core::mem::take(&mut self.next)
95        } else if hb & HB_TEXT != 0 {
96            0
97        } else {
98            self.groups.last().copied().unwrap_or(0)
99        };
100        (g, pane, if self.notab != 0 { HB_NOTAB } else { 0 })
101    }
102}
103
104/// shared state (Mods)
105#[derive(Default)]
106pub(crate) struct S {
107    /// the widget that gets the menu key as a right click in the next pass
108    pub(crate) ctx: u32,
109    /// the pass that asked for it (a widget that never calls behave lets it expire)
110    ctx_set: u32,
111    ctx_frame: u32,
112    ctx_rect: [i32; 4],
113    /// a group member the arrows just focused (a radio selects itself)
114    moved: u32,
115    /// the keys went to a tool window by F6 / Ctrl+F6 (Esc gives them back)
116    pub(crate) tool: bool,
117    /// Space on a focused list: the first check box / switch of the selected row acts
118    pub(crate) toggle: bool,
119}
120
121fn mem_get(m: &[(u32, u32)], k: u32) -> u32 {
122    m.iter().find(|e| e.0 == k).map_or(0, |e| e.1)
123}
124
125fn mem_put(m: &mut Vec<(u32, u32)>, k: u32, v: u32) {
126    if let Some(i) = m.iter().position(|e| e.0 == k) {
127        if m[i].1 == v {
128            return;
129        }
130        m.remove(i);
131    }
132    if m.len() >= MEM_MAX {
133        m.remove(0);
134    }
135    m.push((k, v));
136}
137
138// focusable(h): a box the keyboard may focus
139fn focusable(h: &Hit) -> bool {
140    h.flags as u32 & (HB_FOCUS | HB_DISABLED) == HB_FOCUS
141}
142
143/// stops(list, focus, gmem) -> (the indexes of the Tab stops in order, their layer): the
144/// focusable boxes of the top layer that has any, without HB_NOTAB, one per focus group (the
145/// focused member, else the current one, else the one focused last, else the first)
146pub(crate) fn stops(list: &[Hit], focus: u32, gmem: &[(u32, u32)]) -> (Vec<usize>, i32) {
147    let tab = |h: &Hit| focusable(h) && h.flags as u32 & HB_NOTAB == 0 && !h.continuation;
148    let top = list.iter().filter(|h| tab(h)).map(|h| h.layer as i32).max().unwrap_or(-1);
149    let mut out = Vec::new();
150    if top < 0 {
151        return (out, top);
152    }
153    let cand = |h: &Hit| tab(h) && h.layer as i32 == top;
154    // (group, index, rank): rank 0 focused, 1 current, 2 remembered, 3 first
155    let mut gs: Vec<(u32, usize, u8)> = Vec::new();
156    for (i, h) in list.iter().enumerate() {
157        if !cand(h) || h.group == 0 {
158            continue;
159        }
160        let rank = if focus != 0 && h.id == focus {
161            0
162        } else if h.flags as u32 & HB_CURRENT != 0 {
163            1
164        } else if mem_get(gmem, h.group) == h.id {
165            2
166        } else {
167            3
168        };
169        match gs.iter_mut().find(|e| e.0 == h.group) {
170            Some(e) => {
171                if rank < e.2 {
172                    e.1 = i;
173                    e.2 = rank;
174                }
175            }
176            None => gs.push((h.group, i, rank)),
177        }
178    }
179    for (i, h) in list.iter().enumerate() {
180        if cand(h) && (h.group == 0 || gs.iter().any(|e| e.0 == h.group && e.1 == i)) {
181            out.push(i);
182        }
183    }
184    // the stops of a pane stay together, at the place of its first one (a dock area emits
185    // every tab strip before the panels' contents); the window's root keeps the emission order
186    let mut first: Vec<(u32, usize)> = Vec::new();
187    for (k, &i) in out.iter().enumerate() {
188        let p = list[i].pane;
189        if p != 0 && !first.iter().any(|e| e.0 == p) {
190            first.push((p, k));
191        }
192    }
193    if !first.is_empty() {
194        let mut keyed: Vec<(usize, usize, usize)> = out
195            .iter()
196            .enumerate()
197            .map(|(k, &i)| {
198                let p = list[i].pane;
199                (if p == 0 { k } else { first.iter().find(|e| e.0 == p).map_or(k, |e| e.1) }, k, i)
200            })
201            .collect();
202        keyed.sort_unstable();
203        out = keyed.into_iter().map(|e| e.2).collect();
204    }
205    (out, top)
206}
207
208// span_gap(a0, a1, b0, b1): the distance between two intervals (0 when they overlap)
209fn span_gap(a0: i32, a1: i32, b0: i32, b1: i32) -> i32 {
210    if b1 <= a0 {
211        a0 - b1
212    } else if b0 >= a1 {
213        b0 - a1
214    } else {
215        0
216    }
217}
218
219// nearest(members, cur, vk): the member nearest to members[cur] in the arrow's direction
220fn nearest(ms: &[(u32, [i32; 4])], cur: usize, vk: u32) -> Option<usize> {
221    let r = ms[cur].1;
222    let (cx, cy) = ((r[0] + r[2]) / 2, (r[1] + r[3]) / 2);
223    let mut best: Option<(i64, usize)> = None;
224    for (i, m) in ms.iter().enumerate() {
225        if i == cur {
226            continue;
227        }
228        let q = m.1;
229        let (qx, qy) = ((q[0] + q[2]) / 2, (q[1] + q[3]) / 2);
230        let (along, gap) = match vk {
231            VK_LEFT => (cx - qx, span_gap(r[1], r[3], q[1], q[3])),
232            VK_RIGHT => (qx - cx, span_gap(r[1], r[3], q[1], q[3])),
233            VK_UP => (cy - qy, span_gap(r[0], r[2], q[0], q[2])),
234            _ => (qy - cy, span_gap(r[0], r[2], q[0], q[2])),
235        };
236        if along <= 0 {
237            continue;
238        }
239        let score = along as i64 + gap as i64 * 4;
240        if best.is_none_or(|b| score < b.0) {
241            best = Some((score, i));
242        }
243    }
244    best.map(|b| b.1)
245}
246
247// what F6 does in a window
248enum PaneMove {
249    None,
250    Focus(u32),
251    Window(bool),
252}
253
254impl Ui {
255    // ------------------------------------------------------------------ app API
256
257    /// focus_group_begin(id): the focusable widgets up to focus_group_end are one Tab stop,
258    /// the arrows move inside (id 0: the current box's id)
259    pub fn focus_group_begin(&mut self, id: u32) {
260        let k = if id != 0 { id } else { self.box_id().wrapping_mul(0x9E37_79B1) | 1 };
261        self.wp.ctx.kb.groups.push(k);
262    }
263
264    /// focus_group_end
265    pub fn focus_group_end(&mut self) {
266        self.wp.ctx.kb.groups.pop();
267    }
268
269    /// focus_pane_begin(id): the widgets up to focus_pane_end are a pane (F6 / Shift+F6)
270    pub fn focus_pane_begin(&mut self, id: u32) {
271        self.wp.ctx.kb.panes.push(id);
272    }
273
274    /// focus_pane_end
275    pub fn focus_pane_end(&mut self) {
276        self.wp.ctx.kb.panes.pop();
277    }
278
279    // ------------------------------------------------------------------ for the widgets
280
281    /// kb_auto_group(addr, mine): the next focusable box joins the group of the variable at
282    /// addr (radios, tool buttons); a value seen before starts a new group (a variable the
283    /// next group reuses).  Inside an explicit group nothing changes.
284    pub(crate) fn kb_auto_group(&mut self, addr: usize, mine: i32) {
285        let k = &mut self.wp.ctx.kb;
286        if !k.groups.is_empty() {
287            return;
288        }
289        let bit = 1u64 << (mine as u32 & 63);
290        if addr != k.auto_addr || k.auto_seen & bit != 0 {
291            k.auto_addr = addr;
292            k.auto_serial = k.auto_serial.wrapping_add(1);
293            k.auto_seen = 0;
294        }
295        k.auto_seen |= bit;
296        let h = (addr as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ (k.auto_serial as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F);
297        k.next = ((h >> 32) as u32) | 0x8000_0001;
298    }
299
300    /// kb_box_group(salt): the next focusable box joins the group of the current box
301    /// (swatches)
302    pub(crate) fn kb_box_group(&mut self, salt: u32) {
303        if !self.wp.ctx.kb.groups.is_empty() {
304            return;
305        }
306        let b = self.box_id();
307        self.wp.ctx.kb.next = (b.wrapping_mul(0x9E37_79B1) ^ salt) | 0x8000_0001;
308    }
309
310    /// kb_chord(vk, mods): vk with exactly these Shift / Ctrl / Alt was pressed in this pass
311    /// and not taken yet (consumes it; Caps / Num Lock do not count)
312    pub(crate) fn kb_chord(&mut self, vk: u32, mods: u32) -> bool {
313        let q = &mut self.wp.ctx.keyq;
314        match q.iter().position(|&k| (k as i32) >= 0 && k & 0xFFFF == vk && (k >> 16) & MOD3 == mods) {
315            Some(i) => {
316                q[i] |= 0x8000_0000;
317                true
318            }
319            None => false,
320        }
321    }
322
323    /// group_arrived(id): the arrow keys of its focus group just moved the focus to widget id
324    /// (true once; a widget whose selection follows the focus -- tabs, radios -- selects
325    /// itself then)
326    pub fn group_arrived(&mut self, id: u32) -> bool {
327        self.kb_moved(id)
328    }
329
330    /// kb_moved(id): the arrows of its group just focused the widget (once)
331    pub(crate) fn kb_moved(&mut self, id: u32) -> bool {
332        if id != 0 && self.m.kbnav.moved == id {
333            self.m.kbnav.moved = 0;
334            return true;
335        }
336        false
337    }
338
339    /// kb_notab(on): the focusable boxes up to kb_notab(false) are no Tab stops
340    pub(crate) fn kb_notab(&mut self, on: bool) {
341        let k = &mut self.wp.ctx.kb;
342        if on {
343            k.notab += 1;
344        } else if k.notab > 0 {
345            k.notab -= 1;
346        }
347    }
348
349    /// kb_ctx_take -> EV_RCLICK: behave met the widget the menu key was meant for
350    pub(crate) fn kb_ctx_take(&mut self) -> u32 {
351        let k = &mut self.m.kbnav;
352        k.ctx = 0;
353        k.ctx_frame = self.frame;
354        k.ctx_rect = self.wp.ctx.last_rect;
355        EV_RCLICK
356    }
357
358    /// kb_ctx_rect -> the rectangle (px) of the widget that got the menu key as a right click
359    /// in this pass (menu_popup opens there)
360    pub(crate) fn kb_ctx_rect(&self) -> Option<[i32; 4]> {
361        let k = &self.m.kbnav;
362        if k.ctx_frame != 0 && k.ctx_frame == self.frame { Some(k.ctx_rect) } else { None }
363    }
364
365    /// kb_ctx_redirect(from, to): the menu key meant for `from` goes to `to` (a focused list:
366    /// its selected row)
367    pub(crate) fn kb_ctx_redirect(&mut self, from: u32, to: u32) {
368        if self.m.kbnav.ctx == from && from != 0 {
369            self.m.kbnav.ctx = to;
370        }
371    }
372
373    /// splitter_keys(ev, along_x) -> how far (DIP, + right / down) the keys of this pass move a
374    /// focused splitter of the app (a box with HB_SPLITTER, its events ev; along_x: it moves
375    /// left / right); Home / End: i32::MIN / 2, i32::MAX / 2 (clamp it); Enter / Esc give the
376    /// focus back to the widget focused before F8
377    pub fn splitter_keys(&mut self, ev: u32, along_x: bool) -> i32 {
378        self.kb_grip(ev, along_x)
379    }
380
381    // kb_grip(ev, along_x): splitter_keys for the library's splitters
382    pub(crate) fn kb_grip(&mut self, ev: u32, along_x: bool) -> i32 {
383        if ev & EV_FOCUS == 0 {
384            return 0;
385        }
386        let (dec, inc) = if along_x { (VK_LEFT, VK_RIGHT) } else { (VK_UP, VK_DOWN) };
387        let step = if self.mods & MOD_SHIFT != 0 { 1 } else { GRIP_STEP };
388        let mut d = 0;
389        if self.key(dec) {
390            d -= step;
391        }
392        if self.key(inc) {
393            d += step;
394        }
395        if self.key(VK_HOME) {
396            d = i32::MIN / 2;
397        }
398        if self.key(VK_END) {
399            d = i32::MAX / 2;
400        }
401        if self.key(VK_RETURN) || self.key(VK_ESCAPE) {
402            self.kb_split_return();
403        } else if d != 0 {
404            self.wp.ctx.focus_visible = true;
405        }
406        d
407    }
408
409    /// kb_keeps(k): the live window's own navigation uses this key (a tool window that borrows
410    /// its owner's keys hands the others on)
411    pub(crate) fn kb_keeps(&self, k: u32) -> bool {
412        let vk = k & 0xFFFF;
413        let m = (k >> 16) & MOD3;
414        match vk {
415            VK_TAB => m & (MOD_CTRL | MOD_ALT) == 0,
416            VK_F6 => m & MOD_ALT == 0 && self.accel_find(VK_F6, m).is_none(),
417            VK_F8 => m & (MOD_CTRL | MOD_ALT) == 0 && self.accel_find(VK_F8, m).is_none() && !self.kb_splitters().is_empty(),
418            VK_APPS => self.wp.ctx.focus != 0,
419            VK_F10 => m == MOD_SHIFT && self.wp.ctx.focus != 0,
420            VK_ESCAPE => m == 0 && self.kb_tool_here(),
421            VK_LEFT | VK_RIGHT | VK_UP | VK_DOWN | VK_HOME | VK_END => m == 0 && self.kb_focus_group() != 0,
422            _ => false,
423        }
424    }
425
426    /// kb_take_rest(cap): the unconsumed keys of this pass except the navigation keys of the
427    /// window, consumed (uiw_fwd_take)
428    pub(crate) fn kb_take_rest(&mut self, cap: usize) -> Vec<u32> {
429        let keep: Vec<bool> = self.wp.ctx.keyq.iter().map(|&k| (k as i32) < 0 || self.kb_keeps(k)).collect();
430        let mut out = Vec::new();
431        for (i, k) in self.wp.ctx.keyq.iter_mut().enumerate() {
432            if keep[i] {
433                continue;
434            }
435            if out.len() >= cap {
436                break;
437            }
438            out.push(*k);
439            *k |= 0x8000_0000;
440        }
441        out
442    }
443
444    // ------------------------------------------------------------------ the pass end
445
446    /// kb_nav: at the end of a window's pass (uiw_pass_end): the first navigation key no widget
447    /// took, then the memory of groups, panes and the focus' place
448    pub(crate) fn kb_nav(&mut self) {
449        if self.m.kbnav.ctx != 0 && self.frame.wrapping_sub(self.m.kbnav.ctx_set) > 2 {
450            self.m.kbnav.ctx = 0;
451        }
452        self.kb_autofocus();
453        let n = self.wp.ctx.keyq.len();
454        for qi in 0..n {
455            let k = self.wp.ctx.keyq[qi];
456            if (k as i32) < 0 {
457                continue;
458            }
459            if self.kb_key(k) {
460                if let Some(e) = self.wp.ctx.keyq.get_mut(qi) {
461                    *e |= 0x8000_0000;
462                }
463                break;
464            }
465        }
466        self.kb_remember();
467    }
468
469    // kb_key(k) -> the key was a navigation key and is used
470    fn kb_key(&mut self, k: u32) -> bool {
471        let vk = k & 0xFFFF;
472        let m = (k >> 16) & MOD3;
473        let shift = m & MOD_SHIFT != 0;
474        match vk {
475            VK_TAB if m & (MOD_CTRL | MOD_ALT) == 0 => {
476                self.focus_step(shift);
477                true
478            }
479            VK_LEFT | VK_RIGHT | VK_UP | VK_DOWN | VK_HOME | VK_END if m == 0 => self.kb_group_move(vk),
480            VK_F6 if m & MOD_ALT == 0 && self.accel_find(VK_F6, m).is_none() => {
481                if m & MOD_CTRL != 0 {
482                    self.kb_window_switch(shift);
483                } else {
484                    self.kb_pane_switch(shift);
485                }
486                true
487            }
488            VK_F8 if m & (MOD_CTRL | MOD_ALT) == 0 && self.accel_find(VK_F8, m).is_none() => self.kb_splitter_step(shift),
489            VK_APPS => self.kb_ctx_request(),
490            VK_F10 if m == MOD_SHIFT => self.kb_ctx_request(),
491            VK_ESCAPE if m == 0 => self.kb_escape(),
492            _ => false,
493        }
494    }
495
496    // kb_autofocus: a dialog window's first pass: its first Tab stop gets the focus (no ring)
497    fn kb_autofocus(&mut self) {
498        if !self.wp.ctx.kb.autofocus {
499            return;
500        }
501        self.wp.ctx.kb.autofocus = false;
502        if self.wp.ctx.focus != 0 {
503            return;
504        }
505        let c = &self.wp.ctx;
506        let list = &c.hits[c.hit_cur];
507        let (st, top) = stops(list, 0, &c.kb.gmem);
508        if top != L_BASE as i32 {
509            return;
510        }
511        if let Some(&i) = st.first() {
512            let id = list[i].id;
513            self.wp.ctx.focus = id;
514            self.wp.ctx.focus_visible = false;
515            self.wp.ctx.kb.soft = id;
516            self.repass = true;
517        }
518    }
519
520    // kb_remember: the group and pane of the focused widget remember it; its place among the
521    // Tab stops (for Tab when it goes away)
522    fn kb_remember(&mut self) {
523        let c = &mut self.wp.ctx;
524        let f = c.focus;
525        if f != c.kb.soft {
526            c.kb.soft = 0;
527        }
528        if f == 0 {
529            return;
530        }
531        let list = &c.hits[c.hit_cur];
532        let Some(h) = list.iter().find(|h| h.id == f && focusable(h)) else { return };
533        if h.group != 0 {
534            mem_put(&mut c.kb.gmem, h.group, f);
535        }
536        mem_put(&mut c.kb.pmem, h.pane, f);
537        let (st, _) = stops(list, f, &c.kb.gmem);
538        if let Some(pos) = st.iter().position(|&i| list[i].id == f) {
539            let n = st.len();
540            c.kb.anchor = [f, list[st[(pos + 1) % n]].id, list[st[(pos + n - 1) % n]].id];
541        }
542    }
543
544    // focus_step(back): Tab / Shift+Tab: the next / previous Tab stop of this pass
545    fn focus_step(&mut self, back: bool) {
546        let c = &self.wp.ctx;
547        let list = &c.hits[c.hit_cur];
548        let f = c.focus;
549        let (st, _) = stops(list, f, &c.kb.gmem);
550        let n = st.len();
551        if n == 0 {
552            return;
553        }
554        let t = match st.iter().position(|&i| list[i].id == f) {
555            Some(p) => {
556                if back {
557                    (p + n - 1) % n
558                } else {
559                    (p + 1) % n
560                }
561            }
562            None => {
563                // a focused box that is no Tab stop (a list row's check box, a splitter): from
564                // its place
565                let at = if f != 0 { list.iter().position(|h| h.id == f && h.flags as u32 & (HB_FOCUS | HB_SPLITTER) != 0) } else { None };
566                match at {
567                    Some(a) => {
568                        if back {
569                            st.iter().rposition(|&i| i < a).unwrap_or(n - 1)
570                        } else {
571                            st.iter().position(|&i| i > a).unwrap_or(0)
572                        }
573                    }
574                    None => {
575                        // the focused widget went away: on from the place it had
576                        let an = c.kb.anchor;
577                        let want = if f != 0 && an[0] == f { if back { an[2] } else { an[1] } } else { 0 };
578                        match st.iter().position(|&i| want != 0 && list[i].id == want) {
579                            Some(p) => p,
580                            None => {
581                                if back {
582                                    n - 1
583                                } else {
584                                    0
585                                }
586                            }
587                        }
588                    }
589                }
590            }
591        };
592        let id = list[st[t]].id;
593        self.wp.ctx.focus = id;
594        self.wp.ctx.focus_visible = true;
595        self.repass = true;
596    }
597
598    // kb_focus_group -> the focus group of the focused widget with two members or more (0 none)
599    fn kb_focus_group(&self) -> u32 {
600        let c = &self.wp.ctx;
601        let f = c.focus;
602        if f == 0 {
603            return 0;
604        }
605        let list = &c.hits[c.hit_cur];
606        let Some(h) = list.iter().find(|h| h.id == f && focusable(h)) else { return 0 };
607        if h.group == 0 || h.flags as u32 & HB_TEXT != 0 {
608            return 0;
609        }
610        let g = h.group;
611        if list.iter().any(|m| m.group == g && m.id != f && focusable(m)) { g } else { 0 }
612    }
613
614    // kb_group_move(vk) -> used: an arrow / Home / End inside the focused widget's group
615    fn kb_group_move(&mut self, vk: u32) -> bool {
616        let g = self.kb_focus_group();
617        if g == 0 {
618            return false;
619        }
620        let c = &self.wp.ctx;
621        let f = c.focus;
622        let list = &c.hits[c.hit_cur];
623        let layer = list.iter().find(|h| h.id == f && focusable(h)).map_or(0, |h| h.layer);
624        // the members (the first box of every id)
625        let mut ms: Vec<(u32, [i32; 4])> = Vec::new();
626        for h in list.iter() {
627            if h.group == g && h.layer == layer && focusable(h) && !ms.iter().any(|m| m.0 == h.id) {
628                ms.push((h.id, [h.x0, h.y0, h.x1, h.y1]));
629            }
630        }
631        let n = ms.len();
632        let Some(cur) = ms.iter().position(|m| m.0 == f) else { return false };
633        let t = match vk {
634            VK_HOME => 0,
635            VK_END => n - 1,
636            _ => {
637                let r0 = ms[0].1;
638                let row = ms.iter().all(|m| span_gap(r0[1], r0[3], m.1[1], m.1[3]) == 0 && m.1[3] > r0[1]);
639                let col = ms.iter().all(|m| span_gap(r0[0], r0[2], m.1[0], m.1[2]) == 0 && m.1[2] > r0[0]);
640                if row || col {
641                    if matches!(vk, VK_RIGHT | VK_DOWN) {
642                        (cur + 1) % n
643                    } else {
644                        (cur + n - 1) % n
645                    }
646                } else {
647                    match nearest(&ms, cur, vk) {
648                        Some(t) => t,
649                        None => return true, // the edge of a grid: the arrow stays in it
650                    }
651                }
652            }
653        };
654        let id = ms[t].0;
655        if id != f {
656            self.wp.ctx.focus = id;
657            self.m.kbnav.moved = id;
658            self.repass = true;
659        }
660        self.wp.ctx.focus_visible = true;
661        true
662    }
663
664    // kb_ctx_request -> used: the menu key / Shift+F10 no widget took: the focused widget gets
665    // a right click in the next pass
666    fn kb_ctx_request(&mut self) -> bool {
667        let f = self.wp.ctx.focus;
668        if f == 0 || self.popup_count() != 0 {
669            return false;
670        }
671        let c = &self.wp.ctx;
672        if !c.hits[c.hit_cur].iter().any(|h| h.id == f) {
673            return false;
674        }
675        self.m.kbnav.ctx = f;
676        self.m.kbnav.ctx_set = self.frame;
677        self.repass = true;
678        true
679    }
680
681    // kb_tool_here: the live window is a tool window that has the keys by F6 / Ctrl+F6
682    fn kb_tool_here(&self) -> bool {
683        let cur = self.uiw_cur;
684        cur != 0 && self.m.kbnav.tool && self.m.win.kfocus == cur + 1
685    }
686
687    // kb_escape -> used: Esc no widget took: a splitter gives the focus back; a tool window
688    // that got the keys by F6 gives them back to its owner
689    fn kb_escape(&mut self) -> bool {
690        if self.kb_on_splitter() {
691            self.kb_split_return();
692            return true;
693        }
694        if !self.kb_tool_here() {
695            return false;
696        }
697        let cur = self.uiw_cur;
698        self.m.win.kfocus = 0;
699        self.m.kbnav.tool = false;
700        self.wp.ctx.focus_visible = false;
701        let o = self.m.win.recs[(cur as usize) & (WIN_MAX - 1)].owner;
702        if let Some(p) = self.uiw_parts_mut(o) {
703            p.ctx.focus_visible = p.ctx.focus != 0;
704        }
705        self.repass = true;
706        self.invalidate();
707        true
708    }
709
710    // ------------------------------------------------------------------ panes and windows
711
712    // kb_pane_switch(back): F6 / Shift+F6
713    fn kb_pane_switch(&mut self, back: bool) {
714        if self.wp.ctx.modal_cur != 0 {
715            return; // a content dialog keeps the keyboard
716        }
717        let others = self.kb_ring().len() > 1;
718        let mv = {
719            let c = &self.wp.ctx;
720            let list = &c.hits[c.hit_cur];
721            let f = c.focus;
722            let (st, top) = stops(list, f, &c.kb.gmem);
723            if top > L_BASE as i32 {
724                return;
725            }
726            // the panes in the order of their first Tab stop
727            let mut panes: Vec<u32> = Vec::new();
728            for &i in &st {
729                if !panes.contains(&list[i].pane) {
730                    panes.push(list[i].pane);
731                }
732            }
733            let n = panes.len();
734            let cur = list
735                .iter()
736                .find(|h| f != 0 && h.id == f && h.flags as u32 & (HB_FOCUS | HB_SPLITTER) != 0)
737                .and_then(|h| panes.iter().position(|&p| p == h.pane));
738            let t = match cur {
739                None if n == 0 => None,
740                None => Some(if back { n - 1 } else { 0 }),
741                Some(ci) if !back && ci + 1 < n => Some(ci + 1),
742                Some(ci) if back && ci > 0 => Some(ci - 1),
743                Some(_) if others => None,
744                Some(ci) => Some(if n == 1 { ci } else if back { n - 1 } else { 0 }),
745            };
746            match t {
747                None if others => PaneMove::Window(back),
748                None => PaneMove::None,
749                Some(t) => {
750                    let p = panes[t];
751                    let m = mem_get(&c.kb.pmem, p);
752                    let id = if m != 0 && list.iter().any(|h| h.id == m && h.pane == p && focusable(h)) {
753                        m
754                    } else {
755                        st.iter().map(|&i| &list[i]).find(|h| h.pane == p).map_or(0, |h| h.id)
756                    };
757                    PaneMove::Focus(id)
758                }
759            }
760        };
761        match mv {
762            PaneMove::Focus(id) if id != 0 => {
763                self.wp.ctx.focus = id;
764                self.wp.ctx.focus_visible = true;
765                self.repass = true;
766            }
767            PaneMove::Window(back) => self.kb_window_switch(back),
768            _ => {}
769        }
770    }
771
772    // kb_ring -> the windows F6 / Ctrl+F6 visit: the main window and every shown window that
773    // is not inert and has a focusable widget, by index
774    fn kb_ring(&self) -> Vec<u32> {
775        let mut v = Vec::new();
776        for i in 0..WIN_MAX as u32 {
777            if i != 0 && !self.uiw_drawable(i) || (self.m.win.inert >> i) & 1 != 0 {
778                continue;
779            }
780            let Some(p) = self.uiw_parts(i) else { continue };
781            let c = &p.ctx;
782            let list = if i == self.uiw_cur { &c.hits[c.hit_cur] } else { &c.hits[c.hit_cur ^ 1] };
783            if list.iter().any(focusable) {
784                v.push(i);
785            }
786        }
787        v
788    }
789
790    // kb_window_switch(back): Ctrl+F6 / Ctrl+Shift+F6 (and F6 past the last pane): the keys go
791    // to the next / previous window, to the widget focused there before or its first Tab stop
792    fn kb_window_switch(&mut self, back: bool) {
793        let ring = self.kb_ring();
794        let n = ring.len();
795        if n < 2 {
796            return;
797        }
798        let live = self.uiw_cur;
799        let ci = ring.iter().position(|&w| w == live).unwrap_or(0);
800        let j = if back { ring[(ci + n - 1) % n] } else { ring[(ci + 1) % n] };
801        self.wp.ctx.focus_visible = false; // the window without the keys shows no ring
802        if let Some(p) = self.uiw_parts_mut(j) {
803            let c = &mut p.ctx;
804            let nf = {
805                let list = if j == live { &c.hits[c.hit_cur] } else { &c.hits[c.hit_cur ^ 1] };
806                let f = c.focus;
807                if f != 0 && list.iter().any(|h| h.id == f && focusable(h)) {
808                    f
809                } else {
810                    let (st, _) = stops(list, 0, &c.kb.gmem);
811                    st.first().map_or(0, |&i| list[i].id)
812                }
813            };
814            c.focus = nf;
815            c.focus_visible = nf != 0;
816        }
817        let r = &self.m.win.recs[(j as usize) & (WIN_MAX - 1)];
818        if j != 0 && r.flags & (WF_TOOL | WF_KEYS) == WF_TOOL {
819            // a tool window borrows its owner's keys (win.rs uiw_key_target)
820            let o = r.owner;
821            self.m.win.kfocus = j + 1;
822            self.m.kbnav.tool = true;
823            self.uiw_activate(o);
824        } else {
825            self.m.win.kfocus = 0;
826            self.m.kbnav.tool = false;
827            self.uiw_activate(j);
828        }
829        self.repass = true;
830        self.invalidate();
831    }
832
833    // ------------------------------------------------------------------ splitters
834
835    // kb_splitters -> the splitters of this pass (ids, emission order)
836    fn kb_splitters(&self) -> Vec<u32> {
837        let c = &self.wp.ctx;
838        let mut v: Vec<u32> = Vec::new();
839        for h in &c.hits[c.hit_cur] {
840            if h.flags as u32 & (HB_SPLITTER | HB_DISABLED) == HB_SPLITTER && h.layer as u32 == L_BASE && !v.contains(&h.id) {
841                v.push(h.id);
842            }
843        }
844        v
845    }
846
847    // kb_on_splitter: the focus is on a splitter
848    fn kb_on_splitter(&self) -> bool {
849        let c = &self.wp.ctx;
850        c.focus != 0 && c.hits[c.hit_cur].iter().any(|h| h.id == c.focus && h.flags as u32 & HB_SPLITTER != 0)
851    }
852
853    // kb_splitter_step(back) -> used: F8 / Shift+F8
854    fn kb_splitter_step(&mut self, back: bool) -> bool {
855        let sp = self.kb_splitters();
856        let n = sp.len();
857        if n == 0 {
858            return false;
859        }
860        let f = self.wp.ctx.focus;
861        let t = match sp.iter().position(|&s| s == f) {
862            None => {
863                self.wp.ctx.kb.split_ret = f;
864                if back { n - 1 } else { 0 }
865            }
866            Some(p) => {
867                let q = if back { p as i32 - 1 } else { p as i32 + 1 };
868                if q < 0 || q >= n as i32 {
869                    self.kb_split_return();
870                    return true;
871                }
872                q as usize
873            }
874        };
875        self.wp.ctx.focus = sp[t];
876        self.wp.ctx.focus_visible = true;
877        self.repass = true;
878        true
879    }
880
881    // kb_split_return: back to the widget focused before F8
882    fn kb_split_return(&mut self) {
883        let r = core::mem::take(&mut self.wp.ctx.kb.split_ret);
884        self.wp.ctx.focus = r;
885        self.wp.ctx.focus_visible = r != 0;
886        self.repass = true;
887    }
888}