Skip to main content

ui/
chrome.rs

1// chrome -- window chrome and overlays: content dialog, info bar, status bar, title bar,
2// caption buttons, the chrome of secondary windows, edge grips       [src/ui/ui_chrome.s]
3//
4//   dialog_begin(id, title, open) -> true while open (emit the content, then
5//        dialog_buttons and dialog_end; always call dialog_end)
6//        WinUI ContentDialog on L_DIALOG: smoke over the window, centred 320..548 DIP
7//        wide, title, content; everything below is inert while it is open
8//   dialog_buttons(primary, secondary, close) -> DLG_NONE / DLG_PRIMARY / DLG_SECONDARY /
9//        DLG_CLOSE -- footer buttons (labels, None = none; primary is the accent button);
10//        Enter = primary, Esc = close.  Any result clears the open flag.
11//   dialog_end()
12//   infobar(id, severity, title, msg) -> EV (EV_ACT: the close button was clicked)
13//        IB_INFO / IB_SUCCESS / IB_WARNING / IB_ERROR; the close button has the id id + 1;
14//        placed too narrow, the message and then the title are cut with "…"
15//   statusbar_begin() / statusbar_end()
16//        32 DIP bar (layer fill, top divider) laying out its children left to right,
17//        vertically centred, 16 DIP apart (captions: text(TS_CAPTION, ...))
18//   edge_grips()   resize handles for frameless windows on Linux (the runtime calls it;
19//        also for resizable secondary windows with the library frame); the caption buttons
20//        win over them where they are
21//   titlebar_begin(title, icon) / titlebar_end() / titlebar_open()
22//        custom title bar row (style: SM_TB_H, SM_TB_H_LARGE with next_flags(ST_LARGE)):
23//        icon + title, then the caller's widgets; empty parts are an HB_DRAG region, the end
24//        adds the caption buttons (HB_MIN / HB_MAX / HB_CLOSE; with SM_CAP_LEFT the begin puts
25//        them first, close / min / max).  The system's layout (SysSettings::titlebar: GNOME's
26//        button-layout) wins over the style's: its left buttons come first, its right ones
27//        last, missing ones are not shown; right-to-left mirrors the whole bar (as GTK does).
28//        A double click, a middle and a right click on the empty part do what the system says
29//        (default: maximize / restore, nothing, the window menu; a left press moves the
30//        window).  Headless runs only record what they would ask the window manager for
31//        (scripts: expect wm ...)
32// Windows (win.rs):
33//   win_chrome_begin(i) / win_chrome_end(i) -> natural content height (DIP)
34//        border, title bar (28 DIP tool / 32 DIP dialog: title, close button) and the
35//        content box of a secondary window; inside a WF_DIALOG window dialog_buttons puts
36//        its footer below the content box (pinned to the bottom)
37//
38// Ids: dialog parts (id << 3 | k) | 0x80000000; title bar 0xFFFF7400.. (min +1, max +2,
39// close +3); status bar 0xFFFF7410 .. 0xFFFF7412; edge grips 0xFFFF7430..
40
41use alloc::string::String;
42
43use super::ctx::Hit;
44
45use super::consts::*;
46use super::keys::*;
47use super::layout::BOX_STACK_MAX;
48use super::win::{Shared, UWI_BUILDING, WCH_BASE, WCH_BTN, WCH_CLOSE, WCH_CONTENT, WCH_FOOTER, WCH_ROLL};
49use super::{Sp, Ui};
50
51const TB_ID: u32 = 0xFFFF_7400;
52const TB_MIN_ID: u32 = 0xFFFF_7401;
53const TB_MAX_ID: u32 = 0xFFFF_7402;
54const TB_CLOSE_ID: u32 = 0xFFFF_7403;
55const SB_ID: u32 = 0xFFFF_7410;
56const SB_INNER_ID: u32 = 0xFFFF_7411;
57const SB_SCROLL_ID: u32 = 0xFFFF_7412; // (form.rs) the rolling status bar's view
58const GRIP_ID: u32 = 0xFFFF_7430; // + 0..7
59
60// what a headless run would have asked the window manager for (S::wm_req: kind | arg << 8;
61// scripts: expect wm ...)
62const WMR_MOVE: u32 = 1;
63const WMR_MENU: u32 = 2;
64const WMR_CMD: u32 = 3; // plat::WCMD_* in arg
65const WMR_RESIZE: u32 = 4; // plat::EDGE_* in arg
66
67/// state shared by all windows
68#[derive(Default)]
69pub(crate) struct S {
70    /// width of the next caption button (DIP, 0 = SM_CAP_W)
71    cap_w: i32,
72    /// id of the first edge grip of the window being built
73    grip_base: u32,
74    /// EDGE_* bits of the tiled edges of the window being built (no handles there)
75    grip_off: u32,
76    /// (headless) the last request for the window manager since the last check (WMR_*)
77    pub(crate) wm_req: u32,
78}
79
80/// the request a headless run recorded (S::wm_req) as a script names it: none, move, menu,
81/// minimize, maximize, restore, close, lower, fullscreen, windowed, resize n|s|w|e|nw|ne|sw|se
82pub(crate) fn wm_req_name(r: u32) -> String {
83    let arg = r >> 8;
84    let s = match r & 255 {
85        WMR_MOVE => "move",
86        WMR_MENU => "menu",
87        WMR_CMD => match arg {
88            plat::WCMD_MINIMIZE => "minimize",
89            plat::WCMD_MAXIMIZE => "maximize",
90            plat::WCMD_RESTORE => "restore",
91            plat::WCMD_CLOSE => "close",
92            plat::WCMD_LOWER => "lower",
93            plat::WCMD_FULLSCREEN => "fullscreen",
94            plat::WCMD_WINDOWED => "windowed",
95            _ => "command",
96        },
97        WMR_RESIZE => {
98            let mut e = String::from("resize ");
99            e.push_str(if arg & plat::EDGE_TOP != 0 { "n" } else if arg & plat::EDGE_BOTTOM != 0 { "s" } else { "" });
100            e.push_str(if arg & plat::EDGE_LEFT != 0 { "w" } else if arg & plat::EDGE_RIGHT != 0 { "e" } else { "" });
101            return e;
102        }
103        _ => "none",
104    };
105    String::from(s)
106}
107
108/// per-window state (ui_chrome_wb0: content dialog, title bar, window chrome; swapped by
109/// win.rs)
110#[derive(Default)]
111pub(crate) struct W {
112    /// (form.rs) the status bar being built rolls sideways (a phone's, too narrow for its items)
113    sb_scroll: bool,
114    dlg_clip: [i32; 4],
115    dlg_openp: Option<Shared<i32>>,
116    pub(crate) dlg_active: bool,
117    dlg_nest: u32,
118    dlg_id: u32,
119    dlg_phase: u32,
120    dlg_layer: u32,
121    dlg_dis: u32,
122    /// the dialog opened in this pass (ui.dialog_sound, sound.rs)
123    pub(crate) dlg_fresh: bool,
124    tb_state: u32,
125    /// This titlebar overlays app content and places the agent badge by the window controls.
126    tb_overlay: bool,
127    /// the title bar being built, the style's window_titlebar behind it (reset with the pass)
128    pub(crate) tb_rect: Option<[i32; 4]>,
129    /// dialog_buttons closed the content box of a dialog window
130    /// (form.rs) the content of the window being built rolls (a phone's screen cut it short)
131    /// (form.rs) the content of the open content dialog rolls (taller than a phone's window)
132    dlg_roll: bool,
133    wc_roll: bool,
134    wc_footer: bool,
135    /// the caption buttons' hit boxes of this pass: edge_grips puts them on top again, so
136    /// the buttons win over the resize band where they are (cap_n: how many; reset with
137    /// the pass)
138    cap_hits: [Hit; 4],
139    pub(crate) cap_n: u8,
140}
141
142// a dialog part id: (id << 3 | k) | 0x80000000
143fn dlg_part(id: u32, k: u32) -> u32 {
144    id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
145}
146
147impl Ui {
148    // ------------------------------------------------------------------ content dialog
149
150    /// ui_dialog_begin(id, title, open) -> true if the dialog is open
151    #[cfg_attr(jfgui_dev, track_caller)]
152    pub fn dialog_begin(&mut self, id: u32, title: &str, open: &Shared<i32>) -> bool {
153        let _src = self.src_scope();
154        if self.wp.chrome.dlg_active {
155            self.wp.chrome.dlg_nest += 1; // a dialog inside a dialog is not supported
156            return false;
157        }
158        // slot: aux = open last pass, scrollx = focus before
159        if open.get() == 0 {
160            // closed: give the focus back once
161            let s = self.slot(id);
162            if s.aux != 0 {
163                s.aux = 0;
164                let f = s.scrollx as u32;
165                self.wp.ctx.focus = f;
166                self.repass();
167            }
168            return false;
169        }
170        {
171            let c = &mut self.wp.chrome;
172            c.dlg_active = true;
173            c.dlg_id = id;
174            c.dlg_openp = Some(open.clone());
175            c.dlg_phase = 1;
176            c.dlg_fresh = false;
177        }
178        let focus = self.wp.ctx.focus;
179        let s = self.slot(id);
180        if s.aux == 0 {
181            // just opened: the keyboard focus moves into the dialog
182            s.aux = 1;
183            s.scrollx = focus as i32;
184            self.wp.ctx.focus = id;
185            self.wp.ctx.focus_visible = false;
186            self.wp.chrome.dlg_fresh = true;
187        }
188        self.wp.chrome.dlg_layer = self.layer_set(L_DIALOG);
189        self.modal(L_DIALOG);
190        // the dialog is laid out and hit tested against the whole window
191        self.wp.chrome.dlg_clip = self.wp.ctx.uiclip;
192        let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
193        self.wp.ctx.uiclip = [0, 0, w, h];
194        self.wp.chrome.dlg_dis = self.wp.ctx.disabled_depth;
195        self.wp.ctx.disabled_depth = 0;
196        // smoke (style)
197        let st = self.style;
198        let sp = Sp { x: 0, y: 0, w, h, ..Default::default() };
199        st.dialog_smoke(self, &sp);
200        self.wp.ctx.last_rect = [0, 0, w, h];
201        self.hit(dlg_part(id, 1), HB_BLOCK);
202        // size: the natural size of the last pass, 320 .. 548 DIP wide
203        let (mut dw, nh) = {
204            let s = self.slot(id);
205            (s.measw, s.meash)
206        };
207        let min = self.dp(st.metric(SM_DLG_MIN));
208        if dw < min {
209            dw = min;
210        }
211        let max = self.dp(st.metric(SM_DLG_MAX));
212        if dw > max {
213            dw = max;
214        }
215        let m = self.dp(st.metric(SM_DLG_MARGIN)); // space kept free around it
216        if dw > w - m {
217            dw = w - m;
218        }
219        if dw < 1 {
220            dw = 1;
221        }
222        let x = (w - dw) >> 1;
223        // (form.rs) on a phone or a tablet a dialog taller than the window keeps inside it, its
224        // content rolls (the footer stays below); a desktop's dialogs never change
225        let roll = self.mobile() && nh > h - m;
226        self.wp.chrome.dlg_roll = roll;
227        let (y, dh) = if roll { (m >> 1, h - m) } else { (((h - nh) >> 1).max(0), 0) };
228        self.next_rect_px(x, y, dw, dh);
229        self.box_begin(id, BOX_V | BOX_BLOCK, 0);
230        let r = self.wp.ctx.last_rect;
231        // footer height from its last measurement
232        let fh = self.slot(dlg_part(id, 4)).meash;
233        // paint (style): shadow, surface, border
234        let mut sp = Sp { value: fh, ..Default::default() };
235        sp.place(r);
236        st.dialog_frame(self, &sp);
237        // record
238        self.wp.ctx.last_rect = r;
239        self.wp.ctx.last_state = SS_OPEN as u32;
240        self.sem(K_DIALOG, id, Some(title), 0);
241        // content: title, then the caller's widgets
242        let gap = st.metric(SM_DLG_CONTENT_GAP);
243        let flags = st.metric(SM_DLG_CONTENT_BOX) as u32;
244        if roll {
245            self.next_size(0, -1);
246            self.scroll_begin(dlg_part(id, 2), SC_V); // (part 2: free; 5 .. 7 are the buttons)
247            self.next_size(-1, 0);
248        }
249        self.box_begin(dlg_part(id, 3), flags, gap);
250        let ts = st.metric(SM_DLG_TITLE_TS) as u32;
251        self.text(ts, title);
252        true
253    }
254
255    /// ui_dialog_buttons(primary, secondary, close) -> DLG_* (any result clears the open
256    /// flag).  Inside a content dialog: its footer.  Otherwise, inside a WF_DIALOG window:
257    /// a footer pinned to the window's bottom; a result clears the window's open flag.
258    #[cfg_attr(jfgui_dev, track_caller)]
259    pub fn dialog_buttons(&mut self, primary: Option<&str>, secondary: Option<&str>, close: Option<&str>) -> u32 {
260        let _src = self.src_scope();
261        if self.wp.chrome.dlg_active {
262            let c = &mut self.wp.chrome;
263            if c.dlg_nest != 0 || c.dlg_phase != 1 {
264                return DLG_NONE;
265            }
266            c.dlg_phase = 2;
267            let id = c.dlg_id;
268            self.box_end(); // content
269            if self.wp.chrome.dlg_roll {
270                self.scroll_end();
271            }
272            let r = self.dlg_footer(dlg_part(id, 4), dlg_part(id, 5), primary, secondary, close);
273            if r != 0 {
274                if let Some(o) = &self.wp.chrome.dlg_openp {
275                    o.set(0);
276                }
277                self.repass();
278            }
279            return r;
280        }
281        // a dialog window: close its content box, the footer follows at the bottom
282        if self.wp.chrome.wc_footer {
283            return DLG_NONE;
284        }
285        let cur = self.uiw_cur;
286        if cur == 0 {
287            return DLG_NONE;
288        }
289        let rec = &self.m.win.recs[(cur as usize) & (WIN_MAX - 1)];
290        if rec.flags & WF_DIALOG == 0 || rec.state & UWI_BUILDING == 0 {
291            return DLG_NONE;
292        }
293        let openp = rec.openp.clone();
294        self.wp.chrome.wc_footer = true;
295        let ids = cur << 4 | WCH_BASE;
296        self.wc_pop_to(ids + WCH_CONTENT);
297        if core::mem::take(&mut self.wp.chrome.wc_roll) {
298            self.scroll_end();
299        }
300        let r = self.dlg_footer(ids + WCH_FOOTER, ids + WCH_BTN, primary, secondary, close);
301        if r != 0 {
302            if let Some(o) = openp {
303                o.set(0);
304            }
305            self.repass();
306        }
307        r
308    }
309
310    // dlg_footer(footer_id, button_id, primary, secondary, close) -> DLG_*: the button row
311    // of a dialog (ids button_id .. +2); Enter = primary, Esc = close
312    fn dlg_footer(&mut self, fid: u32, bid: u32, p: Option<&str>, s: Option<&str>, c: Option<&str>) -> u32 {
313        let st = self.style;
314        let gap = st.metric(SM_DLG_FOOTER_GAP);
315        let flags = st.metric(SM_DLG_FOOTER_BOX) as u32;
316        self.box_begin(fid, flags, gap);
317        let order = st.metric(SM_DLG_BTN_ORDER);
318        let mut r = DLG_NONE;
319        if order == 0 {
320            // a single button takes the right half
321            let n = p.is_some() as u32 + s.is_some() as u32 + c.is_some() as u32;
322            if n == 1 {
323                self.spacer(-1);
324            }
325            if let Some(l) = p {
326                self.next_size(-1, 0);
327                if self.accent_button(bid, l) & EV_ACT != 0 {
328                    r = DLG_PRIMARY;
329                }
330            }
331            if let Some(l) = s {
332                self.next_size(-1, 0);
333                if self.button(bid + 1, l) & EV_ACT != 0 {
334                    r = DLG_SECONDARY;
335                }
336            }
337            if let Some(l) = c {
338                self.next_size(-1, 0);
339                if self.button(bid + 2, l) & EV_ACT != 0 {
340                    r = DLG_CLOSE;
341                }
342            }
343        } else {
344            // SM_DLG_BTN_ORDER 1 (macOS): right-aligned at their natural width, primary
345            // last; 2 (KDE): right-aligned, primary first
346            self.spacer(-1);
347            if order != 2 {
348                if let Some(l) = c {
349                    if self.button(bid + 2, l) & EV_ACT != 0 {
350                        r = DLG_CLOSE;
351                    }
352                }
353                if let Some(l) = s {
354                    if self.button(bid + 1, l) & EV_ACT != 0 {
355                        r = DLG_SECONDARY;
356                    }
357                }
358                if let Some(l) = p {
359                    if self.accent_button(bid, l) & EV_ACT != 0 {
360                        r = DLG_PRIMARY;
361                    }
362                }
363            } else {
364                if let Some(l) = p {
365                    if self.accent_button(bid, l) & EV_ACT != 0 {
366                        r = DLG_PRIMARY;
367                    }
368                }
369                if let Some(l) = s {
370                    if self.button(bid + 1, l) & EV_ACT != 0 {
371                        r = DLG_SECONDARY;
372                    }
373                }
374                if let Some(l) = c {
375                    if self.button(bid + 2, l) & EV_ACT != 0 {
376                        r = DLG_CLOSE;
377                    }
378                }
379            }
380        }
381        self.box_end(); // footer
382        // keyboard: Enter = primary, Esc = close (keys a focused widget did not take)
383        if r != 0 {
384            return r;
385        }
386        if p.is_some() && self.key(VK_RETURN) {
387            return DLG_PRIMARY;
388        }
389        if self.key(VK_ESCAPE) {
390            return DLG_CLOSE;
391        }
392        DLG_NONE
393    }
394
395    /// ui_dialog_end
396    #[cfg_attr(jfgui_dev, track_caller)]
397    pub fn dialog_end(&mut self) {
398        let _src = self.src_scope();
399        if self.wp.chrome.dlg_nest != 0 {
400            self.wp.chrome.dlg_nest -= 1;
401            return;
402        }
403        if !self.wp.chrome.dlg_active {
404            return;
405        }
406        if self.wp.chrome.dlg_phase == 1 {
407            // no footer: Esc still closes
408            if self.key(VK_ESCAPE) {
409                if let Some(o) = &self.wp.chrome.dlg_openp {
410                    o.set(0);
411                }
412                self.repass();
413            }
414            self.box_end(); // content
415            if self.wp.chrome.dlg_roll {
416                self.scroll_end();
417            }
418        }
419        self.wp.chrome.dlg_roll = false;
420        self.box_end(); // outer
421        let c = &self.wp.chrome;
422        let (clip, dis, layer) = (c.dlg_clip, c.dlg_dis, c.dlg_layer);
423        self.wp.ctx.uiclip = clip;
424        self.wp.ctx.disabled_depth = dis;
425        self.layer_set(layer);
426        self.wp.chrome.dlg_active = false;
427    }
428
429    // ------------------------------------------------------------------ info bar
430
431    /// ui_infobar(id, severity, title, msg) -> EV (EV_ACT when the close button was clicked)
432    #[cfg_attr(jfgui_dev, track_caller)]
433    pub fn infobar(&mut self, id: u32, severity: u32, title: &str, msg: &str) -> u32 {
434        let _src = self.src_scope();
435        let sev = severity & 3;
436        let st = self.style;
437        // measure (style)
438        let mut sp = Sp { flags: sev, label: Some(title), label2: Some(msg), ..Default::default() };
439        let (w, h) = st.infobar_size(self, &mut sp);
440        self.place(w, h);
441        let r = self.wp.ctx.last_rect;
442        // narrower than the texts: the message cut with "…" (left out where little of it
443        // would stay), then the title; the style measures them again (s[0] / s[1])
444        let fit = if w > r[2] { Some(self.infobar_fit(title, msg, sp.s[0], sp.s[1], w - r[2])) } else { None };
445        if let Some((t, m)) = &fit {
446            sp.label = Some(t);
447            sp.label2 = if m.is_empty() { None } else { Some(m) };
448            st.infobar_size(self, &mut sp);
449        }
450        // paint (style): card, icon, title, message; it reports the close button's place
451        sp.place(r);
452        st.infobar(self, &mut sp);
453        let (cx, cs) = (sp.s[2], sp.s[3]);
454        self.wp.ctx.last_rect = [cx, ((r[3] - cs) >> 1) + r[1], cs, cs];
455        let ev = self.behave(id.wrapping_add(1), HB_FOCUS);
456        // paint (style): the close button
457        sp.place(self.wp.ctx.last_rect);
458        sp.ev = ev;
459        st.infobar_close(self, &sp);
460        if ev & EV_FOCUS != 0 && self.wp.ctx.focus_visible {
461            let lr = self.wp.ctx.last_rect;
462            st.focus_rect(self, lr[0], lr[1], lr[2], lr[3]);
463        }
464        self.sem(K_BUTTON, id.wrapping_add(1), Some("Close"), 0);
465        // record of the bar: label = title, value = severity
466        self.wp.ctx.last_rect = r;
467        self.wp.ctx.last_state = 0;
468        self.sem(K_BOX, id, Some(title), sev as i32);
469        ev & EV_ACT
470    }
471
472    // infobar_fit(title, msg, title width, message width, px too wide) -> (title, message):
473    // the message (TS_BODY) gives way first, then the title (TS_BODY_STRONG); the styles
474    // measure them in these text styles
475    fn infobar_fit(&mut self, title: &str, msg: &str, tw: i32, mw: i32, over: i32) -> (String, String) {
476        let keep = mw - over;
477        if !msg.is_empty() && keep >= self.dp(32) {
478            self.ts_font(TS_BODY);
479            return (String::from(title), self.ellipsize(msg.as_bytes(), keep).0);
480        }
481        let over = over - mw;
482        if over <= 0 {
483            return (String::from(title), String::new());
484        }
485        self.ts_font(TS_BODY_STRONG);
486        (self.ellipsize(title.as_bytes(), (tw - over).max(1)).0, String::new())
487    }
488
489    // ------------------------------------------------------------------ status bar
490
491    /// ui_statusbar_begin: a 32 DIP bar; call it last in a vertical layout (after a fill)
492    #[cfg_attr(jfgui_dev, track_caller)]
493    pub fn statusbar_begin(&mut self) {
494        let _src = self.src_scope();
495        let st = self.style;
496        let h = st.metric(SM_SB_H); // height (DIP)
497        self.next_size(0, h);
498        self.box_begin(SB_ID, BOX_H, 0);
499        let r = self.wp.ctx.last_rect;
500        let mut sp = Sp::default();
501        sp.place(r);
502        st.statusbar(self, &sp);
503        // content row, inset at both sides
504        let pad = self.dp(st.metric(SM_SB_PAD));
505        let mut w = r[2] - pad - pad;
506        if w <= 0 {
507            w = 1;
508        }
509        self.next_rect_px(r[0] + pad, r[1], w, r[3]);
510        let gap = st.metric(SM_SB_GAP); // gap between the items (DIP)
511        // (form.rs) on a phone or a tablet the items roll sideways with the finger while they are
512        // wider than the bar (their widths of the last pass); a desktop's bar never changes
513        let roll = self.mobile() && self.slot(SB_INNER_ID).fixed > w;
514        self.wp.chrome.sb_scroll = roll;
515        if roll {
516            self.scroll_begin(SB_SCROLL_ID, SC_H | SC_NOBAR | BOX_XCENTER);
517        }
518        self.box_begin(SB_INNER_ID, BOX_H | BOX_XCENTER, gap);
519    }
520
521    /// ui_statusbar_end
522    #[cfg_attr(jfgui_dev, track_caller)]
523    pub fn statusbar_end(&mut self) {
524        let _src = self.src_scope();
525        self.box_end();
526        if core::mem::take(&mut self.wp.chrome.sb_scroll) {
527            self.scroll_end();
528        }
529        self.box_end();
530    }
531
532    // ------------------------------------------------------------------ title bar
533
534    /// ui_titlebar_begin(title, icon): a title bar row (icon 0 = none)
535    #[cfg_attr(jfgui_dev, track_caller)]
536    pub fn titlebar_begin(&mut self, title: &str, icon: u32) {
537        let _src = self.src_scope();
538        self.titlebar_begin_opt(Some(title), icon);
539    }
540
541    /// ui_titlebar_begin with title 0: no title text (the record has no label)  // pn-core
542    #[cfg_attr(jfgui_dev, track_caller)]
543    pub fn titlebar_begin_opt(&mut self, title: Option<&str>, icon: u32) {
544        let _src = self.src_scope();
545        self.titlebar_begin_inner(title, icon, false);
546    }
547
548    /// Overlay native window controls and a drag region without painting a titlebar surface.
549    /// Use `next_rect_px` to place it over the empty top edge of a file/start screen.
550    /// The agent indicator remains visible beside the caption buttons when control is enabled.
551    #[cfg_attr(jfgui_dev, track_caller)]
552    pub fn titlebar_begin_overlay(&mut self, title: Option<&str>, icon: u32) {
553        let _src = self.src_scope();
554        self.titlebar_begin_inner(title, icon, true);
555    }
556
557    fn titlebar_begin_inner(&mut self, title: Option<&str>, icon: u32, overlay: bool) {
558        let st = self.style;
559        self.wp.chrome.tb_overlay = overlay;
560        let f = self.take_flags();
561        let h = st.metric(if f & ST_LARGE != 0 { SM_TB_H_LARGE } else { SM_TB_H });
562        self.next_size(0, h);
563        self.box_begin(TB_ID, BOX_H | BOX_XCENTER, 0);
564        // window state (a headless run looks active)
565        self.wp.chrome.tb_state = if self.headless { plat::WST_ACTIVE } else { plat::window_state() };
566        let tbs = self.wp.chrome.tb_state;
567        // background (style)
568        let mut sp = Sp { value: (tbs & plat::WST_ACTIVE != 0) as i32, ..Default::default() };
569        sp.place(self.wp.ctx.last_rect);
570        if !overlay { st.window_titlebar(self, &sp); }
571        self.wp.chrome.tb_rect = if overlay { None } else { Some(self.wp.ctx.last_rect) };
572        // the whole bar is a drag region; widgets placed on it later win the hit test
573        let ev = self.behave(TB_ID, HB_DRAG);
574        self.tb_clicks(ev, tbs);
575        self.wp.ctx.last_state = 0;
576        self.sem(K_TITLEBAR, TB_ID, title, 0);
577        // caption buttons at the left end (the system's layout, else SM_CAP_LEFT: macOS)
578        let left = self.tb_side(false);
579        if left[0] != 0 {
580            if st.metric(SM_CAP_LEFT) != 0 {
581                self.spacer(st.metric(SM_TB_PAD));
582            }
583            self.tb_caps(left);
584        }
585        // icon and title
586        self.spacer(st.metric(SM_TB_PAD)); // space before the icon / title (DIP)
587        if icon != 0 {
588            let s = st.metric(SM_TB_ICON); // icon size (DIP; 0 = the style shows none)
589            if s != 0 {
590                self.icon(icon, s);
591                self.spacer(st.metric(SM_TB_PAD));
592            }
593        }
594        if self.agent_enabled() && !overlay {
595            self.next_size(82, 24);
596            if self.button(super::agent::ID_BADGE, if self.agent_connected() { "Agent ●" } else { "Agent ○" }) & EV_ACT != 0 {
597                self.set_agent_enabled(false);
598            }
599            self.tooltip(if self.agent_connected() { "Agent verbunden · Zum Abschalten klicken" } else { "Agentensteuerung aktiv · Zum Abschalten klicken" });
600            self.spacer(8);
601        }
602        let Some(title) = title else { return };
603        let mut ts = st.metric(SM_TB_TITLE_TS) as u32;
604        if tbs & plat::WST_ACTIVE == 0 {
605            ts |= TX_DISABLED;
606        }
607        self.text(ts, title);
608    }
609
610    /// ui_titlebar_end: caption buttons at the right end (unless the style puts them at the
611    /// left: SM_CAP_LEFT)
612    #[cfg_attr(jfgui_dev, track_caller)]
613    pub fn titlebar_end(&mut self) {
614        let _src = self.src_scope();
615        let st = self.style;
616        if self.wp.chrome.tb_overlay {
617            self.spacer(-1);
618            if self.agent_enabled() {
619                self.next_size(82, 24);
620                if self.button(super::agent::ID_BADGE, if self.agent_connected() { "Agent ●" } else { "Agent ○" }) & EV_ACT != 0 {
621                    self.set_agent_enabled(false);
622                }
623                self.tooltip(if self.agent_connected() { "Agent verbunden · Zum Abschalten klicken" } else { "Agentensteuerung aktiv · Zum Abschalten klicken" });
624                self.spacer(8);
625            }
626        }
627        let right = self.tb_side(true);
628        if right[0] != 0 {
629            if !self.wp.chrome.tb_overlay { self.spacer(-1); }
630            self.tb_caps(right);
631            if st.metric(SM_CAP_LEFT) != 0 {
632                self.spacer(st.metric(SM_TB_PAD)); // the style's buttons keep off the edge
633            }
634        }
635        self.box_end();
636        self.wp.chrome.tb_rect = None;
637    }
638
639    /// The title bar being built, between `titlebar_begin` and `titlebar_end` (None elsewhere
640    /// and on an overlay, which paints no bar): its rectangle (px) and whether the window is
641    /// active.  For a style whose bar has the same colours in every theme (classic): a widget
642    /// drawn right on it cannot use the theme's T_TEXT.
643    pub fn titlebar_open(&self) -> Option<([i32; 4], bool)> {
644        let c = &self.wp.chrome;
645        c.tb_rect.map(|r| (r, c.tb_state & plat::WST_ACTIVE != 0))
646    }
647
648    // tb_clicks(ev, state): the presses on the empty part of the title bar (behave's events
649    // ev, plat::WST_* state): a double click, a middle and a right press do what the system
650    // says (SysSettings::titlebar), a left press lets the system move the window.  The window
651    // manager owns the pointer afterwards: the press is forgotten (not in headless runs, which
652    // only record the request)
653    fn tb_clicks(&mut self, ev: u32, tbs: u32) {
654        let tb = self.tb_sys;
655        let c = &self.wp.ctx;
656        let (act, btn) = if ev & EV_DOUBLE != 0 && tb.double != plat::TB_ACT_NONE {
657            (tb.double, BTN_LEFT)
658        } else if c.active == TB_ID && c.pressed & (1 << BTN_LEFT) != 0 {
659            (u8::MAX, BTN_LEFT) // move
660        } else if c.hot == TB_ID && c.pressed & (1 << BTN_MIDDLE) != 0 && tb.middle != plat::TB_ACT_NONE {
661            (tb.middle, BTN_MIDDLE)
662        } else if c.ractive == TB_ID && c.pressed & (1 << BTN_RIGHT) != 0 && tb.right != plat::TB_ACT_NONE {
663            (tb.right, BTN_RIGHT)
664        } else {
665            return;
666        };
667        let (mx, my) = (c.mx, c.my);
668        match act {
669            u8::MAX => self.wm_move(),
670            plat::TB_ACT_MAXIMIZE => self.tb_cmd(if tbs & plat::WST_MAXIMIZED != 0 { plat::WCMD_RESTORE } else { plat::WCMD_MAXIMIZE }),
671            plat::TB_ACT_MINIMIZE => self.tb_cmd(plat::WCMD_MINIMIZE),
672            plat::TB_ACT_LOWER => self.tb_cmd(plat::WCMD_LOWER),
673            _ => self.wm_menu(mx, my),
674        }
675        if self.headless {
676            return;
677        }
678        // the OS owns the pointer now: forget the press
679        let c = &mut self.wp.ctx;
680        match btn {
681            BTN_LEFT => c.active = 0,
682            BTN_RIGHT => c.ractive = 0,
683            _ => {}
684        }
685        c.mbtn &= !(1 << btn);
686    }
687
688    // wm_move / wm_menu(x, y) / wm_resize(edge): plat::begin_move, window_menu (Linux; the
689    // OS's own caption has it elsewhere), begin_resize -- headless runs only record them
690    fn wm_move(&mut self) {
691        if self.headless {
692            self.m.chrome.wm_req = WMR_MOVE;
693        } else {
694            plat::begin_move();
695        }
696    }
697
698    fn wm_menu(&mut self, _x: i32, _y: i32) {
699        if self.headless {
700            self.m.chrome.wm_req = WMR_MENU;
701            return;
702        }
703        #[cfg(target_os = "linux")]
704        plat::window_menu(_x, _y);
705    }
706
707    fn wm_resize(&mut self, edge: u32) {
708        if self.headless {
709            self.m.chrome.wm_req = WMR_RESIZE | edge << 8;
710        } else {
711            plat::begin_resize(edge);
712        }
713    }
714
715    // tb_side(right) -> the caption buttons of one end of the title bar, left to right
716    // (plat::TB_BTN_*, 0 ends): the system's layout, else the style's (SM_CAP_LEFT: close,
717    // minimize, maximize at the left; else minimize, maximize, close at the right)
718    fn tb_side(&self, right: bool) -> [u8; 3] {
719        if self.mobile() && self.uiw_cur == 0 {
720            return [0; 3]; // a phone's or a tablet's system owns the app's window (form.rs)
721        }
722        if let Some((l, r)) = self.tb_sys.buttons {
723            return if right { r } else { l };
724        }
725        match (self.style.metric(SM_CAP_LEFT) != 0, right) {
726            (true, false) => [plat::TB_BTN_CLOSE, plat::TB_BTN_MIN, plat::TB_BTN_MAX],
727            (false, true) => [plat::TB_BTN_MIN, plat::TB_BTN_MAX, plat::TB_BTN_CLOSE],
728            _ => [0; 3],
729        }
730    }
731
732    // tb_caps(buttons): the caption buttons of one end, in order
733    fn tb_caps(&mut self, b: [u8; 3]) {
734        for k in b {
735            match k {
736                plat::TB_BTN_CLOSE => self.tb_cap_close(),
737                plat::TB_BTN_MIN => self.tb_cap_min(),
738                plat::TB_BTN_MAX => self.tb_cap_max(),
739                _ => break,
740            }
741        }
742    }
743
744    // tb_cap_min / tb_cap_max / tb_cap_close: one caption button of the title bar and its
745    // window command
746    fn tb_cap_min(&mut self) {
747        if self.cap_button(TB_MIN_ID, IC_CHROME_MIN, HB_MIN, "Minimize") & EV_ACT != 0 {
748            self.tb_cmd(plat::WCMD_MINIMIZE);
749        }
750    }
751
752    fn tb_cap_max(&mut self) {
753        let max = self.wp.chrome.tb_state & plat::WST_MAXIMIZED != 0;
754        let (ic, l) = if max { (IC_CHROME_RESTORE, "Restore") } else { (IC_CHROME_MAX, "Maximize") };
755        if self.cap_button(TB_MAX_ID, ic, HB_MAX, l) & EV_ACT != 0 {
756            let max = self.wp.chrome.tb_state & plat::WST_MAXIMIZED != 0;
757            self.tb_cmd(if max { plat::WCMD_RESTORE } else { plat::WCMD_MAXIMIZE });
758        }
759    }
760
761    fn tb_cap_close(&mut self) {
762        if self.cap_button(TB_CLOSE_ID, IC_CHROME_CLOSE, HB_CLOSE, "Close") & EV_ACT != 0 {
763            self.tb_cmd(plat::WCMD_CLOSE);
764        }
765    }
766
767    // tb_cmd(cmd): plat::window_cmd (headless runs record it)
768    fn tb_cmd(&mut self, cmd: u32) {
769        if self.headless {
770            self.m.chrome.wm_req = WMR_CMD | cmd << 8;
771        } else {
772            plat::window_cmd(cmd);
773        }
774    }
775
776    // cap_button(id, icon, hbflags, label) -> EV: a caption button over the full height of
777    // the title bar (close: red hover with a white glyph)
778    fn cap_button(&mut self, id: u32, icon: u32, hb: u32, label: &str) -> u32 {
779        let st = self.style;
780        let bh = self.box_rect()[3]; // bar height
781        let w = if self.m.chrome.cap_w != 0 { self.m.chrome.cap_w } else { st.metric(SM_CAP_W) };
782        let w = self.dp(w);
783        self.place(w, bh);
784        let n = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
785        let ev = self.behave(id, hb);
786        // its hit box, for edge_grips
787        if let Some(h) = self.wp.ctx.hits[self.wp.ctx.hit_cur].get(n).copied() {
788            let c = &mut self.wp.chrome;
789            if let Some(slot) = c.cap_hits.get_mut(c.cap_n as usize) {
790                *slot = h;
791                c.cap_n += 1;
792            }
793        }
794        // paint (style)
795        let active = (self.wp.chrome.tb_state & plat::WST_ACTIVE != 0) as i32;
796        let mut sp = Sp { ev, flags: hb, icon, value: active, ..Default::default() };
797        sp.place(self.wp.ctx.last_rect);
798        st.caption_button(self, &sp);
799        self.sem(K_BUTTON, id, Some(label), 0);
800        ev
801    }
802
803    // ------------------------------------------------------------------ window chrome
804
805    /// win_chrome_begin(i): (window i is live and its pass has begun) the border, the title
806    /// bar (drag region with the title, close button) and the content box of a secondary
807    /// window
808    pub(crate) fn win_chrome_begin(&mut self, i: u32) {
809        let st = self.style;
810        let iu = (i as usize) & (WIN_MAX - 1);
811        let (flags, tbh) = (self.m.win.recs[iu].flags, self.m.win.recs[iu].tbh);
812        let ids = i << 4 | WCH_BASE; // chrome ids
813        self.wp.chrome.wc_footer = false;
814        // active look: tool windows share the app's activation, others their own
815        let mut tbs = 0;
816        if self.uiw_app_active() {
817            tbs = plat::WST_ACTIVE;
818            if flags & WF_TOOL == 0 && i != self.m.win.active {
819                tbs = 0;
820            }
821        }
822        self.wp.chrome.tb_state = tbs;
823        let b = self.uiw_border();
824        if b != 0 {
825            // paint (style): the window border
826            let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
827            let sp = Sp { x: 0, y: 0, w, h, value: b, ..Default::default() };
828            st.window_border(self, &sp);
829        }
830        if tbh != 0 {
831            self.next_size(0, tbh);
832            self.box_begin(ids, BOX_H | BOX_XCENTER, 0);
833            // background (style)
834            let mut sp = Sp { value: (tbs & plat::WST_ACTIVE != 0) as i32, ..Default::default() };
835            sp.place(self.wp.ctx.last_rect);
836            st.window_titlebar(self, &sp);
837            // the whole bar is a drag region (the platform moves the window: Windows through
838            // WM_NCHITTEST, Linux with a press here)
839            self.behave(ids, HB_DRAG);
840            if self.m.win.recs[(i as usize) & (WIN_MAX - 1)].flags & WF_TOUCH_HOLD != 0 {
841                self.touch_grab(super::touch::AX_ALL | (TF_HOLD | TF_HOLD_LONG) as u8);
842                self.touch_hold_show(ids);
843            }
844            if !self.headless && self.wp.ctx.active == ids && self.wp.ctx.pressed & (1 << BTN_LEFT) != 0 {
845                plat::begin_move(); // (the live window: plat::win_use)
846                self.wp.ctx.active = 0; // the OS owns the pointer now
847                self.wp.ctx.mbtn &= !(1 << BTN_LEFT);
848            } else if self.headless && self.mptype == PTR_TOUCH && self.wp.ctx.active == ids && self.wp.ctx.mbtn & (1 << BTN_LEFT) != 0 {
849                let pressed = self.wp.ctx.pressed & (1 << BTN_LEFT) != 0;
850                self.touch_window_drag(i, pressed); // (a script's finger moves it as the platform would)
851            }
852            let title = alloc::string::String::from(self.uiw_title(i));
853            self.wp.ctx.last_state = 0;
854            self.sem(K_TITLEBAR, ids, Some(&title), 0);
855            self.spacer(st.metric(SM_WC_TITLE_PAD)); // space before the title (DIP)
856            // close button at the left end (SM_CAP_LEFT: macOS; the system's layout puts it
857            // there; a layout without it keeps the style's side: the window has no other way
858            // to be closed with the pointer)
859            let mut capw = 0; // its width (DIP)
860            let left = match self.tb_sys.buttons {
861                Some((l, r)) if l.contains(&plat::TB_BTN_CLOSE) || r.contains(&plat::TB_BTN_CLOSE) => l.contains(&plat::TB_BTN_CLOSE) as i32,
862                _ => st.metric(SM_CAP_LEFT),
863            };
864            if left != 0 {
865                capw = self.wc_close_btn(ids, i);
866                if capw != 0 {
867                    self.spacer(st.metric(SM_WC_TITLE_PAD));
868                }
869            }
870            // the title fills the bar and is cut with "…" in narrow windows, so the close
871            // button always stays inside (U3)
872            self.next_size(SIZE_FILL, 0);
873            let mut ts = st.metric(SM_TB_TITLE_TS) as u32 | TX_ELLIPSIS;
874            if tbs & plat::WST_ACTIVE == 0 {
875                ts |= TX_DISABLED;
876            }
877            self.text(ts, &title);
878            self.spacer(st.metric(SM_WC_TITLE_GAP)); // space after the title (DIP)
879            if left == 0 {
880                self.wc_close_btn(ids, i);
881            } else if capw != 0 {
882                // left close button: the same space at the right keeps the title centred
883                self.spacer(capw);
884                self.spacer(st.metric(SM_WC_TITLE_PAD));
885            }
886            self.box_end();
887        }
888        // content
889        self.next_size(0, -1);
890        // (form.rs) on a phone or a tablet a window the screen cut shorter than its content rolls
891        // it (a dialog's footer stays below); a desktop's windows never change
892        let roll = self.mobile() && {
893            let mut natural = self.slot(ids + WCH_CONTENT).meash;
894            if flags & WF_DIALOG != 0 {
895                natural += self.slot(ids + WCH_FOOTER).meash;
896            }
897            natural > self.wp.rt.fb.h - b * 2 - self.dp(tbh)
898        };
899        self.wp.chrome.wc_roll = roll;
900        let fixed = if roll { 0 } else { BOX_FIXED };
901        if roll {
902            self.scroll_begin(ids + WCH_ROLL, SC_V);
903            self.next_size(-1, 0);
904        }
905        if flags & WF_DIALOG != 0 {
906            let gap = st.metric(SM_DLG_CONTENT_GAP);
907            let bf = st.metric(SM_DLG_CONTENT_BOX) as u32 | fixed;
908            self.box_begin(ids + WCH_CONTENT, bf, gap);
909            // dialog: the content area on the layer colour, the footer on the background
910            let mut sp = Sp::default();
911            sp.place(self.wp.ctx.last_rect);
912            st.window_dialog_fill(self, &sp);
913        } else {
914            self.box_begin(ids + WCH_CONTENT, BOX_V | fixed, 0);
915        }
916    }
917
918    // wc_close_btn(ids, i) -> button width (DIP; 0 = none, WF_NOCLOSE): the close button of
919    // a secondary window's title bar and its command
920    fn wc_close_btn(&mut self, ids: u32, i: u32) -> i32 {
921        let (flags, id) = {
922            let r = &self.m.win.recs[(i as usize) & (WIN_MAX - 1)];
923            (r.flags, r.id)
924        };
925        if flags & WF_NOCLOSE != 0 {
926            return 0;
927        }
928        let st = self.style;
929        let w = st.metric(if flags & WF_TOOL != 0 { SM_CAP_W_TOOL } else { SM_CAP_W });
930        // (for a finger, unless the window is so narrow that the title bar would be gone)
931        let wide = super::layout::px2dip_at(self.wp.rt.fb.w, self.wp.rt.dpi) >= super::touch::CAPTION_MIN_W;
932        let w = if self.m.touch.on && wide { w.max(super::touch::CAPTION_DIP) } else { w };
933        self.m.chrome.cap_w = w;
934        let ev = self.cap_button(ids + WCH_CLOSE, IC_CHROME_CLOSE, HB_CLOSE, "Close");
935        self.m.chrome.cap_w = 0;
936        if ev & EV_ACT != 0 {
937            self.cmd_run(UIC_WINCLOSE, id);
938        }
939        w
940    }
941
942    /// win_chrome_end(i) -> natural height of the content (DIP, content box + footer):
943    /// closes the content box (unless dialog_buttons did)
944    pub(crate) fn win_chrome_end(&mut self, i: u32) -> i32 {
945        let ids = i << 4 | WCH_BASE;
946        let footer = self.wp.chrome.wc_footer;
947        if !footer {
948            self.wc_pop_to(ids + WCH_CONTENT);
949            if core::mem::take(&mut self.wp.chrome.wc_roll) {
950                self.scroll_end();
951            }
952        }
953        let mut h = self.slot(ids + WCH_CONTENT).meash;
954        if footer {
955            h += self.slot(ids + WCH_FOOTER).meash;
956        }
957        self.px2dip(h)
958    }
959
960    // wc_pop_to(id): close boxes up to and including the box `id` (boxes the app left open
961    // inside it are closed with a warning); nothing when it is not open
962    fn wc_pop_to(&mut self, id: u32) {
963        for _ in 0..BOX_STACK_MAX {
964            let b = self.box_id();
965            if b == id {
966                self.box_end();
967                return;
968            }
969            if b == self.wp.lay.root_id {
970                return;
971            }
972            self.warn("ui_box_begin without ui_box_end in a window");
973            self.box_end();
974        }
975    }
976
977    // ------------------------------------------------------------------ edge grips
978
979    /// ui_edge_grips: invisible resize handles along the window edges for windows without a
980    /// system frame on Linux (AF_CUSTOM_TITLEBAR or the library's CSD title bar): a press
981    /// starts plat::begin_resize with the edge; the pointer shows the matching arrows.
982    /// Called by the runtime after App::frame (so the grips win the hit test); the caption
983    /// buttons of the pass are put on top of them again, so a button wins over the band
984    /// where it is (its top edge, the corner of a close button at the window's corner) and the
985    /// band stays usable elsewhere.  (GTK puts the band outside the visible frame, into the
986    /// shadow; the library's windows have no shadow margin.)  Nothing on Windows (WM_NCHITTEST
987    /// does it there) or while maximized / full screen, none on the tiled edges of a tiled
988    /// window (plat::WST_TILED_*: the screen edge stays the title bar's); headless runs only
989    /// with the forced library title bar (jfgui_CSD=1: scripts test the band), recording the
990    /// request.
991    pub(crate) fn edge_grips(&mut self) {
992        // (Windows: the system's frame, for a finger it is invisible and greedy: win_touch.rs)
993        let on = if self.headless { plat::getenv("jfgui_CSD").is_some_and(|v| v.starts_with('1')) } else { !cfg!(windows) || self.m.touch.on };
994        // (with a finger the main window fills the phone's page: its edges belong to the content)
995        if !on || (self.m.touch.on && self.uiw_cur == 0) {
996            return;
997        }
998        // ids: the main window's, else in the window's chrome range
999        let i = self.uiw_cur & (WIN_MAX as u32 - 1);
1000        self.m.chrome.grip_base = if i == 0 { GRIP_ID } else { (i << 4) + 8 | WCH_BASE };
1001        let state = if !self.headless {
1002            plat::window_state()
1003        } else if self.m.win.recs[i as usize].fs {
1004            plat::WST_FULLSCREEN
1005        } else {
1006            0
1007        };
1008        if state & (plat::WST_MAXIMIZED | plat::WST_FULLSCREEN) != 0 {
1009            return;
1010        }
1011        // tiled: no handles on the edges that touch the tiling (as GTK; a corner touching one
1012        // goes too)
1013        self.m.chrome.grip_off = if state & plat::WST_TILED != 0 { state >> plat::WST_TILED_SHIFT & 15 } else { 0 };
1014        let st = self.style;
1015        let t = self.dp(st.metric(SM_GRIP)); // edge handle thickness
1016        let c = self.dp(st.metric(SM_GRIP_CORNER)); // corner handle size
1017        // a finger needs more than a pointer (not at the top: the title bar moves the window)
1018        let (tf, cf) = if self.m.touch.on { (t.max(self.dp(14)), c.max(self.dp(32))) } else { (t, c) };
1019        let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1020        use plat::{EDGE_BOTTOM, EDGE_TOP};
1021        // right-to-left: the logical left edge is the window's right one
1022        let (left, right) = if self.rtl { (plat::EDGE_RIGHT, plat::EDGE_LEFT) } else { (plat::EDGE_LEFT, plat::EDGE_RIGHT) };
1023        let (nwse, nesw) = if self.rtl { (CUR_SIZE_NESW, CUR_SIZE_NWSE) } else { (CUR_SIZE_NWSE, CUR_SIZE_NESW) };
1024        let content = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
1025        // edges: top, bottom, left, right (between the corners)
1026        self.grip(0, EDGE_TOP, CUR_SIZE_NS, c, 0, w - c - c, t);
1027        self.grip(1, EDGE_BOTTOM, CUR_SIZE_NS, cf, h - tf, w - cf - cf, tf);
1028        self.grip(2, left, CUR_SIZE_WE, 0, c, tf, h - c - cf);
1029        self.grip(3, right, CUR_SIZE_WE, w - tf, c, tf, h - c - cf);
1030        // corners (squares)
1031        self.grip(4, EDGE_TOP | left, nwse, 0, 0, c, c);
1032        self.grip(5, EDGE_TOP | right, nesw, w - c, 0, c, c);
1033        self.grip(6, EDGE_BOTTOM | left, nesw, 0, h - cf, cf, cf);
1034        self.grip(7, EDGE_BOTTOM | right, nwse, w - cf, h - cf, cf, cf);
1035        let layer = self.wp.ctx.cmd_layer_ui as u8;
1036        // a finger's wider handles must not swallow the small widgets at the edge (the bottom bar
1037        // of a panel): those on top again too; a list or a bar stays below the handle
1038        if self.m.touch.on {
1039            let small = self.dp(super::touch::TARGET_DIP);
1040            let c = &mut self.wp.ctx;
1041            let list = &mut c.hits[c.hit_cur];
1042            for k in 0..content {
1043                let mut b = list[k];
1044                let (bw, bh) = (b.x1 - b.x0, b.y1 - b.y0);
1045                let widget = b.id != 0 && b.flags as u32 & (HB_BLOCK | HB_DRAG) == 0 && bw > 0 && bh > 0 && bw < small && bh < small;
1046                let edge = b.x0 < tf || b.x1 > w - tf || b.y1 > h - tf || (b.y1 > h - cf && (b.x0 < cf || b.x1 > w - cf));
1047                if widget && edge && list.len() < super::ctx::HITS_MAX {
1048                    b.continuation = true;
1049                    b.layer = b.layer.max(layer);
1050                    list.push(b);
1051                }
1052            }
1053        }
1054        // the caption buttons on top again (the same widget: continuation boxes)
1055        for k in 0..self.wp.chrome.cap_n as usize {
1056            let mut h = self.wp.chrome.cap_hits[k];
1057            h.continuation = true;
1058            h.layer = h.layer.max(layer);
1059            let c = &mut self.wp.ctx;
1060            if c.hits[c.hit_cur].len() < super::ctx::HITS_MAX {
1061                c.hits[c.hit_cur].push(h);
1062            }
1063        }
1064    }
1065
1066    // grip(k, edge, cursor, x, y, w, h): one handle; a press resizes
1067    #[allow(clippy::too_many_arguments)]
1068    fn grip(&mut self, k: u32, edge: u32, cur: u32, x: i32, y: i32, w: i32, h: i32) {
1069        if edge & self.m.chrome.grip_off != 0 {
1070            return; // a tiled edge
1071        }
1072        let id = k.wrapping_add(self.m.chrome.grip_base);
1073        self.hit_rect(id, x, y, w, h, (cur & 0xFF) << HB_CURSOR_SHIFT);
1074        if self.wp.ctx.active == id && self.wp.ctx.pressed & (1 << BTN_LEFT) != 0 {
1075            self.wm_resize(edge);
1076            if !self.headless {
1077                // the window manager owns the pointer now: forget the press
1078                self.wp.ctx.active = 0;
1079                self.wp.ctx.mbtn &= !(1 << BTN_LEFT);
1080            }
1081        }
1082    }
1083}