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