Skip to main content

ui/
inspect.rs

1// inspect -- the live inspector: an overlay in the app's windows and the tool window
2// "Inspektor" (ROADMAP §12)
3//
4// App-facing:
5//   inspect_enable(on)         arm the inspector: its hot keys open and close it (off: closed,
6//                              the keys reach the app again)
7//   inspect_show(on)           open / close it (opening arms it)
8//   inspect_open() -> bool     it is open
9//   inspect_keys(&[chord])     its hot keys, VK | MOD_* << 16 (default Ctrl+Shift+I and F12)
10// Command line: --inspect arms and opens it; jfgui_INSPECT=1 arms it, jfgui_INSPECT=open opens
11// it too.
12//
13// Closed (the default) it costs nothing: the runtime's hooks test one flag and nothing is
14// recorded or drawn.  Open:
15//   * the overlay in every window of the app: the widget under the pointer outlined, with a tag
16//     (kind, id in hex and its hashed name when known, label, size in DIP, state); the selected
17//     widget; optionally the layout boxes, the Tab order and the repainted rectangles of the
18//     last frames (flashing).  The overlay is drawn into the framebuffer after the app's
19//     commands, outside their lists: the app's dirty regions stay its own (ins_render_begin
20//     adds the overlay's old and new rectangles, which are redrawn from the app's commands)
21//   * the tool window "Inspektor" (WF_TOOL: it never takes the keyboard by itself; Esc, its
22//     close button and drops on it never reach the app): the widget tree of the current pass
23//     (the semantic records of the app's windows inside their layout boxes; collapsible,
24//     selectable; selecting highlights in the app), the properties of the selected widget, a
25//     script selector to copy (Ctrl+C or "Kopieren"), frame statistics, the theme's colour
26//     tokens; "Auswählen" picks a widget with a click into the app (the click does not reach
27//     it; Esc ends the pick)
28// The inspector's own window is left out of the tree, the overlay and the pick.
29// Development builds edit in it (inspect_edit.rs): the tab "Farben" is editable, the tab
30// "Darstellung" and the widget operations below the properties are added; and they show where
31// the app called the selected widget, with "Im Editor öffnen" (inspect_src.rs).
32//
33// Script commands (auto.rs `inspect ...`, sc_inspect):
34//   inspect on|off|toggle      open / close (on arms it)     inspect arm|disarm   hot keys
35//   inspect pick               the pick mode ("Auswählen")   inspect select S|none
36//   inspect option outlines|order|dirty on|off               inspect tab widgets|frame|colors|look
37//   inspect copy               copy the selector ("Kopieren")
38//   inspect snap               remember the main window's frame (for `inspect expect same`)
39//   inspect expect open|closed|idle|same|excluded            inspect expect pick|flash on|off
40//   inspect expect selected S|none   inspect expect hover S|none
41//   inspect expect selector "s"      inspect expect copied "s"
42// idle: the inspector did no work since it closed (or since the last idle check); same: the main
43// window renders byte for byte as at `inspect snap`; excluded: the tree holds no record of the
44// inspector's own window.  Editing (development builds): inspect set|reset|undo|redo|save|list|
45// do ..., inspect expect changes|list|value|undo|redo|message (inspect_edit.rs); the sources:
46// inspect editor "C"|none, inspect open [dry], inspect expect source|sources|opened|note
47// (inspect_src.rs).
48
49use alloc::collections::{BTreeMap, BTreeSet};
50use alloc::format;
51use alloc::string::String;
52use alloc::vec::Vec;
53
54use super::auto::{parse_qstring, skip_ws, word_eq, Sel};
55use super::cmd::{cmd_bbox, cmd_hash, render_layer, Cmd};
56use super::consts::*;
57use super::keys::*;
58use super::layout::px2dip_at;
59use super::{App, Sem, Shared, TreeRow, TreeState, Ui};
60use crate::i18n::tr;
61
62/// the window id of the inspector's tool window (UIC_WINCLOSE / UIC_ESCAPE / UIC_DROP with it
63/// never reach the app)
64pub(crate) const INSPECT_WIN: u32 = 0x7FFF_1000;
65
66// widget ids inside the inspector's window (its own hit list; below the hashed range, bits
67// 29 and 30 clear: the tree's chevrons take id | 0x4000_0000)
68const ID_ROOT: u32 = 0x07FE_0000;
69const ID_PICK: u32 = 0x07FE_0001;
70const ID_OUTL: u32 = 0x07FE_0002;
71const ID_ORDER: u32 = 0x07FE_0003;
72const ID_DIRTY: u32 = 0x07FE_0004;
73const ID_COPY: u32 = 0x07FE_0005;
74const ID_TABS: u32 = 0x07FE_0010; // + 1 + i: the tabs
75const ID_TOKENS: u32 = 0x07FE_0020;
76const ID_TREE: u32 = 0x07FE_1000; // + 1 + i: the rows
77
78const WIN_W: i32 = 400; // the tool window's first size (DIP)
79const WIN_H: i32 = 660;
80const ROW_H: i32 = 22; // tree rows (DIP)
81const KEY_W: i32 = 140; // property names (DIP)
82const LABEL_CUT: usize = 48; // characters of a label in the tree and the tag
83
84// overlay colours: the same on every theme (the overlay must stand out on all of them)
85const C_HOVER: u32 = 0xFFFF_8C00;
86const C_HOVER_FILL: u32 = 0x1CFF_8C00;
87const C_SEL: u32 = 0xFF1E_88E5;
88const C_SEL_FILL: u32 = 0x221E_88E5;
89const C_BOX: u32 = 0xB000_ACC1;
90const C_DIRTY: u32 = 0x00E5_3935; // alpha by age
91const C_TAG: u32 = 0xF021_2121;
92const C_TAG_TEXT: u32 = 0xFFFF_FFFF;
93const C_ORDER: u32 = 0xE06A_1B9A;
94const FLASH_MS: u32 = 700; // a repainted rectangle fades out in this time
95const FLASH_MAX: usize = 96;
96
97/// the names of the theme tokens (T_* / 4)
98use super::theme::TOKEN_NAMES;
99const LAYER_NAMES: [&str; L_COUNT] = ["base", "dialog", "popup", "tip"];
100
101/// where a widget was called: the file and line rustc gives #[track_caller] (inspect_src.rs;
102/// development builds only, a --release build has none)
103pub(crate) type Src = &'static core::panic::Location<'static>;
104
105/// a layout box begun or ended in a pass (box_begin / box_end while the inspector is open):
106/// its window, the number of semantic records before it, id, rectangle (window px, mirrored
107/// right-to-left like the records), layer, the call of its box_begin
108#[derive(Clone, Copy)]
109pub(crate) struct BoxEv {
110    win: u8,
111    begin: bool,
112    layer: u8,
113    at: u32,
114    id: u32,
115    rect: [i32; 4],
116    src: Option<Src>,
117}
118
119// what a node of the tree stands for
120const N_WIDGET: u8 = 0;
121const N_BOX: u8 = 1;
122const N_WIN: u8 = 2;
123
124/// a node of the widget tree: its record (boxes and windows as K_BOX records), depth, a key
125/// that stays the same from pass to pass, whether deeper nodes follow, its source
126struct Node {
127    rec: Sem,
128    what: u8,
129    /// the index of the semantic record (widgets), usize::MAX for boxes and windows
130    sem: usize,
131    depth: u16,
132    key: u64,
133    kids: bool,
134    src: Option<Src>,
135}
136
137/// statistics: the last run_passes, the last render of each window (app windows)
138#[derive(Clone, Copy, Default)]
139struct Stats {
140    passes: u32,
141    build_us: u64,
142    render_us: [u64; WIN_MAX],
143    dirty_px: [i64; WIN_MAX],
144    area_px: [i64; WIN_MAX],
145    cmds: [u32; WIN_MAX],
146    bytes: [u32; WIN_MAX],
147    cap: [u32; WIN_MAX],
148}
149
150/// the inspector's state (ui.m.ins)
151pub(crate) struct S {
152    /// the hot keys work
153    pub(crate) armed: bool,
154    /// open: the overlay and the tool window
155    pub(crate) on: bool,
156    /// closed: its window is destroyed after the passes (ins_gone)
157    pub(crate) gone: bool,
158    keys: Vec<u32>,
159    outlines: i32,
160    order: i32,
161    flash: i32,
162    tab: i32,
163    picking: bool,
164    /// a pick to resolve in the next pass: window, point (px of the framebuffer)
165    pick_at: Option<(u32, i32, i32)>,
166    /// buttons whose release belongs to a pick (bit per BTN_*)
167    up_eat: u32,
168    /// the selected node (0 = none) and its record in the last pass
169    sel: u64,
170    pub(crate) sel_rec: Option<Sem>,
171    /// ... where the app called it (development builds, inspect_src.rs)
172    pub(crate) sel_src: Option<Src>,
173    /// passes that still scroll the selected row into view (the view's height may change
174    /// with the properties shown below it)
175    reveal: u8,
176    tree: TreeState,
177    collapsed: BTreeSet<u64>,
178    /// the boxes of the current pass (all windows)
179    pub(crate) boxes: Vec<BoxEv>,
180    /// the pointer in each window (framebuffer px, -1 = outside), what is under it
181    ptr: [(i32, i32); WIN_MAX],
182    hover_key: [u64; WIN_MAX],
183    /// the overlay drawn into each window: commands, their rectangles, hash
184    ov: [Vec<Cmd>; WIN_MAX],
185    ov_box: [Vec<[i32; 4]>; WIN_MAX],
186    ov_hash: [u64; WIN_MAX],
187    /// the live window's overlay is drawn after its render; when the render started
188    ov_now: bool,
189    t_render: u64,
190    /// repainted rectangles: window, rectangle (px), time (ms)
191    flashes: Vec<(u8, [i32; 4], u32)>,
192    st: Stats,
193    open: Shared<i32>,
194    rect: Shared<WinRect>,
195    /// work done (passes, renders) -- a script checks that a closed inspector does none
196    work: u32,
197    work_mark: u32,
198    /// `inspect snap`: the main window's frame (w, h, pixels)
199    snap: Option<(i32, i32, Vec<u8>)>,
200}
201
202impl Default for S {
203    fn default() -> S {
204        S {
205            armed: false,
206            on: false,
207            gone: false,
208            keys: alloc::vec![VK_I | (MOD_CTRL | MOD_SHIFT) << 16, VK_F12],
209            outlines: 0,
210            order: 0,
211            flash: 0,
212            tab: 0,
213            picking: false,
214            pick_at: None,
215            up_eat: 0,
216            sel: 0,
217            sel_rec: None,
218            sel_src: None,
219            reveal: 0,
220            tree: TreeState::default(),
221            collapsed: BTreeSet::new(),
222            boxes: Vec::new(),
223            ptr: [(-1, -1); WIN_MAX],
224            hover_key: [0; WIN_MAX],
225            ov: Default::default(),
226            ov_box: Default::default(),
227            ov_hash: [0; WIN_MAX],
228            ov_now: false,
229            t_render: 0,
230            flashes: Vec::new(),
231            st: Stats::default(),
232            open: Shared::new(0),
233            rect: Shared::default(),
234            work: 0,
235            work_mark: 0,
236            snap: None,
237        }
238    }
239}
240
241// ------------------------------------------------------------------ helpers
242
243// splitmix64 of h ^ v: node keys
244fn mix(h: u64, v: u64) -> u64 {
245    let mut z = (h ^ v).wrapping_add(0x9E37_79B9_7F4A_7C15);
246    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
247    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
248    z ^ (z >> 31)
249}
250
251// FNV-1a 64 of a text
252fn hash_str(s: &str) -> u64 {
253    s.bytes().fold(0xCBF2_9CE4_8422_2325, |h, b| (h ^ b as u64).wrapping_mul(0x0000_0100_0000_01B3))
254}
255
256// the key of the n-th node with this base key in the pass (never 0)
257fn ordinal(seen: &mut BTreeMap<u64, u32>, base: u64) -> u64 {
258    let n = seen.entry(base).or_insert(0);
259    let k = mix(base, *n as u64);
260    *n += 1;
261    k.max(1)
262}
263
264// the point lies in the rectangle (x, y, w, h)
265fn inside(r: &[i32; 4], x: i32, y: i32) -> bool {
266    r[2] > 0 && r[3] > 0 && x >= r[0] && y >= r[1] && x < r[0].wrapping_add(r[2]) && y < r[1].wrapping_add(r[3])
267}
268
269// the selector form of an id: #123 (small ids written by hand), #0x0812ABCD
270fn id_text(id: u32) -> String {
271    if id < 0x1_0000 { format!("#{}", id) } else { format!("#{:#010X}", id).replace("0X", "0x") }
272}
273
274// a label cut to LABEL_CUT characters (…), line breaks as blanks
275fn cut(l: &str) -> String {
276    let mut s: String = l.chars().take(LABEL_CUT).map(|c| if c == '\n' || c == '\t' { ' ' } else { c }).collect();
277    if l.chars().count() > LABEL_CUT {
278        s.push('…');
279    }
280    s
281}
282
283// a label as a script string: " \ and control characters escaped (auto.rs parse_qstring)
284fn quote(l: &str) -> String {
285    let mut s = String::from("\"");
286    for c in l.chars() {
287        match c {
288            '"' => s.push_str("\\\""),
289            '\\' => s.push_str("\\\\"),
290            '\n' => s.push_str("\\n"),
291            '\t' => s.push_str("\\t"),
292            c => s.push(c),
293        }
294    }
295    s.push('"');
296    s
297}
298
299fn kind_name(k: u16) -> &'static str {
300    KIND_NAMES[if k < K_KIND_COUNT { k as usize } else { 0 }]
301}
302
303fn state_text(st: u16) -> String {
304    let mut s = String::new();
305    for (k, n) in STATE_NAMES.iter().enumerate() {
306        if st & (1 << k) != 0 {
307            if !s.is_empty() {
308                s.push_str(", ");
309            }
310            s.push_str(n);
311        }
312    }
313    s
314}
315
316// a frame of thickness t around r (4 rectangles: crisp at every scale)
317fn frame(ov: &mut Vec<Cmd>, r: [i32; 4], t: i32, c: u32) {
318    let [x, y, w, h] = r;
319    if w <= 0 || h <= 0 {
320        return;
321    }
322    let t = t.min(w).min(h).max(1);
323    ov.push(Cmd::Rect { x, y, w, h: t, c });
324    ov.push(Cmd::Rect { x, y: y + h - t, w, h: t, c });
325    if h > 2 * t {
326        ov.push(Cmd::Rect { x, y: y + t, w: t, h: h - 2 * t, c });
327        ov.push(Cmd::Rect { x: x + w - t, y: y + t, w: t, h: h - 2 * t, c });
328    }
329}
330
331// the area of the rectangles (x, y, w, h) as rectangles that do not overlap
332fn disjoint(rs: &[[i32; 4]]) -> Vec<[i32; 4]> {
333    let mut out: Vec<[i32; 4]> = Vec::new();
334    for r in rs {
335        let mut pieces = alloc::vec![[r[0], r[1], r[0] + r[2], r[1] + r[3]]]; // x0, y0, x1, y1
336        for o in &out {
337            let o = [o[0], o[1], o[0] + o[2], o[1] + o[3]];
338            let mut next = Vec::new();
339            for p in pieces {
340                if p[0] >= o[2] || p[2] <= o[0] || p[1] >= o[3] || p[3] <= o[1] {
341                    next.push(p);
342                    continue;
343                }
344                // the parts of p outside o: above, below, left, right
345                let (y0, y1) = (p[1].max(o[1]), p[3].min(o[3]));
346                for q in [[p[0], p[1], p[2], o[1]], [p[0], o[3], p[2], p[3]], [p[0], y0, o[0], y1], [o[2], y0, p[2], y1]] {
347                    let q = [q[0].max(p[0]), q[1].max(p[1]), q[2].min(p[2]), q[3].min(p[3])];
348                    if q[2] > q[0] && q[3] > q[1] {
349                        next.push(q);
350                    }
351                }
352            }
353            pieces = next;
354        }
355        out.extend(pieces.iter().map(|p| [p[0], p[1], p[2] - p[0], p[3] - p[1]]));
356    }
357    out
358}
359
360// two records are the same widget of the same pass
361fn same(a: &Sem, b: &Sem) -> bool {
362    a.win == b.win && a.id == b.id && a.kind == b.kind && a.rect == b.rect && a.label == b.label && a.layer == b.layer
363}
364
365impl Ui {
366    // ------------------------------------------------------------------ app API
367
368    /// arm the inspector (`--inspect`, `jfgui_INSPECT=1`): its hot keys (Ctrl+Shift+I and F12,
369    /// inspect_keys) open and close it; false closes it, the keys reach the app again
370    pub fn inspect_enable(&mut self, on: bool) {
371        self.m.ins.armed = on;
372        if !on {
373            self.inspect_show(false);
374        }
375    }
376
377    /// open / close the inspector: its overlay in the app's windows and its tool window
378    /// "Inspektor" (opening arms it); closed it costs nothing
379    pub fn inspect_show(&mut self, on: bool) {
380        let s = &mut self.m.ins;
381        if on {
382            s.armed = true;
383        }
384        if on == s.on {
385            return;
386        }
387        s.on = on;
388        s.gone = !on;
389        s.boxes.clear();
390        s.picking = false;
391        s.pick_at = None;
392        if on {
393            s.open.set(1);
394        } else {
395            s.open.set(0);
396            s.flashes.clear();
397            s.ptr = [(-1, -1); WIN_MAX];
398            s.hover_key = [0; WIN_MAX];
399            s.ov_hash = [0; WIN_MAX];
400            for v in s.ov.iter_mut() {
401                v.clear();
402            }
403            for v in s.ov_box.iter_mut() {
404                v.clear();
405            }
406            s.work_mark = s.work;
407            self.src_clear(); // the records' sources (inspect_src.rs)
408            // the overlay's pixels: every window is drawn again from its commands
409            self.dirty_force_full();
410            for r in self.m.win.recs.iter_mut() {
411                if let Some(p) = r.save.as_deref_mut() {
412                    p.dirty.full = true;
413                }
414            }
415        }
416        self.invalidate();
417        self.need_render = true;
418    }
419
420    /// the inspector is open
421    pub fn inspect_open(&self) -> bool {
422        self.m.ins.on
423    }
424
425    /// the inspector's hot keys: VK | MOD_* << 16 each (MOD_SHIFT / MOD_CTRL / MOD_ALT);
426    /// default Ctrl+Shift+I and F12, none: only the API and `--inspect` open it
427    pub fn inspect_keys(&mut self, keys: &[u32]) {
428        self.m.ins.keys = keys.to_vec();
429    }
430
431    /// (runtime: prepare) --inspect: armed and open; jfgui_INSPECT=1 armed, =open also open
432    pub(crate) fn inspect_args(&mut self, flag: bool) {
433        let env = plat::env("jfgui_INSPECT");
434        let env = env.as_deref().unwrap_or("");
435        if flag || env == "open" {
436            self.inspect_show(true);
437        } else if env.starts_with('1') {
438            self.inspect_enable(true);
439        }
440    }
441
442    // the inspector's window index (-1: not created)
443    fn ins_win(&self) -> i32 {
444        self.uiw_index(INSPECT_WIN)
445    }
446
447    // ------------------------------------------------------------------ runtime hooks
448
449    /// (process_events, while armed) the hot keys and the pick take their events: 0 the event
450    /// goes on to the app, 1 taken (passes), 2 taken (nothing to build)
451    pub(crate) fn ins_event(&mut self, ev: &plat::Event) -> u32 {
452        let w = ev.win as u32 & (WIN_MAX as u32 - 1);
453        let on = self.m.ins.on;
454        match ev.typ {
455            plat::E_KEY_DOWN => {
456                let chord = (ev.data & 0xFFFF) | (ev.mods as u32 & (MOD_SHIFT | MOD_CTRL | MOD_ALT)) << 16;
457                if self.m.ins.keys.contains(&chord) {
458                    self.inspect_show(!on);
459                    return 1;
460                }
461                if on && self.m.ins.picking && ev.data & 0xFFFF == VK_ESCAPE {
462                    self.m.ins.picking = false;
463                    self.invalidate();
464                    return 1;
465                }
466                0
467            }
468            plat::E_DOWN if on && self.m.ins.picking && w as i32 != self.ins_win() => {
469                let (x, y) = if ev.x != -1 { (ev.x, ev.y) } else { self.m.ins.ptr[w as usize] };
470                self.m.ins.pick_at = Some((w, x, y));
471                self.m.ins.picking = false;
472                self.m.ins.up_eat |= 1 << (ev.btn & 31);
473                self.invalidate();
474                1
475            }
476            plat::E_UP if self.m.ins.up_eat & 1 << (ev.btn & 31) != 0 => {
477                self.m.ins.up_eat &= !(1 << (ev.btn & 31));
478                2
479            }
480            plat::E_MOVE | plat::E_DOWN | plat::E_UP if on && ev.x != -1 => {
481                self.ins_pointer(w, ev.x, ev.y);
482                0
483            }
484            plat::E_LEAVE if on => {
485                self.ins_pointer(w, -1, -1);
486                0
487            }
488            plat::E_FOCUS_OUT => {
489                self.m.ins.up_eat = 0; // (a release the window will not see)
490                0
491            }
492            _ => 0,
493        }
494    }
495
496    // the pointer is at (x, y) of window w (-1: it left): the overlay follows what is under it
497    fn ins_pointer(&mut self, w: u32, x: i32, y: i32) {
498        for i in 0..WIN_MAX {
499            let p = if i as u32 == w { (x, y) } else { (-1, -1) };
500            if self.m.ins.ptr[i] == p {
501                continue;
502            }
503            self.m.ins.ptr[i] = p;
504            let k = self.ins_at(i as u32, p.0, p.1).map_or(0, |k| {
505                let r = &self.sems[k];
506                mix(mix(r.id as u64, r.kind as u64), (r.rect[0] as u64) << 32 ^ r.rect[1] as u32 as u64).max(1)
507            });
508            if k != self.m.ins.hover_key[i] {
509                self.m.ins.hover_key[i] = k;
510                self.need_render = true;
511            }
512        }
513    }
514
515    /// (run_passes: UIC_WINCLOSE / UIC_ESCAPE / UIC_DROP of the inspector's window) taken
516    pub(crate) fn ins_cmd(&mut self, id: u32) -> bool {
517        match id {
518            UIC_WINCLOSE => {
519                self.inspect_show(false);
520                true
521            }
522            UIC_ESCAPE => {
523                self.m.ins.picking = false;
524                true
525            }
526            UIC_DROP => true,
527            _ => false,
528        }
529    }
530
531    /// (layout.rs box_begin, while open) a box of the live window begins at r (px)
532    pub(crate) fn ins_box(&mut self, id: u32, r: [i32; 4]) {
533        let rect = self.phys_rect(r);
534        let (layer, at, src) = (self.wp.ctx.cmd_layer_ui as u8, self.sems.len() as u32, self.src_cur());
535        let e = BoxEv { win: self.uiw_cur as u8, begin: true, layer, at, id, rect, src };
536        self.m.ins.boxes.push(e);
537    }
538
539    /// (layout.rs box_end, while open) the live window's innermost box ends
540    pub(crate) fn ins_box_end(&mut self) {
541        let e = BoxEv { win: self.uiw_cur as u8, begin: false, layer: 0, at: self.sems.len() as u32, id: 0, rect: [0; 4], src: None };
542        self.m.ins.boxes.push(e);
543    }
544
545    // ------------------------------------------------------------------ the tree
546
547    // the semantic record under (x, y) of window w: the highest layer, the smallest one, the
548    // last one
549    fn ins_at(&self, w: u32, x: i32, y: i32) -> Option<usize> {
550        if x == -1 && y == -1 {
551            return None;
552        }
553        let mut best: Option<(usize, u8, i64)> = None;
554        for (i, r) in self.sems.iter().enumerate() {
555            if r.win as u32 != w || !inside(&r.rect, x, y) {
556                continue;
557            }
558            let area = r.rect[2] as i64 * r.rect[3] as i64;
559            if best.is_none_or(|(_, l, a)| r.layer > l || (r.layer == l && area <= a)) {
560                best = Some((i, r.layer, area));
561            }
562        }
563        best.map(|b| b.0)
564    }
565
566    // the tree of the current pass: per window of the app (the main window first) its records
567    // inside the layout boxes they were emitted in; boxes without records are left out
568    fn ins_tree(&self) -> Vec<Node> {
569        let iw = self.ins_win();
570        let mut wins: Vec<u32> = Vec::new();
571        for r in &self.sems {
572            let w = r.win as u32;
573            if w as i32 != iw && !wins.contains(&w) {
574                wins.push(w);
575            }
576        }
577        wins.sort_unstable();
578        let multi = wins.len() > 1;
579        let mut out: Vec<Node> = Vec::new();
580        let mut seen: BTreeMap<u64, u32> = BTreeMap::new();
581        for &w in &wins {
582            let base = multi as u16;
583            let wkey = mix(1, w as u64).max(1);
584            if multi {
585                let fb = self.win_fb(w);
586                let title = if w == 0 { String::from(tr("Hauptfenster")) } else { String::from(self.uiw_title(w)) };
587                let rec = Sem { kind: K_BOX, rect: [0, 0, fb.w, fb.h], label: Some(title), win: w as u8, id: self.uiw_rec_id(w), ..Sem::default() };
588                out.push(Node { rec, what: N_WIN, sem: usize::MAX, depth: 0, key: wkey, kids: false, src: None });
589            }
590            let evs: Vec<BoxEv> = self.m.ins.boxes.iter().filter(|e| e.win as u32 == w).copied().collect();
591            let mut ei = 0;
592            // open boxes: their node, whether a record is inside
593            let mut stack: Vec<(usize, bool)> = Vec::new();
594            let n = self.sems.len();
595            for k in 0..=n {
596                while ei < evs.len() && (k == n || evs[ei].at as usize <= k) {
597                    let e = evs[ei];
598                    ei += 1;
599                    if e.begin {
600                        let depth = base + stack.len() as u16;
601                        let key = ordinal(&mut seen, mix(mix(2, w as u64), e.id as u64));
602                        let rec = Sem { id: e.id, kind: K_BOX, rect: e.rect, layer: e.layer, win: w as u8, ..Sem::default() };
603                        out.push(Node { rec, what: N_BOX, sem: usize::MAX, depth, key, kids: false, src: e.src });
604                        stack.push((out.len() - 1, false));
605                    } else if let Some((i, used)) = stack.pop() {
606                        if !used {
607                            out.truncate(i); // an empty box (with its empty boxes)
608                        }
609                    }
610                }
611                if k == n {
612                    break;
613                }
614                let r = &self.sems[k];
615                if r.win as u32 != w {
616                    continue;
617                }
618                for s in stack.iter_mut().rev() {
619                    if s.1 {
620                        break; // (the boxes below are marked already)
621                    }
622                    s.1 = true;
623                }
624                let depth = base + stack.len() as u16;
625                let parent = stack.last().map_or(wkey, |s| out[s.0].key);
626                let b = if r.id != 0 {
627                    mix(mix(3, w as u64), (r.id as u64) << 16 | r.kind as u64)
628                } else {
629                    mix(mix(4, parent), hash_str(r.label.as_deref().unwrap_or("")) ^ r.kind as u64)
630                };
631                let key = ordinal(&mut seen, b);
632                out.push(Node { rec: r.clone(), what: N_WIDGET, sem: k, depth, key, kids: false, src: self.src_of(k) });
633            }
634            while let Some((i, used)) = stack.pop() {
635                if !used {
636                    out.truncate(i);
637                }
638            }
639        }
640        for i in 0..out.len() {
641            out[i].kids = out.get(i + 1).is_some_and(|n| n.depth > out[i].depth);
642        }
643        out
644    }
645
646    // the node a pick at (x, y) of window w selects: the widget there (ins_at's rule), else the
647    // smallest box
648    fn ins_pick_node(nodes: &[Node], w: u32, x: i32, y: i32) -> Option<usize> {
649        let mut best: Option<(usize, u8, u8, i64)> = None; // (node, widget?, layer, area)
650        for (i, n) in nodes.iter().enumerate() {
651            if n.what == N_WIN || n.rec.win as u32 != w || !inside(&n.rec.rect, x, y) {
652                continue;
653            }
654            let wd = (n.what == N_WIDGET) as u8;
655            let area = n.rec.rect[2] as i64 * n.rec.rect[3] as i64;
656            if best.is_none_or(|(_, bw, l, a)| (wd, n.rec.layer) > (bw, l) || ((wd, n.rec.layer) == (bw, l) && area <= a)) {
657                best = Some((i, wd, n.rec.layer, area));
658            }
659        }
660        best.map(|b| b.0)
661    }
662
663    // select node i: its ancestors open, the tree scrolls to it
664    fn ins_select(&mut self, nodes: &[Node], i: usize) {
665        self.m.ins.sel = nodes[i].key;
666        self.m.ins.sel_rec = Some(nodes[i].rec.clone());
667        self.m.ins.sel_src = nodes[i].src;
668        let mut d = nodes[i].depth;
669        for n in nodes[..i].iter().rev() {
670            if d == 0 {
671                break;
672            }
673            if n.depth < d {
674                self.m.ins.collapsed.remove(&n.key);
675                d = n.depth;
676            }
677        }
678        self.m.ins.reveal = 3;
679        self.m.ins.tab = 0;
680    }
681
682    // the script selector of a record: kind:"Label" when that names it alone, else #id, else
683    // its centre (DIP)
684    pub(crate) fn ins_selector(&self, r: &Sem) -> String {
685        let iw = self.ins_win();
686        if let Some(l) = r.label.as_deref().filter(|l| !l.is_empty() && l.len() < 250) {
687            let n = self.sems.iter().filter(|s| s.win as i32 != iw && s.kind == r.kind && s.label.as_deref() == Some(l)).count();
688            if n == 1 {
689                return format!("{}:{}", kind_name(r.kind), quote(l));
690            }
691        }
692        if r.id != 0 {
693            return id_text(r.id);
694        }
695        let dpi = self.win_dpi(r.win as u32);
696        format!("{},{}", px2dip_at(r.rect[0] + r.rect[2] / 2, dpi), px2dip_at(r.rect[1] + r.rect[3] / 2, dpi))
697    }
698
699    // copy the selected widget's selector (Ctrl+C, "Kopieren")
700    fn ins_copy(&mut self) {
701        if let Some(r) = self.m.ins.sel_rec.clone() {
702            let s = self.ins_selector(&r);
703            self.clip_set(CLIP_TEXT, s.as_bytes());
704        }
705    }
706
707    // ------------------------------------------------------------------ the tool window
708
709    /// (run_passes, after the main window's pass, while open) the inspector's part of the
710    /// pass: the tree of this pass, a pick, the tool window
711    pub(crate) fn inspect_frame(&mut self) {
712        if self.m.ins.open.get() == 0 {
713            self.inspect_show(false); // its window was closed
714            return;
715        }
716        self.m.ins.work = self.m.ins.work.wrapping_add(1);
717        let nodes = self.ins_tree();
718        if let Some((w, x, y)) = self.m.ins.pick_at.take() {
719            if let Some(i) = Ui::ins_pick_node(&nodes, w, x, y) {
720                self.ins_select(&nodes, i);
721                self.repass();
722            }
723        }
724        let sel = self.m.ins.sel;
725        let n = nodes.iter().find(|n| sel != 0 && n.key == sel);
726        self.m.ins.sel_rec = n.map(|n| n.rec.clone());
727        self.m.ins.sel_src = n.and_then(|n| n.src);
728        if self.m.ins.rect.get().flags & WRF_POS == 0 {
729            self.ins_place();
730        }
731        let open = self.m.ins.open.clone();
732        let rect = self.m.ins.rect.clone();
733        self.ied_plain(true); // (inspect_edit.rs) the style's own look: the edits do not reach it
734        if self.window_begin(INSPECT_WIN, tr("Inspektor"), Some(&open), WF_TOOL | WF_RESIZABLE, Some(&rect)) {
735            self.ins_ui(&nodes);
736        }
737        self.window_end();
738        self.ied_plain(false);
739        self.ied_after(); // the edits its controls asked for (inspect_edit.rs)
740    }
741
742    // the first placement: beside the main window when the monitor has room (headless:
743    // always), else inside its right edge
744    fn ins_place(&mut self) {
745        let dpi = self.win_dpi(0);
746        let fb = self.win_fb(0);
747        let cw = px2dip_at(fb.w, dpi);
748        let outside = self.headless
749            || match (plat::win_get_rect(0), plat::win_work_area(0)) {
750                (Some(r), Some(a)) => r[0] + r[2] + super::layout::dp_at(WIN_W + 24, dpi) <= a[0] + a[2],
751                _ => false,
752            };
753        let rc = if outside { WinRect::new(cw + 12, 0, WIN_W, WIN_H, WRF_POS) } else { WinRect::new((cw - WIN_W - 16).max(0), 48, WIN_W, WIN_H, WRF_POS) };
754        self.m.ins.rect.set(rc);
755    }
756
757    // the content of the tool window
758    fn ins_ui(&mut self, nodes: &[Node]) {
759        self.next_size(-1, -1);
760        self.box_begin(ID_ROOT, BOX_V | pad(8), 6);
761        // the pick and the overlay's options
762        self.box_begin(0, BOX_H | BOX_WRAP | BOX_XCENTER, 10);
763        let mut v = self.m.ins.picking as i32;
764        if self.toggle_button(ID_PICK, tr("Auswählen"), &mut v) & EV_ACT != 0 {
765            self.m.ins.picking = v != 0;
766        }
767        let mut v = self.m.ins.outlines;
768        self.checkbox(ID_OUTL, tr("Umrisse"), &mut v);
769        self.m.ins.outlines = v;
770        let mut v = self.m.ins.order;
771        self.checkbox(ID_ORDER, tr("Tab-Reihenfolge"), &mut v);
772        self.m.ins.order = v;
773        let mut v = self.m.ins.flash;
774        self.checkbox(ID_DIRTY, tr("Neu gezeichnet"), &mut v);
775        self.m.ins.flash = v;
776        self.box_end();
777        if self.m.ins.picking {
778            self.text(TS_CAPTION | TX_ACCENT, tr("Klicken Sie auf ein Widget (Esc beendet)"));
779        }
780        self.hr_ins_note(); // a failed build of hot reload (hotreload.rs)
781        let mut tab = self.m.ins.tab;
782        let mut tabs = alloc::vec![tr("Widgets"), tr("Bildaufbau"), tr("Farben")];
783        if super::inspect_edit::EDIT {
784            tabs.push(tr("Darstellung")); // (inspect_edit.rs) development builds
785        }
786        self.segmented(ID_TABS, &mut tab, &tabs);
787        self.m.ins.tab = tab;
788        match tab {
789            1 => self.ins_stats(nodes),
790            2 => {
791                if !self.ied_colors() {
792                    self.ins_colors(); // (a --release build: read only)
793                }
794            }
795            3 => self.ied_look(),
796            _ => {
797                self.ins_tree_ui(nodes);
798                self.ins_props();
799            }
800        }
801        // the window's keys: Ctrl+C copies the selector, Esc ends a pick (never UIC_ESCAPE)
802        if self.kb_chord(VK_C, MOD_CTRL) {
803            self.ins_copy();
804        }
805        self.ied_keys(); // Ctrl+Z / Ctrl+Y: the changes (inspect_edit.rs)
806        if self.key(VK_ESCAPE) {
807            self.m.ins.picking = false;
808            self.repass();
809        }
810        self.box_end();
811    }
812
813    // a node's row: label, detail
814    fn ins_row_text(&self, n: &Node) -> (String, Option<String>) {
815        match n.what {
816            N_WIN => (n.rec.label.clone().unwrap_or_default(), (n.rec.id != 0).then(|| id_text(n.rec.id))),
817            N_BOX => (String::from("box"), Some(id_text(n.rec.id))),
818            _ => {
819                let mut s = String::from(kind_name(n.rec.kind));
820                if let Some(l) = n.rec.label.as_deref().filter(|l| !l.is_empty()) {
821                    s.push(' ');
822                    s.push_str(&quote(&cut(l)));
823                }
824                (s, (n.rec.id != 0).then(|| id_text(n.rec.id)))
825            }
826        }
827    }
828
829    // the widget tree (the visible rows: collapsed nodes hide what is below them)
830    fn ins_tree_ui(&mut self, nodes: &[Node]) {
831        let mut vis: Vec<usize> = Vec::new();
832        let mut hide: Option<u16> = None;
833        for (i, n) in nodes.iter().enumerate() {
834            if let Some(d) = hide {
835                if n.depth > d {
836                    continue;
837                }
838                hide = None;
839            }
840            vis.push(i);
841            if n.kids && self.m.ins.collapsed.contains(&n.key) {
842                hide = Some(n.depth);
843            }
844        }
845        let texts: Vec<(String, Option<String>)> = vis.iter().map(|&i| self.ins_row_text(&nodes[i])).collect();
846        let rows: Vec<TreeRow> = vis
847            .iter()
848            .zip(texts.iter())
849            .map(|(&i, (l, d))| TreeRow {
850                key: nodes[i].key,
851                depth: nodes[i].depth,
852                has_children: nodes[i].kids,
853                check: None,
854                icon: 0,
855                label: l,
856                detail: d.as_deref(),
857            })
858            .collect();
859        let mut st = core::mem::take(&mut self.m.ins.tree);
860        st.expanded = nodes.iter().filter(|n| n.kids && !self.m.ins.collapsed.contains(&n.key)).map(|n| n.key).collect();
861        let sel = self.m.ins.sel;
862        st.cur = (sel != 0).then_some(sel);
863        self.next_size(-1, -1);
864        let ev = self.tree(ID_TREE, &mut st, &rows, ROW_H);
865        // a selected widget: its row into the middle of the view when it is not in view (in
866        // the next pass; the view's height may still change while the layout settles)
867        if self.m.ins.reveal > 0 {
868            self.m.ins.reveal -= 1;
869            let vh = self.last_rect()[3];
870            if let Some(p) = rows.iter().position(|r| r.key == sel) {
871                let rh = self.dp(ROW_H);
872                let y = p as i32 * rh;
873                let s = self.slot(ID_TREE);
874                if super::list::into_view(y, rh, s.scrolly, vh) != 0 {
875                    s.scrolly = (y - (vh - rh) / 2).max(0); // (the view clamps it)
876                    s.adur = s.scrolly as u32;
877                    self.repass();
878                }
879            }
880        }
881        if let Some(k) = ev.collapsed {
882            self.m.ins.collapsed.insert(k);
883        }
884        if let Some(k) = ev.expanded {
885            self.m.ins.collapsed.remove(&k);
886        }
887        if ev.sel_changed {
888            if let Some(k) = st.cur {
889                self.m.ins.sel = k;
890                let n = nodes.iter().find(|n| n.key == k);
891                self.m.ins.sel_rec = n.map(|n| n.rec.clone());
892                self.m.ins.sel_src = n.and_then(|n| n.src);
893            }
894        }
895        self.m.ins.tree = st;
896    }
897
898    // the properties of the selected node
899    fn ins_props(&mut self) {
900        self.separator();
901        let Some(r) = self.m.ins.sel_rec.clone() else {
902            self.text(TS_BODY | TX_SECONDARY, tr("Nichts ausgewählt"));
903            return;
904        };
905        let w = r.win as u32;
906        let dpi = self.win_dpi(w);
907        let d = |v: i32| px2dip_at(v, dpi);
908        self.key_value(tr("Art"), kind_name(r.kind), KEY_W);
909        let mut id = if r.id == 0 { String::from("0") } else { format!("{:#010X} ({})", r.id, r.id).replace("0X", "0x") };
910        if let Some(n) = self.id_name(r.id) {
911            id.push_str(&format!("  \"{}\"", n));
912        }
913        self.key_value(tr("Kennung"), &id, KEY_W);
914        let label = r.label.clone().unwrap_or_default();
915        self.key_value(tr("Beschriftung"), &cut(&label), KEY_W);
916        let rc = r.rect;
917        self.key_value(tr("Rechteck"), &format!("{}, {}  {} x {} DIP", d(rc[0]), d(rc[1]), d(rc[2]), d(rc[3])), KEY_W);
918        self.key_value("", &format!("{}, {}  {} x {} px", rc[0], rc[1], rc[2], rc[3]), KEY_W);
919        let value = if r.min != 0 { format!("{} / {} .. {}", r.value, r.min, r.max) } else { format!("{} / {}", r.value, r.max) };
920        self.key_value(tr("Wert"), &value, KEY_W);
921        let st = state_text(r.state);
922        self.key_value(tr("Zustand"), if st.is_empty() { "-" } else { &st }, KEY_W);
923        let layer = format!("{} ({})", r.layer, LAYER_NAMES[(r.layer as usize) & (L_COUNT - 1)]);
924        self.key_value(tr("Ebene"), &layer, KEY_W);
925        let win = if w == 0 { String::from(tr("Hauptfenster")) } else { format!("{} \"{}\"", self.uiw_rec_id(w), self.uiw_title(w)) };
926        self.key_value(tr("Fenster"), &win, KEY_W);
927        // the selector and its copy button
928        let sel = self.ins_selector(&r);
929        self.box_begin(0, BOX_H | BOX_XCENTER, 12);
930        self.next_size(KEY_W, 0);
931        self.text(TS_BODY | TX_SECONDARY | TX_ELLIPSIS, tr("Selektor"));
932        self.next_size(-1, 0);
933        self.text(TS_BODY | TX_ELLIPSIS, &sel);
934        self.next_flags(ST_COMPACT);
935        if self.button(ID_COPY, tr("Kopieren")) & EV_ACT != 0 {
936            self.ins_copy();
937        }
938        self.box_end();
939        if super::inspect_src::SRC {
940            let src = self.m.ins.sel_src;
941            self.isrc_props(src); // where the app called it, "Im Editor öffnen" (inspect_src.rs)
942        }
943        self.ied_operate(&r); // click, drag, set, type, cmd (inspect_edit.rs)
944    }
945
946    // the frame statistics
947    fn ins_stats(&mut self, nodes: &[Node]) {
948        let iw = self.ins_win();
949        let s = self.m.ins.st;
950        let app = |i: usize| i as i32 != iw;
951        let sum = |v: &[u64; WIN_MAX]| (0..WIN_MAX).filter(|&i| app(i)).map(|i| v[i]).sum::<u64>();
952        let sumi = |v: &[i64; WIN_MAX]| (0..WIN_MAX).filter(|&i| app(i)).map(|i| v[i]).sum::<i64>();
953        let sumu = |v: &[u32; WIN_MAX]| (0..WIN_MAX).filter(|&i| app(i)).map(|i| v[i] as u64).sum::<u64>();
954        self.key_value(tr("Durchgänge"), &format!("{}", s.passes), KEY_W);
955        self.key_value(tr("Aufbau"), &format!("{} µs", s.build_us), KEY_W);
956        self.key_value(tr("Zeichnen"), &format!("{} µs", sum(&s.render_us)), KEY_W);
957        let (px, area) = (sumi(&s.dirty_px), sumi(&s.area_px).max(1));
958        let pm = (px * 1000 / area) as i32;
959        self.key_value(tr("Neu gezeichnete Fläche"), &format!("{} px ({}.{} %)", px, pm / 10, pm % 10), KEY_W);
960        let cmds = format!("{} ({} / {} KiB)", sumu(&s.cmds), sumu(&s.bytes) / 1024, sumu(&s.cap) / 1024);
961        self.key_value(tr("Zeichenbefehle"), &cmds, KEY_W);
962        let widgets = nodes.iter().filter(|n| n.what == N_WIDGET).count();
963        self.key_value(tr("Widgets"), &format!("{}", widgets), KEY_W);
964        let (n, nmax, b, bmax) = self.text.gc_usage();
965        self.key_value(tr("Glyphen-Cache"), &format!("{} / {} ({} / {} KiB)", n, nmax, b / 1024, bmax / 1024), KEY_W);
966    }
967
968    // the theme's colour tokens: swatch, name, value
969    fn ins_colors(&mut self) {
970        self.next_size(-1, -1);
971        self.scroll_begin(ID_TOKENS, SC_V);
972        let stroke = self.color(T_CTRL_STROKE);
973        for (i, name) in TOKEN_NAMES.iter().enumerate() {
974            let c = self.theme[i];
975            self.box_begin(0, BOX_H | BOX_XCENTER, 10);
976            self.next_size(32, 18);
977            let r = self.place(0, 0);
978            // white and black halves under the colour show its alpha
979            let h2 = r[2] / 2;
980            self.draw_color(stroke);
981            self.draw_rect(r[0], r[1], r[2], r[3]);
982            self.draw_color(0xFFFF_FFFF);
983            self.draw_rect(r[0] + 1, r[1] + 1, h2 - 1, r[3] - 2);
984            self.draw_color(0xFF00_0000);
985            self.draw_rect(r[0] + h2, r[1] + 1, r[2] - h2 - 1, r[3] - 2);
986            self.draw_color(c);
987            self.draw_rect(r[0] + 1, r[1] + 1, r[2] - 2, r[3] - 2);
988            self.next_size(160, 0);
989            self.text(TS_CAPTION, &format!("T_{}", name));
990            self.text(TS_CAPTION | TX_SECONDARY, &format!("#{:08X}", c));
991            self.box_end();
992        }
993        self.scroll_end();
994    }
995
996    // ------------------------------------------------------------------ the overlay
997
998    /// (render_live, after dirty_compute, while open) n dirty rectangles of the live window's
999    /// commands (-1 = everything): noted (statistics, flashes); when the overlay changes the
1000    /// rectangles of the old and the new one join them -> the new count (-1 = everything)
1001    pub(crate) fn ins_render_begin(&mut self, n: i32) -> i32 {
1002        self.m.ins.ov_now = false;
1003        let w = self.uiw_cur;
1004        if w as i32 == self.ins_win() {
1005            return n; // the inspector itself: no overlay
1006        }
1007        let wi = w as usize & (WIN_MAX - 1);
1008        self.m.ins.work = self.m.ins.work.wrapping_add(1);
1009        self.m.ins.t_render = plat::time_us();
1010        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1011        // statistics and flashes: the app's own rectangles
1012        if n != 0 {
1013            let area = fw as i64 * fh as i64;
1014            let px = if n < 0 { area } else { self.drects[..n as usize].iter().map(|r| r[2] as i64 * r[3] as i64).sum() };
1015            self.m.ins.st.dirty_px[wi] = px;
1016            self.m.ins.st.area_px[wi] = area;
1017            if self.m.ins.flash != 0 {
1018                let now = self.now_ms;
1019                let rs: Vec<[i32; 4]> = if n < 0 { alloc::vec![[0, 0, fw, fh]] } else { self.drects[..n as usize].to_vec() };
1020                let f = &mut self.m.ins.flashes;
1021                f.retain(|e| now.wrapping_sub(e.2) < FLASH_MS);
1022                for r in rs {
1023                    f.push((w as u8, r, now));
1024                }
1025                if f.len() > FLASH_MAX {
1026                    let k = f.len() - FLASH_MAX;
1027                    f.drain(..k);
1028                }
1029            }
1030        }
1031        {
1032            let c = &self.wp.cmd;
1033            let st = &mut self.m.ins.st;
1034            st.cmds[wi] = c.layers.iter().map(|l| l.len() as u32).sum();
1035            st.bytes[wi] = c.len.iter().sum();
1036            st.cap[wi] = c.cap.iter().sum();
1037        }
1038        // the overlay; when it changed, its old and new rectangles are drawn again
1039        let ov = self.ins_overlay(w);
1040        let h = ov.iter().fold(0xCBF2_9CE4_8422_2325u64, |h, c| mix(h, cmd_hash(c)));
1041        let boxes: Vec<[i32; 4]> = ov
1042            .iter()
1043            .filter_map(cmd_bbox)
1044            .filter_map(|b| {
1045                let (x0, y0, x1, y1) = (b[0].max(0), b[1].max(0), b[2].min(fw), b[3].min(fh));
1046                (x1 > x0 && y1 > y0).then_some([x0, y0, x1 - x0, y1 - y0])
1047            })
1048            .collect();
1049        let mut n = n;
1050        if h != self.m.ins.ov_hash[wi] && n >= 0 {
1051            let old = core::mem::take(&mut self.m.ins.ov_box[wi]);
1052            self.drects.truncate(n as usize);
1053            if n as usize + old.len() + boxes.len() > super::dirty::DRECTS_MAX {
1054                self.drects.clear();
1055                n = -1;
1056            } else {
1057                self.drects.extend_from_slice(&old);
1058                self.drects.extend_from_slice(&boxes);
1059                n += (old.len() + boxes.len()) as i32;
1060            }
1061        }
1062        self.m.ins.ov_now = n != 0 && !ov.is_empty();
1063        self.m.ins.ov[wi] = ov;
1064        self.m.ins.ov_box[wi] = boxes;
1065        self.m.ins.ov_hash[wi] = h;
1066        // repainted rectangles still fading: more frames
1067        if self.m.ins.flash != 0 && !self.m.ins.flashes.is_empty() {
1068            self.animating = true;
1069        }
1070        n
1071    }
1072
1073    /// (render_live, after the app's commands were drawn, before --verify-dirty) the overlay
1074    /// on top, inside the rectangles that were drawn (split into disjoint pieces: the overlay's
1075    /// translucent fills are blended once)
1076    pub(crate) fn ins_render_end(&mut self) {
1077        let w = self.uiw_cur;
1078        if w as i32 == self.ins_win() {
1079            return;
1080        }
1081        let wi = w as usize & (WIN_MAX - 1);
1082        if self.m.ins.ov_now {
1083            let n = self.wp.rt.present_n;
1084            let pieces = if n < 0 { None } else { Some(disjoint(&self.drects[..(n as usize).min(self.drects.len())])) };
1085            self.ins_draw(false, pieces.as_deref());
1086        }
1087        self.m.ins.st.render_us[wi] = plat::time_us().saturating_sub(self.m.ins.t_render);
1088    }
1089
1090    /// (runtime verify_dirty, while open) the live window's overlay into the full redraw it
1091    /// compares with (shadow) or into the repaired framebuffer
1092    pub(crate) fn ins_verify(&mut self, shadow: bool) {
1093        if self.uiw_cur as i32 != self.ins_win() {
1094            self.ins_draw(shadow, None);
1095        }
1096    }
1097
1098    // draw the live window's overlay into its framebuffer or its shadow surface, inside the
1099    // rectangles (None: everywhere)
1100    fn ins_draw(&mut self, shadow: bool, clip: Option<&[[i32; 4]]>) {
1101        let wi = self.uiw_cur as usize & (WIN_MAX - 1);
1102        if self.m.ins.ov[wi].is_empty() {
1103            return;
1104        }
1105        let icon_style = (self.style, self.color(T_ACCENT), self.theme_dark, self.color(T_TEXT_DISABLED), self.wp.rt.dpi as u32);
1106        let ov = core::mem::take(&mut self.m.ins.ov[wi]);
1107        let Ui { wp, dc, text, icon_hook, .. } = self;
1108        let sf = if shadow { &mut wp.rt.shadow } else { &mut wp.rt.fb };
1109        dc.begin(sf);
1110        match clip {
1111            None => render_layer(&ov, 0, dc, sf, text, icon_hook, icon_style),
1112            Some(rs) => {
1113                for r in rs {
1114                    dc.clip_push(r[0], r[1], r[2], r[3]);
1115                    render_layer(&ov, 0, dc, sf, text, icon_hook, icon_style);
1116                    dc.clip_pop();
1117                }
1118            }
1119        }
1120        dc.end();
1121        self.m.ins.ov[wi] = ov;
1122    }
1123
1124    // the overlay of the live window w (framebuffer px)
1125    fn ins_overlay(&mut self, w: u32) -> Vec<Cmd> {
1126        let mut ov = Vec::new();
1127        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1128        let hair = self.hair();
1129        let thick = self.dp(2).max(1);
1130        // the layout boxes of the last pass
1131        if self.m.ins.outlines != 0 {
1132            for e in self.m.ins.boxes.iter().filter(|e| e.begin && e.win as u32 == w) {
1133                frame(&mut ov, e.rect, hair, C_BOX);
1134            }
1135        }
1136        // the repainted rectangles of the last frames, fading
1137        if self.m.ins.flash != 0 {
1138            let now = self.now_ms;
1139            for &(fwin, r, t) in &self.m.ins.flashes {
1140                let age = now.wrapping_sub(t);
1141                if fwin as u32 != w || age >= FLASH_MS {
1142                    continue;
1143                }
1144                let k = FLASH_MS - age;
1145                ov.push(Cmd::Rect { x: r[0], y: r[1], w: r[2], h: r[3], c: (0x48 * k / FLASH_MS) << 24 | C_DIRTY });
1146                frame(&mut ov, r, hair, (0x60 + 0x9F * k / FLASH_MS) << 24 | C_DIRTY);
1147            }
1148        }
1149        self.ts_font(TS_CAPTION);
1150        let (font, size) = self.text.get_font();
1151        let (asc, desc) = (self.text.ascent(), self.text.descent());
1152        let pad = self.dp(4);
1153        // the selection
1154        if let Some(r) = self.m.ins.sel_rec.as_ref().filter(|r| r.win as u32 == w) {
1155            ov.push(Cmd::Rect { x: r.rect[0], y: r.rect[1], w: r.rect[2], h: r.rect[3], c: C_SEL_FILL });
1156            frame(&mut ov, r.rect, thick, C_SEL);
1157        }
1158        // the widget under the pointer, with its tag
1159        let mut tag = Vec::new();
1160        let (px, py) = self.m.ins.ptr[w as usize & (WIN_MAX - 1)];
1161        if let Some(k) = self.ins_at(w, px, py) {
1162            let r = self.sems[k].clone();
1163            ov.push(Cmd::Rect { x: r.rect[0], y: r.rect[1], w: r.rect[2], h: r.rect[3], c: C_HOVER_FILL });
1164            frame(&mut ov, r.rect, hair.max(thick / 2), C_HOVER);
1165            let mut s = String::from(kind_name(r.kind));
1166            if r.id != 0 {
1167                s.push_str("  ");
1168                s.push_str(&id_text(r.id));
1169                if let Some(n) = self.id_name(r.id) {
1170                    s.push_str(&format!(" (\"{}\")", n));
1171                }
1172            }
1173            if let Some(l) = r.label.as_deref().filter(|l| !l.is_empty()) {
1174                s.push_str("  ");
1175                s.push_str(&quote(&cut(l)));
1176            }
1177            let dpi = self.wp.rt.dpi;
1178            s.push_str(&format!("  {} x {}", px2dip_at(r.rect[2], dpi), px2dip_at(r.rect[3], dpi)));
1179            let st = state_text(r.state);
1180            if !st.is_empty() {
1181                s.push_str(&format!("  [{}]", st));
1182            }
1183            let tw = self.text.width(&s).min(fw - 2 * pad).max(1);
1184            let (bw, bh) = (tw + 2 * pad, asc + desc + 2 * pad);
1185            let x = r.rect[0].clamp(0, (fw - bw).max(0));
1186            let above = r.rect[1] - bh - hair;
1187            let y = if above >= 0 { above } else { (r.rect[1] + r.rect[3] + hair).min((fh - bh).max(0)) };
1188            let t = hair as u16;
1189            tag.push(Cmd::RRect { x, y, w: bw, h: bh, r: self.dp(3) as u16, t, fill: C_TAG, stroke: C_HOVER, stroke2: C_HOVER });
1190            tag.push(Cmd::Text { x: x + pad, y: y + pad + asc, font: font as u16, size, c: C_TAG_TEXT, maxw: tw, s: s.into_bytes() });
1191        }
1192        // the Tab order: a number at each Tab stop (the last pass's hit list)
1193        if self.m.ins.order != 0 {
1194            let c = &self.wp.ctx;
1195            let list = &c.hits[c.hit_cur ^ 1];
1196            let (stops, _) = super::kbnav::stops(list, c.focus, c.kb.gmem());
1197            // (numbered in Tab order; stops scrolled out of their view keep their number)
1198            let rects: Vec<(usize, [i32; 4])> = stops
1199                .iter()
1200                .enumerate()
1201                .map(|(k, &i)| {
1202                    let h = &list[i];
1203                    (k, if self.rtl { [fw - h.x1, h.y0, h.x1 - h.x0, h.y1 - h.y0] } else { [h.x0, h.y0, h.x1 - h.x0, h.y1 - h.y0] })
1204                })
1205                .filter(|(_, r)| r[2] > 0 && r[3] > 0)
1206                .collect();
1207            for (k, r) in rects.iter() {
1208                let s = format!("{}", k + 1);
1209                let tw = self.text.width(&s);
1210                let bh = asc + desc + 2;
1211                let bw = (tw + 2 * pad).max(bh);
1212                // on the stop's top left corner (it covers little of the widget)
1213                let (x, y) = ((r[0] - bw / 2).clamp(0, (fw - bw).max(0)), (r[1] - bh / 2).clamp(0, (fh - bh).max(0)));
1214                frame(&mut ov, *r, hair, C_ORDER);
1215                ov.push(Cmd::RRect { x, y, w: bw, h: bh, r: (bh / 2) as u16, t: 0, fill: C_ORDER, stroke: 0, stroke2: 0 });
1216                ov.push(Cmd::Text { x: x + (bw - tw) / 2, y: y + 1 + asc, font: font as u16, size, c: C_TAG_TEXT, maxw: 0, s: s.into_bytes() });
1217            }
1218        }
1219        ov.append(&mut tag); // the tag above everything
1220        ov
1221    }
1222
1223    // ------------------------------------------------------------------ statistics
1224
1225    /// (run_passes, at its end, while open) passes and App::frame time of that run
1226    pub(crate) fn ins_passes(&mut self, passes: u32, build_us: u64) {
1227        self.m.ins.st.passes = passes;
1228        self.m.ins.st.build_us = build_us;
1229    }
1230
1231    /// (run_passes, at its end, after closing) the tool window's state and buffers are freed:
1232    /// the app's windows are as if the inspector never ran (its placement is kept)
1233    pub(crate) fn ins_gone(&mut self) {
1234        self.m.ins.gone = false;
1235        self.uiw_use(0);
1236        self.window_destroy(INSPECT_WIN);
1237    }
1238
1239    // ------------------------------------------------------------------ script commands
1240
1241    /// `inspect ...` (auto.rs; see the module header)
1242    pub(crate) fn sc_inspect(&mut self, app: &mut dyn App, a: &[u8]) {
1243        const BAD: &[u8] = b"inspect on|off|toggle|arm|disarm|pick|select S|option outlines|order|dirty on|off|tab widgets|frame|colors|look|copy|snap|expect ...|set|reset|undo|redo|save|list|do ...|editor \"C\"|open [dry]";
1244        let w = |n: &str| word_eq(a, n).is_some();
1245        if w("on") || w("off") || w("toggle") {
1246            let on = if w("toggle") { !self.m.ins.on } else { w("on") };
1247            self.inspect_show(on);
1248            self.run_passes(app);
1249        } else if w("arm") || w("disarm") {
1250            self.inspect_enable(w("arm"));
1251            self.run_passes(app);
1252        } else if w("pick") {
1253            if !self.m.ins.on {
1254                self.fail(b"the inspector is closed");
1255                return;
1256            }
1257            self.m.ins.picking = true;
1258            self.invalidate();
1259            self.run_passes(app);
1260        } else if let Some(r) = word_eq(a, "select") {
1261            let r = skip_ws(r);
1262            if word_eq(r, "none").is_some() {
1263                self.m.ins.sel = 0;
1264                self.m.ins.sel_rec = None;
1265                self.m.ins.sel_src = None;
1266            } else {
1267                let Some(k) = self.ins_resolve(r) else { return };
1268                let nodes = self.ins_tree();
1269                match nodes.iter().position(|n| n.sem == k) {
1270                    Some(i) => self.ins_select(&nodes, i),
1271                    None => {
1272                        self.fail(b"not in the inspector's tree");
1273                        return;
1274                    }
1275                }
1276            }
1277            self.invalidate();
1278            self.run_passes(app);
1279        } else if let Some(r) = word_eq(a, "option") {
1280            let r = skip_ws(r);
1281            let v = word_eq(skip_ws(super::auto::next_word(r)), "on").is_some() as i32;
1282            if word_eq(r, "outlines").is_some() {
1283                self.m.ins.outlines = v;
1284            } else if word_eq(r, "order").is_some() {
1285                self.m.ins.order = v;
1286            } else if word_eq(r, "dirty").is_some() {
1287                self.m.ins.flash = v;
1288            } else {
1289                self.fail(b"inspect option outlines|order|dirty on|off");
1290                return;
1291            }
1292            self.invalidate();
1293            self.run_passes(app);
1294        } else if let Some(r) = word_eq(a, "tab") {
1295            let r = skip_ws(r);
1296            // look: "Darstellung" (development builds, inspect_edit.rs)
1297            let n = if super::inspect_edit::EDIT { 4 } else { 3 };
1298            let t = ["widgets", "frame", "colors", "look"][..n].iter().position(|n| word_eq(r, n).is_some());
1299            match t {
1300                Some(t) => self.m.ins.tab = t as i32,
1301                None => {
1302                    self.fail(if n == 4 { &b"inspect tab widgets|frame|colors|look"[..] } else { b"inspect tab widgets|frame|colors" });
1303                    return;
1304                }
1305            }
1306            self.invalidate();
1307            self.run_passes(app);
1308        } else if w("copy") {
1309            self.ins_copy();
1310            self.run_passes(app);
1311        } else if w("snap") {
1312            self.render();
1313            self.gp_settle_all();
1314            let fb = &self.wp.rt.fb;
1315            self.m.ins.snap = Some((fb.w, fb.h, fb.px.clone()));
1316        } else if let Some(r) = word_eq(a, "expect") {
1317            self.sc_inspect_expect(skip_ws(r));
1318        } else if !self.isrc_script(app, a) && !self.ied_script(app, a) {
1319            // (editor, open: inspect_src.rs; set, reset, undo, redo, save, list, do:
1320            // inspect_edit.rs)
1321            self.fail(BAD);
1322        }
1323    }
1324
1325    // a selector of the app's widgets -> the index of its record (None: failed, reported)
1326    fn ins_resolve(&mut self, p: &[u8]) -> Option<usize> {
1327        match self.resolve(p).0 {
1328            None => {
1329                self.sel_fail();
1330                None
1331            }
1332            Some(Sel::Point) => {
1333                self.fail(b"a widget selector (#id or a label), not a point");
1334                None
1335            }
1336            Some(Sel::Rec(k)) => {
1337                if self.sems[k].win as i32 == self.ins_win() {
1338                    self.fail(b"a widget of the inspector itself");
1339                    return None;
1340                }
1341                Some(k)
1342            }
1343        }
1344    }
1345
1346    // inspect expect ...
1347    fn sc_inspect_expect(&mut self, a: &[u8]) {
1348        let w = |n: &str| word_eq(a, n).is_some();
1349        if w("open") || w("closed") {
1350            if self.m.ins.on != w("open") {
1351                self.fail(if self.m.ins.on { b"the inspector is open" } else { b"the inspector is closed" });
1352            }
1353        } else if let Some(r) = word_eq(a, "pick") {
1354            let want = word_eq(skip_ws(r), "on").is_some();
1355            if self.m.ins.picking != want {
1356                self.fail(if want { b"no pick" } else { b"picking" });
1357            }
1358        } else if let Some(r) = word_eq(a, "flash") {
1359            let now = self.now_ms;
1360            let live = self.m.ins.flash != 0 && self.m.ins.flashes.iter().any(|e| now.wrapping_sub(e.2) < FLASH_MS);
1361            if live != word_eq(skip_ws(r), "on").is_some() {
1362                self.fail(if live { b"repainted rectangles are shown" } else { b"no repainted rectangle is shown" });
1363            }
1364        } else if w("idle") {
1365            let d = self.m.ins.work.wrapping_sub(self.m.ins.work_mark);
1366            self.m.ins.work_mark = self.m.ins.work;
1367            let (b, s) = (self.m.ins.boxes.len(), self.src_count());
1368            if d != 0 || b != 0 || s != 0 || self.m.ins.ov.iter().any(|v| !v.is_empty()) {
1369                self.fail(format!("the inspector worked {} times, {} boxes, {} sources recorded", d, b, s).as_bytes());
1370            }
1371        } else if w("same") {
1372            self.render();
1373            let fb = &self.wp.rt.fb;
1374            let d = match &self.m.ins.snap {
1375                None => Err(String::from("no inspect snap")),
1376                Some((sw, sh, _)) if (*sw, *sh) != (fb.w, fb.h) => Err(format!("the size is {}x{}, was {}x{}", fb.w, fb.h, sw, sh)),
1377                Some((_, _, px)) => {
1378                    let n = px.chunks_exact(4).zip(fb.px.chunks_exact(4)).filter(|(a, b)| a[..3] != b[..3]).count();
1379                    if n == 0 { Ok(()) } else { Err(format!("{} pixels differ", n)) }
1380                }
1381            };
1382            if let Err(e) = d {
1383                self.fail(e.as_bytes());
1384            }
1385        } else if w("excluded") {
1386            let iw = self.ins_win();
1387            let own = iw >= 0 && self.sems.iter().any(|r| r.win as i32 == iw);
1388            if !own {
1389                self.fail(b"the inspector's window has no records");
1390            } else if self.ins_tree().iter().any(|n| n.rec.win as i32 == iw) {
1391                self.fail(b"the tree holds records of the inspector's window");
1392            }
1393        } else if let Some(r) = word_eq(a, "selected") {
1394            let r = skip_ws(r);
1395            if word_eq(r, "none").is_some() {
1396                if self.m.ins.sel_rec.is_some() {
1397                    self.fail(b"a widget is selected");
1398                }
1399                return;
1400            }
1401            let Some(k) = self.ins_resolve(r) else { return };
1402            match &self.m.ins.sel_rec {
1403                Some(s) if same(s, &self.sems[k]) => {}
1404                Some(s) => {
1405                    let m = format!("selected: {} {} {:?}", kind_name(s.kind), id_text(s.id), s.label.as_deref().unwrap_or(""));
1406                    self.fail(m.as_bytes());
1407                }
1408                None => self.fail(b"nothing selected"),
1409            }
1410        } else if let Some(r) = word_eq(a, "hover") {
1411            let r = skip_ws(r);
1412            let h = (0..WIN_MAX as u32).find_map(|i| {
1413                let (x, y) = self.m.ins.ptr[i as usize];
1414                self.ins_at(i, x, y)
1415            });
1416            if word_eq(r, "none").is_some() {
1417                if h.is_some() {
1418                    self.fail(b"a widget is under the pointer");
1419                }
1420                return;
1421            }
1422            let Some(k) = self.ins_resolve(r) else { return };
1423            if h != Some(k) {
1424                let m = match h {
1425                    Some(i) => format!("under the pointer: {} {}", kind_name(self.sems[i].kind), id_text(self.sems[i].id)),
1426                    None => String::from("nothing under the pointer"),
1427                };
1428                self.fail(m.as_bytes());
1429            }
1430        } else if let Some(r) = word_eq(a, "selector") {
1431            let Some((want, _)) = parse_qstring(skip_ws(r), 256) else {
1432                self.fail(b"expected a quoted string");
1433                return;
1434            };
1435            let have = self.m.ins.sel_rec.clone().map(|s| self.ins_selector(&s)).unwrap_or_default();
1436            if have.as_bytes() != want.as_slice() {
1437                self.fail(format!("selector is {}", have).as_bytes());
1438            }
1439        } else if let Some(r) = word_eq(a, "copied") {
1440            let Some((want, _)) = parse_qstring(skip_ws(r), 256) else {
1441                self.fail(b"expected a quoted string");
1442                return;
1443            };
1444            let have = self.clip_read(CLIP_TEXT).unwrap_or_default();
1445            if have != want {
1446                self.fail(format!("the clipboard holds {:?}", String::from_utf8_lossy(&have)).as_bytes());
1447            }
1448        } else if !self.isrc_expect(a) && !self.ied_expect(a) {
1449            // (source, opened, note: inspect_src.rs; changes, list, undo, redo, message:
1450            // inspect_edit.rs)
1451            self.fail(b"inspect expect open|closed|pick on|off|flash on|off|idle|same|excluded|selected S|hover S|selector \"s\"|copied \"s\"|source \"s\"|sources \"s\"|opened \"s\"|note \"s\"");
1452        }
1453    }
1454
1455    // ------------------------------------------------------------------ hot reload
1456
1457    /// (hotreload.rs) what a restart keeps: armed, open, tab, options (outlines, order,
1458    /// dirty), the selected widget's script selector
1459    pub(crate) fn ins_carry(&self) -> (bool, bool, i32, [i32; 3], Option<String>) {
1460        let s = &self.m.ins;
1461        let sel = s.sel_rec.as_ref().map(|r| self.ins_selector(r));
1462        (s.armed, s.on, s.tab, [s.outlines, s.order, s.flash], sel)
1463    }
1464
1465    /// (hotreload.rs) set it again after a restart; the selector is looked up in the tree of a
1466    /// pass with the inspector open (a widget that is gone stays unselected)
1467    pub(crate) fn ins_restore(&mut self, app: &mut dyn App, armed: bool, on: bool, tab: i32, opts: [i32; 3], sel: Option<&str>) {
1468        self.inspect_enable(armed || on);
1469        self.inspect_show(on);
1470        [self.m.ins.outlines, self.m.ins.order, self.m.ins.flash] = opts;
1471        self.run_passes(app);
1472        if let Some(Sel::Rec(k)) = sel.filter(|_| on).and_then(|p| self.resolve(p.as_bytes()).0) {
1473            let nodes = self.ins_tree();
1474            if let Some(i) = nodes.iter().position(|n| n.sem == k) {
1475                self.ins_select(&nodes, i);
1476            }
1477        }
1478        self.m.ins.tab = tab.clamp(0, 2);
1479        self.invalidate();
1480        self.run_passes(app);
1481    }
1482}