Skip to main content

kui_core/runtime/devtools/
mod.rs

1#![cfg_attr(not(feature = "devtools"), allow(dead_code))]
2//! The devtools panel, drawn by the core into any app's frame.
3//!
4//! Turned on by [`Core::set_devtools`] — or by `KUI_DEVTOOLS` in the
5//! environment, which the windowed runners read before the first frame
6//! ([`Core::devtools_from_env`]) — the panel sits beside the host's
7//! tree in the main window (`left`, `right`, `bottom`; a handle on the
8//! pane's inner edge resizes it), in a window of its own (`window`), or
9//! nowhere with its chords still live (`off`). It shows what
10//! the runtime is doing while the app runs: a header with the window's
11//! title, the frame counter and an icon strip (the theme base, the accent,
12//! native menus, the placement), and three tabs —
13//!
14//! - **facts**: the latency graph; the status block — what the runtime
15//!   believes right now, each row read from the door it comes from; and
16//!   the key legend a host declared ([`Core::set_devtools_legend`]);
17//! - **events**: every `UiEvent` handed to the host from any window, as a
18//!   virtual list whose rows open into the payload as data, with a
19//!   filter, a pause and a follow toggle — plus every warning the core
20//!   raised and the panel's own notes;
21//! - **tree**: the main window's last frame ([`Core::nodes`]), collapsible,
22//!   filterable, with a picker that finds the node under the pointer from
23//!   the app itself, and an inspector for the selected one.
24//!
25//! The chords are `Ctrl+Shift+<letter>`, a family no app keymap should
26//! use while the panel is on: `T` cycles the base (the app's own → light →
27//! dark), `A` the accent, `M` the menus (the platform's own → native →
28//! drawn), `D` moves the panel
29//! (left → right → bottom → window → off; the header has a button per
30//! placement too), `N` the tab, `C` clears the stream, `I`
31//! moves the keyboard into the panel and back out, `P` picks a node, and
32//! `Escape` leaves the picker. The `I` chord alone is the app's to
33//! respell ([`Core::set_devtools_key`]: `f12`, `mod+shift+d`, any
34//! [`Accel`] spelling), since it is the one an app names in its own
35//! help — the others are the panel's, reached once the keyboard is in.
36//!
37//! **How it is in the frame**. Docked, the main
38//! window's `begin_frame` opens an *app container* under the root, keyed
39//! so the host's children are named as if it were not there; the host's
40//! `configure_root` is split between the two — layout and paint to the
41//! container, everything addressed to the root; `Ui::finish` closes the
42//! container and opens the dock as the root's next child, under
43//! [`OriginId::DEVTOOLS`]. `handle_input` acts on the chords before
44//! routing and on every event of that origin before returning, and logs
45//! what it does return. Everything the panel declares in the main window
46//! is under `Key::ROOT.str("kui-devtools")`. State is the session's, so a
47//! second window can draw the same panel from the same stream.
48
49use std::collections::VecDeque;
50
51use rustc_hash::FxHashSet;
52
53use crate::color::Color;
54use crate::env::{Appearance, SystemEnv};
55use crate::geom::{Rect, Size, Vec2};
56use crate::key::Key;
57use crate::menu::Accel;
58use crate::runtime::inspect::NodeInfo;
59use crate::spec::{NodeSpec, Sizing};
60use crate::theme::{Theme, ThemeSource};
61use crate::tree::OriginId;
62use crate::value::Value;
63use crate::widgets::RowHeights;
64use crate::window::{WindowCommand, WindowConfig, WindowId};
65use crate::{Core, InputEvent, KeyCode, UiEvent};
66
67#[cfg(feature = "devtools")]
68mod icons;
69#[cfg(feature = "devtools")]
70mod panel;
71#[cfg(feature = "devtools")]
72mod stream;
73#[cfg(all(test, feature = "devtools"))]
74mod tests;
75#[cfg(feature = "devtools")]
76mod tree;
77
78/// Width of the side dock, logical px.
79pub const DOCK_SIDE_W: f32 = 340.0;
80/// Height of the bottom dock, logical px.
81pub const DOCK_BOTTOM_H: f32 = 280.0;
82/// The size the popped-out window opens at.
83pub const WINDOW_SIZE: Size = Size { w: 420.0, h: 720.0 };
84/// The name of the panel's own window, when it has one: what
85/// `Core::windows` lists and `Ui::window_name` answers there.
86pub const DEVTOOLS_WINDOW: &str = "kui-devtools";
87/// The label of the node everything the panel declares in the main
88/// window is under, so `key_of(DEVTOOLS_KEY)` finds it — and a tree reader
89/// that wants the app alone skips its subtree.
90pub const DEVTOOLS_KEY: &str = "kui-devtools";
91/// The app container's label. Never indexed for `key_of`.
92const APP_KEY: &str = "kui-devtools/app";
93/// How many stream entries are kept.
94pub const STREAM_CAP: usize = 512;
95/// A closed stream row's height, logical px.
96const STREAM_ROW_H: f32 = 18.0;
97/// How many are dropped at once when the cap is hit, so a busy stream is
98/// not rebuilding its row heights on every event.
99const STREAM_EVICT: usize = 64;
100
101/// The accents `Ctrl+Shift+A` walks: kui's own, then four the OS might
102/// report. `None` is the app's own.
103const ACCENTS: [(&str, u32); 5] = [
104    ("kui blue", 0x3b5bd4ff),
105    ("macOS blue", 0x007affff),
106    ("macOS yellow", 0xffc409ff),
107    ("macOS pink", 0xf74f9eff),
108    ("forest", 0x2f7d4fff),
109];
110
111/// Where the panel sits. The header has a button per placement, and
112/// `Ctrl+Shift+D` walks them in [`Dock::ALL`]'s order.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
114pub enum Dock {
115    /// A column on the left of the main window, [`DOCK_SIDE_W`] wide to
116    /// start with; the handle on its edge resizes it.
117    Left,
118    /// The same column on the right.
119    #[default]
120    Right,
121    /// A strip along the bottom, [`DOCK_BOTTOM_H`] tall to start with.
122    Bottom,
123    /// A window of its own, [`DEVTOOLS_WINDOW`] — "undock".
124    Window,
125    /// Hidden: nothing drawn, no window declared, the chords still live
126    /// (`Ctrl+Shift+D` brings it back) — "close".
127    Off,
128}
129
130impl Dock {
131    pub const ALL: [Dock; 5] = [
132        Dock::Left,
133        Dock::Right,
134        Dock::Bottom,
135        Dock::Window,
136        Dock::Off,
137    ];
138
139    pub fn name(self) -> &'static str {
140        match self {
141            Dock::Left => "left",
142            Dock::Right => "right",
143            Dock::Bottom => "bottom",
144            Dock::Window => "window",
145            Dock::Off => "off",
146        }
147    }
148
149    /// A placement by name; `"side"` is the right, the name it had before
150    /// there was a left.
151    pub fn parse(s: &str) -> Option<Dock> {
152        if s == "side" {
153            return Some(Dock::Right);
154        }
155        Self::ALL.into_iter().find(|d| d.name() == s)
156    }
157
158    fn next(self) -> Dock {
159        let i = Self::ALL.iter().position(|d| *d == self).unwrap_or(0);
160        Self::ALL[(i + 1) % Self::ALL.len()]
161    }
162
163    /// Whether the panel is drawn inside the main window.
164    pub fn docked(self) -> bool {
165        matches!(self, Dock::Left | Dock::Right | Dock::Bottom)
166    }
167
168    /// Whether the panel is a column beside the app.
169    pub fn is_side(self) -> bool {
170        matches!(self, Dock::Left | Dock::Right)
171    }
172}
173
174/// The smallest a docked pane can be — dragged, or squeezed by the
175/// window — and the least it leaves the app while the window allows.
176pub const SIDE_MIN_W: f32 = 280.0;
177pub const BOTTOM_MIN_H: f32 = 160.0;
178const APP_MIN: f32 = 160.0;
179
180/// The panel's tabs.
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
182pub enum Tab {
183    Facts,
184    #[default]
185    Events,
186    Tree,
187}
188
189/// One tab an app or an extension declared this frame: its name (the
190/// identity), the label the strip shows, and
191/// the slot an extension fills it through — `None` for the host form,
192/// whose content is the host's own subtree, anchored to the body.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub(crate) struct TabDecl {
195    pub(crate) name: String,
196    pub(crate) label: String,
197    pub(crate) slot: Option<String>,
198}
199
200/// Which tab the panel shows: one of its own three, or a declared one by
201/// index into this frame's list.
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub(crate) enum Shown {
204    Builtin(Tab),
205    Custom(usize),
206}
207
208impl Tab {
209    const ALL: [Tab; 3] = [Tab::Facts, Tab::Events, Tab::Tree];
210
211    fn name(self) -> &'static str {
212        match self {
213            Tab::Facts => "facts",
214            Tab::Events => "events",
215            Tab::Tree => "tree",
216        }
217    }
218
219    /// What the strip shows and the access tree names: the name with
220    /// its first letter up, as a declared tab's label is written.
221    fn label(self) -> &'static str {
222        match self {
223            Tab::Facts => "Facts",
224            Tab::Events => "Events",
225            Tab::Tree => "Tree",
226        }
227    }
228
229    /// One of the three by its name in any case — `tree`, `Tree`, `TREE`
230    /// — since the strip labels them with a capital and a caller writes
231    /// what it reads there. A declared tab's name is the
232    /// app's own spelling and is matched exactly.
233    fn parse(s: &str) -> Option<Tab> {
234        Self::ALL
235            .into_iter()
236            .find(|t| t.name().eq_ignore_ascii_case(s))
237    }
238}
239
240/// One entry of the stream.
241pub(crate) struct Entry {
242    /// Its number, ever increasing: what an expanded row is remembered by
243    /// across evictions.
244    seq: u64,
245    /// The main window's frame count when it was logged.
246    frame: u64,
247    kind: EntryKind,
248    window: WindowId,
249    key: Key,
250    /// The node's label when it was logged — the tree that named it is
251    /// gone by the time the row is read.
252    label: Option<String>,
253    origin: OriginId,
254    payload: Value,
255    /// How many lines the payload takes as a tree (`stream::lines`), so a
256    /// row's height is arithmetic and never a measurement.
257    lines: u16,
258}
259
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261enum EntryKind {
262    Event,
263    Warning,
264    Note,
265}
266
267/// What the main window's core believed at the end of its last frame,
268/// read by whichever window draws the facts tab. Strings rather than the
269/// facts themselves: the panel prints them, and a second window's core
270/// cannot ask the first's doors.
271/// One declared token as the panel shows it:
272/// which origin declared it, its halves, and what it resolved to this
273/// frame — the value the inspector matches a node's paint against.
274#[derive(Clone, Debug, PartialEq)]
275pub(crate) struct TokenFact {
276    pub(crate) origin: OriginId,
277    pub(crate) name: String,
278    pub(crate) kind: crate::tokens::TokenKind,
279    pub(crate) light: Color,
280    pub(crate) dark: Color,
281    /// This frame's colour, for a colour token.
282    pub(crate) resolved: Color,
283    /// The px, for a length token.
284    pub(crate) length: f32,
285    /// A derived colour token's recipe, as the panel prints it beside the
286    /// hex: `peach → lift 0.3`.
287    pub(crate) recipe: Option<String>,
288}
289
290#[derive(Clone, Default)]
291pub(crate) struct Facts {
292    title: String,
293    /// `(name, value)` rows of the status block, in order.
294    rows: Vec<(&'static str, String)>,
295    /// Every declared token, the host's first then each extension's,
296    /// each origin in its declaration order.
297    pub(crate) tokens: Vec<TokenFact>,
298    /// The main core's theme source, for the panel's own window to
299    /// mirror when no override is in force — and the base it resolves to,
300    /// so the base toggle's "the app's own" can say which that is.
301    theme_source: ThemeSource,
302    app_appearance: Appearance,
303    /// The viewport the main window's nodes were laid out in — what the
304    /// picker's overlay covers.
305    viewport: Size,
306    /// The frame's focus, hover and region, for the inspector's state rows.
307    focus: Option<Key>,
308    focus_visible: bool,
309    region: Option<Key>,
310    hovered: Option<Key>,
311    pressed: Option<Key>,
312    /// The main window's pointer, for the picker.
313    cursor: Option<Vec2>,
314}
315
316/// The session's devtools state.
317pub(crate) struct State {
318    pub(crate) on: bool,
319    pub(crate) dock: Dock,
320    /// The docked pane's extent — a side column's width, the bottom
321    /// strip's height — as the handle last left it.
322    side_w: f32,
323    bottom_h: f32,
324    tab: Tab,
325    // The overrides. `None` leaves the app's own.
326    base: Option<Appearance>,
327    /// `Some(i)` walks [`ACCENTS`]; with `custom_accent` it is that colour.
328    accent: Option<usize>,
329    custom_accent: Option<Color>,
330    native_menus: Option<bool>,
331    // The stream.
332    stream: VecDeque<Entry>,
333    seq: u64,
334    /// The rows the events tab shows (indices into `stream`, after the
335    /// filter) and their heights — rebuilt when `stream_dirty`.
336    rows: Vec<usize>,
337    heights: RowHeights,
338    expanded: FxHashSet<u64>,
339    stream_dirty: bool,
340    /// The stream grew since the list last followed it.
341    stream_grew: bool,
342    /// The list's travel (`max_offset.y`) the follow last pinned against:
343    /// a travel that differs is content that changed under the pin, an
344    /// offset short of an unchanged travel is the user's wheel.
345    followed_max: f32,
346    paused: bool,
347    follow: bool,
348    stream_filter: String,
349    // The tree.
350    selected: Option<Key>,
351    /// The row under the pointer in whichever window draws the tree, for
352    /// the outline the main window paints.
353    hovered_row: Option<Key>,
354    collapsed: FxHashSet<Key>,
355    tree_filter: String,
356    /// The picker is up.
357    pick: bool,
358    /// The picker was raised from a declared tab (`Core::set_devtools_pick`
359    /// while one was on show): the pick lands in `selected` for
360    /// that tab to read, and the tab stays up rather than the tree tab
361    /// taking over.
362    pick_keep_tab: bool,
363    /// The node the picker last saw under the pointer.
364    pick_hover: Option<Key>,
365    /// Scroll the tree to this node's row on the next build, with the
366    /// rows above it expanded.
367    reveal: Option<Key>,
368    /// Collapse every row with children on the next build.
369    fold_all: bool,
370    /// The row the keyboard is on: the tree's own cursor,
371    /// moved by the arrows on the list's sink and painted as a ring while
372    /// the list holds focus. None until a key lands on the list.
373    tree_cursor: Option<Key>,
374    /// A key the list's sink heard, for the next build to apply with the
375    /// rows in hand: `up`, `down`, `left`, `right`, `home`, `end`,
376    /// `pageup`, `pagedown`, `enter`, `space`.
377    tree_key: Option<String>,
378    // What the main window wrote for the others.
379    facts: Facts,
380    nodes: Vec<NodeInfo>,
381    /// Frames of the main window so far, and `KUI_SMOKE_FRAMES` when set.
382    frames: u64,
383    smoke_frames: Option<u64>,
384    warnings_seen: usize,
385    legend: Vec<(String, String)>,
386    /// The chord that moves the keyboard into the panel and back out —
387    /// `Ctrl+Shift+I` unless the app respelled it.
388    inspect_key: Accel,
389    /// The tabs declared in the main window's last finished frame: what the
390    /// strip lists after its own three. Set by the main
391    /// window at the end of its frame; a left dock and the panel's own
392    /// window read it a frame late, as they read the facts.
393    tabs: Vec<TabDecl>,
394    /// The declared tab the panel shows, by name — over `tab` while the
395    /// name is in `tabs`; a name that is gone falls back to `tab`.
396    custom: Option<String>,
397    /// The inspect chord waiting for the next main-window build.
398    toggle_region: bool,
399    /// The picker was left by a raw `Escape` press: the editor channel's
400    /// half of the same press, still to come, is the panel's too.
401    escape_owed: bool,
402    /// The dock wants the keyboard in its own window: `Focus` it once.
403    focus_window: bool,
404    /// The picker was raised from the panel's own window: `Focus` the
405    /// main window once, since that is where the picking happens and
406    /// the panel's window is what the pointer and the keyboard are in.
407    focus_main: bool,
408    /// A build in the panel's own window changed what the main window
409    /// paints (the hovered row's outline): ask it to draw once.
410    redraw_main: bool,
411}
412
413impl Default for State {
414    fn default() -> Self {
415        State {
416            on: false,
417            dock: Dock::default(),
418            side_w: DOCK_SIDE_W,
419            bottom_h: DOCK_BOTTOM_H,
420            tab: Tab::default(),
421            base: None,
422            accent: None,
423            custom_accent: None,
424            native_menus: None,
425            stream: VecDeque::new(),
426            seq: 0,
427            rows: Vec::new(),
428            heights: RowHeights::new(0, STREAM_ROW_H),
429            expanded: FxHashSet::default(),
430            stream_dirty: false,
431            stream_grew: false,
432            followed_max: 0.0,
433            paused: false,
434            follow: true,
435            stream_filter: String::new(),
436            selected: None,
437            hovered_row: None,
438            collapsed: FxHashSet::default(),
439            tree_filter: String::new(),
440            pick: false,
441            pick_keep_tab: false,
442            pick_hover: None,
443            reveal: None,
444            fold_all: false,
445            tree_cursor: None,
446            tree_key: None,
447            facts: Facts::default(),
448            nodes: Vec::new(),
449            frames: 0,
450            smoke_frames: std::env::var("KUI_SMOKE_FRAMES")
451                .ok()
452                .and_then(|s| s.parse().ok()),
453            warnings_seen: 0,
454            legend: Vec::new(),
455            inspect_key: DEFAULT_INSPECT_KEY,
456            tabs: Vec::new(),
457            custom: None,
458            toggle_region: false,
459            escape_owed: false,
460            focus_window: false,
461            focus_main: false,
462            redraw_main: false,
463        }
464    }
465}
466
467impl State {
468    /// What `KUI_DEVTOOLS` asks for: `1`/`true` the panel where it goes by
469    /// default (the right), a placement name the panel there, anything
470    /// else nothing.
471    fn from_var(var: Option<&str>) -> Self {
472        let mut s = State::default();
473        if !cfg!(feature = "devtools") {
474            return s;
475        }
476        if let Some(v) = var {
477            let v = v.trim().to_ascii_lowercase();
478            if v == "1" || v == "true" || v == "on" {
479                s.on = true;
480            } else if let Some(d) = Dock::parse(&v) {
481                s.on = true;
482                s.dock = d;
483            }
484        }
485        s
486    }
487
488    fn base_name(&self) -> &'static str {
489        match self.base {
490            None => "app",
491            Some(Appearance::Light) => "light",
492            Some(_) => "dark",
493        }
494    }
495
496    /// The menu override's name, the select's spelling: `platform` is
497    /// the host's own mode, whatever that is.
498    fn menus_name(&self) -> &'static str {
499        match self.native_menus {
500            None => "platform",
501            Some(true) => "native",
502            Some(false) => "drawn",
503        }
504    }
505
506    fn accent_name(&self) -> String {
507        match (self.accent, self.custom_accent) {
508            (Some(i), _) => ACCENTS[i].0.to_string(),
509            (None, Some(c)) => format!("#{:06x}", c.to_hex() >> 8),
510            (None, None) => "app".to_string(),
511        }
512    }
513
514    /// The theme the overrides add up to, or `None` for the app's own.
515    /// `app` is the app's own source, which the half an override leaves
516    /// alone is read from: "the app's base" is the base *the app* chose,
517    /// not the OS's, so an app that pinned dark on a light desktop stays
518    /// dark under an accent override, and an app with a brand accent
519    /// keeps it under a base override. (The accent half used to go out
520    /// as `DerivedWithAccent`, which follows `env.system`, so a
521    /// pinned-dark app flipped light the moment `Ctrl+Shift+A` was
522    /// pressed — the pomodoro's report of 2026-09-12.)
523    fn theme_override(&self, app: ThemeSource, sys: &SystemEnv) -> Option<ThemeSource> {
524        let accent = self
525            .accent
526            .map(|i| Color::hex(ACCENTS[i].1))
527            .or(self.custom_accent);
528        match (self.base, accent) {
529            (None, None) => None,
530            (None, Some(c)) => Some(match app {
531                // The app follows the OS's base: keep following it.
532                ThemeSource::Derived | ThemeSource::DerivedWithAccent(_) => {
533                    ThemeSource::DerivedWithAccent(c)
534                }
535                // The app pinned a palette: the same palette, recoloured.
536                ThemeSource::Pinned(t) => ThemeSource::Pinned(t.with_accent(c)),
537            }),
538            (Some(base), c) => {
539                // The app's own accent under the chosen base — the OS's
540                // where the app follows the OS, so a host that reports
541                // none still paints kui's blue byte for byte.
542                let own = match app {
543                    ThemeSource::Derived => sys.accent,
544                    ThemeSource::DerivedWithAccent(a) => Some(a),
545                    ThemeSource::Pinned(t) => Some(t.accent),
546                };
547                Some(ThemeSource::Pinned(Theme::derive(base, c.or(own))))
548            }
549        }
550    }
551
552    fn push(&mut self, mut e: Entry) {
553        if self.stream.len() >= STREAM_CAP {
554            self.stream.drain(..STREAM_EVICT);
555            // The rows that went take their open state with them.
556            if let Some(first) = self.stream.front().map(|e| e.seq) {
557                self.expanded.retain(|seq| *seq >= first);
558            }
559        }
560        e.seq = self.seq;
561        self.seq += 1;
562        e.lines = lines(&e.payload);
563        self.stream.push_back(e);
564        self.stream_dirty = true;
565        self.stream_grew = true;
566    }
567
568    fn note(&mut self, text: impl Into<String>) {
569        let frame = self.frames;
570        self.push(Entry {
571            seq: 0,
572            frame,
573            kind: EntryKind::Note,
574            window: WindowId::MAIN,
575            key: Key::ROOT,
576            label: None,
577            origin: OriginId::DEVTOOLS,
578            payload: Value::str(text.into()),
579            lines: 0,
580        });
581    }
582
583    /// One of the panel's actions, by the name its button and its chord
584    /// share. Returns whether `what` was one.
585    fn act(&mut self, what: &str) -> bool {
586        match what {
587            "base" => {
588                self.base = match self.base {
589                    None => Some(Appearance::Light),
590                    Some(Appearance::Light) => Some(Appearance::Dark),
591                    Some(_) => None,
592                };
593                self.note(format!("theme base: {}", self.base_name()));
594            }
595            "accent" => {
596                self.custom_accent = None;
597                self.accent = match self.accent {
598                    None => Some(0),
599                    Some(i) if i + 1 < ACCENTS.len() => Some(i + 1),
600                    Some(_) => None,
601                };
602                self.note(format!("accent: {}", self.accent_name()));
603            }
604            "menus" => {
605                // The select's three, round: the host's own mode (which
606                // `begin_frame` puts back from `dt_menus`), native, drawn.
607                // Not a toggle from a compile-time guess at the host's
608                // mode, which could never return to it (backlog RG12).
609                self.native_menus = match self.native_menus {
610                    None => Some(true),
611                    Some(true) => Some(false),
612                    Some(false) => None,
613                };
614                self.note(format!("menus: {}", self.menus_name()));
615            }
616            "dock" => {
617                self.dock = self.dock.next();
618                self.pick = false;
619                self.note(format!("dock: {}", self.dock.name()));
620            }
621            "inspect" => match self.dock {
622                // Nothing to enter with the panel hidden: it comes back
623                // first, docked.
624                Dock::Off => {
625                    self.dock = Dock::Right;
626                    self.toggle_region = true;
627                }
628                Dock::Window => self.focus_window = true,
629                _ => self.toggle_region = true,
630            },
631            "clear" => {
632                self.stream.clear();
633                self.expanded.clear();
634                self.stream_dirty = true;
635            }
636            "pause" => self.paused = !self.paused,
637            "follow" => {
638                self.follow = !self.follow;
639                self.stream_grew |= self.follow;
640            }
641            "tab" => {
642                // The panel's three, then the declared ones, then round.
643                let n = Tab::ALL.len() + self.tabs.len();
644                let i = match self.shown() {
645                    Shown::Builtin(t) => Tab::ALL.iter().position(|x| *x == t).unwrap_or(0),
646                    Shown::Custom(i) => Tab::ALL.len() + i,
647                };
648                self.select_tab((i + 1) % n);
649            }
650            "pick" => {
651                self.pick = !self.pick;
652                // The chord's pick shows the tree tab and lands there;
653                // a tab's earlier, cancelled pick must not decide otherwise.
654                self.pick_keep_tab = false;
655                if self.pick {
656                    self.show(Tab::Tree);
657                    if self.dock == Dock::Off {
658                        self.dock = Dock::Right;
659                    }
660                    // Picking happens in the main window; raised from
661                    // the panel's own, the keyboard (for Escape) and the
662                    // pointer are both in the wrong one.
663                    self.focus_main = self.dock == Dock::Window;
664                }
665                self.pick_hover = None;
666            }
667            "fold-all" => self.fold_all = true,
668            "unfold-all" => {
669                self.collapsed.clear();
670                self.fold_all = false;
671            }
672            other => {
673                if let Some(name) = other.strip_prefix("tab:custom:") {
674                    if let Some(i) = self.tabs.iter().position(|t| t.name == name) {
675                        self.select_tab(Tab::ALL.len() + i);
676                    }
677                } else if let Some(tab) = other.strip_prefix("tab:").and_then(Tab::parse) {
678                    self.tab = tab;
679                    self.custom = None;
680                } else if let Some(base) = other.strip_prefix("base:") {
681                    // The Facts tab's select: one choice, not the next.
682                    self.base = match base {
683                        "light" => Some(Appearance::Light),
684                        "dark" => Some(Appearance::Dark),
685                        _ => None,
686                    };
687                    self.note(format!("theme base: {}", self.base_name()));
688                } else if let Some(accent) = other.strip_prefix("accent:") {
689                    self.custom_accent = None;
690                    self.accent = accent.parse().ok().filter(|i| *i < ACCENTS.len());
691                    self.note(format!("accent: {}", self.accent_name()));
692                } else if let Some(menus) = other.strip_prefix("menus:") {
693                    self.native_menus = match menus {
694                        "native" => Some(true),
695                        "drawn" => Some(false),
696                        _ => None,
697                    };
698                    self.note(format!("menus: {}", self.menus_name()));
699                } else if let Some(dock) = other.strip_prefix("dock:").and_then(Dock::parse) {
700                    // One of the header's placement buttons.
701                    if dock != self.dock {
702                        self.dock = dock;
703                        self.pick = false;
704                        self.note(format!("dock: {}", dock.name()));
705                    }
706                } else if let Some(key) = other.strip_prefix("node:").and_then(parse_key) {
707                    // A tree row: select it, or unselect the selected one.
708                    self.selected = if self.selected == Some(key) {
709                        None
710                    } else {
711                        Some(key)
712                    };
713                } else if let Some(key) = other.strip_prefix("goto:").and_then(parse_key) {
714                    // The inspector's parent row or a breadcrumb: select
715                    // and bring its row into view.
716                    self.select(key);
717                } else if let Some(key) = other.strip_prefix("fold:").and_then(parse_key) {
718                    if !self.collapsed.remove(&key) {
719                        self.collapsed.insert(key);
720                    }
721                } else if let Some(seq) = other.strip_prefix("row:").and_then(|s| s.parse().ok()) {
722                    if !self.expanded.remove(&seq) {
723                        self.expanded.insert(seq);
724                    }
725                    self.stream_dirty = true;
726                } else if let Some(code) = other.strip_prefix("tree-key:") {
727                    // A key on the tree's list: the build applies it, since
728                    // moving needs the rows and folding needs the children.
729                    self.tree_key = Some(code.to_string());
730                } else {
731                    return false;
732                }
733            }
734        }
735        true
736    }
737
738    /// Selects `key` and asks the next tree build to expand the rows
739    /// above it and scroll to it — what the picker and the inspector's
740    /// links do. The build has the nodes; this has only the key.
741    fn select(&mut self, key: Key) {
742        self.selected = Some(key);
743        self.show(Tab::Tree);
744        self.reveal = Some(key);
745    }
746
747    /// Shows one of the panel's own tabs.
748    fn show(&mut self, tab: Tab) {
749        self.tab = tab;
750        self.custom = None;
751    }
752
753    /// The tab on show: `custom` while it names a declared tab, else
754    /// `tab`.
755    pub(crate) fn shown(&self) -> Shown {
756        match self.custom.as_deref() {
757            Some(name) => match self.tabs.iter().position(|t| t.name == name) {
758                Some(i) => Shown::Custom(i),
759                None => Shown::Builtin(self.tab),
760            },
761            None => Shown::Builtin(self.tab),
762        }
763    }
764
765    /// Whether `name` is a declared tab the panel lists — what `shown`
766    /// falls back on when it is not, and what the laziness rule asks so
767    /// a stale `custom` (a tab the app stopped declaring) shows nothing
768    /// and builds nothing.
769    fn lists(&self, name: &str) -> bool {
770        self.tabs.iter().any(|t| t.name == name)
771    }
772
773    /// Selects the `i`th tab of the strip: the three, then the declared.
774    fn select_tab(&mut self, i: usize) {
775        if i < Tab::ALL.len() {
776            self.show(Tab::ALL[i]);
777        } else if let Some(t) = self.tabs.get(i - Tab::ALL.len()) {
778            self.custom = Some(t.name.clone());
779        }
780    }
781}
782
783fn parse_key(hex: &str) -> Option<Key> {
784    u64::from_str_radix(hex, 16).ok().map(Key)
785}
786
787/// `{ dt: what }`, the payload every control of the panel posts.
788fn action(what: impl Into<String>) -> Value {
789    Value::map([("dt", Value::str(what.into()))])
790}
791
792/// What `Ctrl+Shift+I` parses to: the inspect chord until an app
793/// respells it.
794const DEFAULT_INSPECT_KEY: Accel = Accel {
795    code: KeyCode::Char('i'),
796    mods: crate::input::KeyMods {
797        shift: true,
798        ctrl: true,
799        alt: false,
800        super_key: false,
801    },
802};
803
804/// Whether a press is this chord: the modifiers exactly, and the key by
805/// the layout's character first and the physical position second — case-blind
806/// for a character, since Shift is part
807/// of the chord and the layout has already applied it.
808fn hits(accel: Accel, press: &crate::input::KeyPress) -> bool {
809    if press.mods != accel.mods {
810        return false;
811    }
812    let same = |code: KeyCode| match (accel.code, code) {
813        (KeyCode::Char(a), KeyCode::Char(b)) => a.eq_ignore_ascii_case(&b),
814        (a, b) => a == b,
815    };
816    same(press.code) || same(press.physical)
817}
818
819/// The panel's chord for a key press, if it is one: the inspect chord
820/// (`Ctrl+Shift+I`, or what the app respelled it to), else `Ctrl+Shift`
821/// and a letter, by the layout's character first and the physical
822/// position second.
823fn chord(press: &crate::input::KeyPress, inspect: Accel) -> Option<&'static str> {
824    if hits(inspect, press) {
825        return Some("inspect");
826    }
827    let m = press.mods;
828    if !m.ctrl || !m.shift || m.alt {
829        return None;
830    }
831    let letter = |c: KeyCode| match c {
832        KeyCode::Char(ch) => Some(ch.to_ascii_lowercase()),
833        _ => None,
834    };
835    match letter(press.code).or_else(|| letter(press.physical))? {
836        't' => Some("base"),
837        'a' => Some("accent"),
838        'm' => Some("menus"),
839        'd' => Some("dock"),
840        'c' => Some("clear"),
841        'n' => Some("tab"),
842        // `I` is the inspect chord's letter only while that is what the
843        // chord is: respelled to `F12`, `Ctrl+Shift+I` is the app's again.
844        'p' => Some("pick"),
845        _ => None,
846    }
847}
848
849/// The key of a declared tab's body: fixed by the
850/// tab's name alone, so the content can anchor to it before it exists —
851/// the host form is built before a right dock's body, after a left one's.
852pub(crate) fn tab_body_key(name: &str) -> Key {
853    Key::ROOT.str(DEVTOOLS_KEY).str("tab").str(name)
854}
855
856impl Core {
857    /// Declares a devtools tab this frame. A name
858    /// declared already this frame warns `duplicate-tab` and keeps the
859    /// first; returns whether this one stood. The bare door under
860    /// `Ui::devtools_tab` / `devtools_tab_with`, for a binding that
861    /// opens the content itself.
862    pub fn devtools_tab_declare(&mut self, name: &str, label: &str, slot: Option<&str>) -> bool {
863        if !cfg!(feature = "devtools") || self.tree.is_empty() {
864            return false;
865        }
866        if self.dt_tabs.iter().any(|t| t.name == name) {
867            self.diag.raise(crate::diag::duplicate_tab(name));
868            return false;
869        }
870        self.dt_tabs.push(TabDecl {
871            name: name.to_string(),
872            label: label.to_string(),
873            slot: slot.map(str::to_string),
874        });
875        true
876    }
877
878    /// Whether the host form of tab `name` is shown this frame — the
879    /// panel is on, docked in this (the main) window, and `name` is the
880    /// tab on show. Read before the content is
881    /// built, from the session's state, which is in place while the
882    /// host's view runs.
883    pub fn devtools_tab_shown(&self, name: &str) -> bool {
884        if !cfg!(feature = "devtools") || self.dt_window || self.env.window.id != WindowId::MAIN {
885            return false;
886        }
887        let s = self.session.state();
888        let d = &s.devtools;
889        d.on && d.dock.docked() && d.custom.as_deref() == Some(name) && d.lists(name)
890    }
891
892    /// The tab on show, by name, when it is a declared one — what the
893    /// Node and Lua drivers read once a frame to call a tab's function
894    /// child. `None` for one of the panel's own,
895    /// for the panel off, popped out, or another window's frame.
896    pub fn devtools_shown_tab(&self) -> Option<String> {
897        if !cfg!(feature = "devtools") || self.dt_window || self.env.window.id != WindowId::MAIN {
898            return None;
899        }
900        let s = self.session.state();
901        let d = &s.devtools;
902        if !(d.on && d.dock.docked()) {
903            return None;
904        }
905        d.custom.clone().filter(|name| d.lists(name))
906    }
907
908    /// Opens the host form's content node: a float anchored to the tab's
909    /// body by key, the body's size, clipped, keyed as the host's own
910    /// child. The caller builds inside and
911    /// closes. The bare door under `Ui::devtools_tab_with`; it does not
912    /// declare, and it does not ask whether the tab is on show.
913    pub fn devtools_tab_open(&mut self, name: &str) {
914        let spec = NodeSpec::column()
915            .float(crate::spec::FloatConfig {
916                anchor: crate::spec::FloatAnchor::Node(tab_body_key(name)),
917                ..Default::default()
918            })
919            .fill()
920            .clip();
921        self.open_keyed(&format!("devtools-tab:{name}"), spec);
922    }
923
924    /// Whether `name` is a slot a declared tab names this frame — what
925    /// counts as *declared* for the `unknown-slot` check whether or not
926    /// the panel mounted it.
927    pub(crate) fn devtools_tab_slot(&self, name: &str) -> bool {
928        self.dt_tabs.iter().any(|t| t.slot.as_deref() == Some(name))
929    }
930
931    /// Mounts the extension form of the tab on show, if it has one: the
932    /// slot is declared at the cursor under the host's origin — so the
933    /// fill's replies reach the host — and filled inside a float anchored to the
934    /// tab's body, the panel's facts as params. Called with the filler in
935    /// hand: from `Ui::finish` in the
936    /// main window before the filler's own finish, and in the panel's
937    /// window after its build. Nothing to do when the tab on show is the
938    /// panel's own or a host form.
939    pub(crate) fn devtools_fill_mount(&mut self, filler: &mut dyn crate::slot::Fill) {
940        if !cfg!(feature = "devtools") || self.tree.is_empty() {
941            return;
942        }
943        let (name, slot, params) = {
944            let s = self.session.state();
945            let d = &s.devtools;
946            if !d.on || !(d.dock.docked() || self.dt_window) {
947                return;
948            }
949            let tabs = if self.dt_window {
950                &d.tabs
951            } else {
952                &self.dt_tabs
953            };
954            let Some(name) = d.custom.as_deref() else {
955                return;
956            };
957            let Some(decl) = tabs.iter().find(|t| t.name == name) else {
958                return;
959            };
960            let Some(slot) = decl.slot.clone() else {
961                return;
962            };
963            (decl.name.clone(), slot, d.facts_params())
964        };
965        let saved = self.origin;
966        self.origin = OriginId::HOST;
967        if let Some(key) = self.begin_slot(&slot) {
968            self.devtools_tab_open(&name);
969            filler.fill(&slot, key, &params, &mut crate::ui::Ui::new(self));
970            self.close();
971        }
972        self.origin = saved;
973    }
974}
975
976impl State {
977    /// The panel's facts as a slot's params: the
978    /// selected, hovered and picked nodes as hex keys (`null` for none),
979    /// the region and the focus by label from the facts rows.
980    fn facts_params(&self) -> Value {
981        let key = |k: Option<Key>| match k {
982            Some(k) => Value::str(format!("{:016x}", k.0)),
983            None => Value::Null,
984        };
985        let row = |name: &str| {
986            self.facts
987                .rows
988                .iter()
989                .find(|(k, _)| *k == name)
990                .map(|(_, v)| Value::str(v.clone()))
991                .unwrap_or(Value::Null)
992        };
993        Value::map([
994            ("selected", key(self.selected)),
995            ("hovered", key(self.hovered_row)),
996            ("picked", key(self.pick_hover)),
997            ("region", row("region")),
998            ("focus", row("focus")),
999        ])
1000    }
1001}
1002
1003// ---------------------------------------------------------------------------
1004// The doors.
1005
1006impl Core {
1007    /// Turns the devtools panel on or off for this session. On,
1008    /// it is drawn where [`Self::set_devtools_dock`] says — beside the
1009    /// host's tree in the main window by default — and its chords are
1010    /// live in every window. `KUI_DEVTOOLS=1` in the environment is the
1011    /// same call made by nobody; `KUI_DEVTOOLS=bottom` (or `left`,
1012    /// `right`, `window`, `off`) also says where.
1013    pub fn set_devtools(&mut self, on: bool) {
1014        // Built without the panel: the door stays and does nothing.
1015        if !cfg!(feature = "devtools") {
1016            return;
1017        }
1018        let mut s = self.session.state();
1019        if s.devtools.on == on {
1020            return;
1021        }
1022        s.devtools.on = on;
1023        if !on {
1024            s.devtools.pick = false;
1025            s.devtools.pick_keep_tab = false;
1026        }
1027    }
1028
1029    /// Opens the panel if `KUI_DEVTOOLS` in the environment asks for it
1030    /// (`1`, `true`, or a placement name), and says whether it did. What
1031    /// the windowed runners call once, before the first frame — the door
1032    /// for a program that was never told about the panel — and what a
1033    /// headless core never reads, so a variable left exported cannot put
1034    /// a dock into a test's tree.
1035    pub fn devtools_from_env(&mut self) -> bool {
1036        let asked = State::from_var(std::env::var("KUI_DEVTOOLS").ok().as_deref());
1037        if asked.on {
1038            self.set_devtools(true);
1039            self.set_devtools_dock(asked.dock);
1040        }
1041        asked.on
1042    }
1043
1044    /// Whether the panel is on.
1045    pub fn devtools(&self) -> bool {
1046        self.session.state().devtools.on
1047    }
1048
1049    /// Where the panel sits; `Ctrl+Shift+D` moves it from there.
1050    pub fn set_devtools_dock(&mut self, dock: Dock) {
1051        self.session.state().devtools.dock = dock;
1052    }
1053
1054    pub fn devtools_dock(&self) -> Dock {
1055        self.session.state().devtools.dock
1056    }
1057
1058    /// Seeds the panel's theme override — what its `T` and `A` chords
1059    /// cycle from. `None` for either leaves the app's own.
1060    pub fn set_devtools_theme(&mut self, base: Option<Appearance>, accent: Option<Color>) {
1061        let mut s = self.session.state();
1062        s.devtools.base = base;
1063        s.devtools.accent = None;
1064        s.devtools.custom_accent = accent;
1065    }
1066
1067    /// Respells the chord that moves the keyboard into the panel and
1068    /// back out — and brings the panel back when it is `off` — from its
1069    /// default `Ctrl+Shift+I`: any [`Accel`] spelling (`"f12"`,
1070    /// `"mod+shift+d"`, `"⌥⌘I"`). The other chords stay `Ctrl+Shift+
1071    /// <letter>`; this is the one an app puts in its own help, and the
1072    /// one whose default an app's keymap may want for itself. A chord
1073    /// the app takes is the app's for good: with `F12` set,
1074    /// `Ctrl+Shift+I` reaches the app's sinks like any other press.
1075    pub fn set_devtools_key(&mut self, key: Accel) {
1076        self.session.state().devtools.inspect_key = key;
1077    }
1078
1079    /// The chord that moves the keyboard into the panel, as set or as
1080    /// it defaults.
1081    pub fn devtools_key(&self) -> Accel {
1082        self.session.state().devtools.inspect_key
1083    }
1084
1085    /// The node the panel's tree tab has selected: what an inspector in a
1086    /// declared tab reads to say which node
1087    /// it is about. Answered from the session, so it is right inside the
1088    /// host's view and inside an extension's fill alike.
1089    pub fn devtools_selected(&self) -> Option<Key> {
1090        self.session.state().devtools.selected
1091    }
1092
1093    /// The tree row under the pointer in whichever window draws the
1094    /// tree — the node the main window outlines.
1095    pub fn devtools_hovered(&self) -> Option<Key> {
1096        self.session.state().devtools.hovered_row
1097    }
1098
1099    /// The node the picker last saw under the pointer, while picking.
1100    pub fn devtools_picked(&self) -> Option<Key> {
1101        self.session.state().devtools.pick_hover
1102    }
1103
1104    /// Selects a node in the panel's tree tab from outside it — an
1105    /// inspector driving the highlight from its side — and reveals it
1106    /// there, as the picker does; `None` clears. The tab does not move:
1107    /// the caller is drawing in one.
1108    pub fn set_devtools_selected(&mut self, key: Option<Key>) {
1109        let mut s = self.session.state();
1110        let d = &mut s.devtools;
1111        d.selected = key;
1112        d.reveal = key;
1113        drop(s);
1114        self.devtools_redraw_others();
1115    }
1116
1117    /// Raises the panel's picker from outside it — an inspector in a
1118    /// declared tab asking "which node?" — or puts it away. Picking happens in
1119    /// the main window, over the app: the
1120    /// node under the pointer is `devtools_picked` while it is up, and
1121    /// the press lands it in `devtools_selected`. Raised while a declared
1122    /// tab is on show, the pick leaves that tab up; raised otherwise — a
1123    /// tab named through [`Self::set_devtools_tab`] but not declared yet
1124    /// included — it is the `Ctrl+Shift+P` pick, which shows the tree
1125    /// tab. A hidden panel comes back docked, as the chord's does.
1126    pub fn set_devtools_pick(&mut self, on: bool) {
1127        if !cfg!(feature = "devtools") {
1128            return;
1129        }
1130        let focus_main = {
1131            let mut s = self.session.state();
1132            let d = &mut s.devtools;
1133            if d.pick == on {
1134                return;
1135            }
1136            d.pick = on;
1137            d.pick_hover = None;
1138            // A declared tab is up when `custom` names one the panel
1139            // lists — not merely when it is set: since F67 a name no frame
1140            // has declared yet is kept there, and the strip falls back to
1141            // the panel's own tab, so a pick raised then is the chord's
1142            // and shows the tree (backlog RG14).
1143            let on_tab = d.custom.as_deref().is_some_and(|n| d.lists(n));
1144            d.pick_keep_tab = on && on_tab;
1145            if on {
1146                if !on_tab {
1147                    d.show(Tab::Tree);
1148                }
1149                if d.dock == Dock::Off {
1150                    d.dock = Dock::Right;
1151                }
1152            }
1153            on && d.dock == Dock::Window
1154        };
1155        if focus_main {
1156            self.interaction
1157                .window_commands
1158                .push(WindowCommand::Focus(WindowId::MAIN));
1159        }
1160        self.devtools_redraw_others();
1161    }
1162
1163    /// Whether the panel's picker is up.
1164    pub fn devtools_picking(&self) -> bool {
1165        self.session.state().devtools.pick
1166    }
1167
1168    /// Shows the panel's tab named `name` from outside the panel — what
1169    /// the strip's click and `Ctrl+Shift+N` do, for an app with a command
1170    /// that jumps to its own tab. `name` is one of the panel's
1171    /// own (`facts`, `events`, `tree`, in any case — the strip labels
1172    /// them `Facts`, `Events`, `Tree`) or a declared tab's, exactly as
1173    /// the app declared it. A declared
1174    /// name the panel does not list yet is kept and shows once a frame
1175    /// declares it, as a strip click on it would; the return says whether
1176    /// the panel lists it now (it lists a declared tab from the first
1177    /// frame it is on). A hidden panel comes back docked, as the
1178    /// picker's does. The panel's `on` is not touched: that is
1179    /// [`Self::set_devtools`]'s. Edge-triggered — called once a frame it
1180    /// would pin the strip against the user's own clicks.
1181    pub fn set_devtools_tab(&mut self, name: &str) -> bool {
1182        if !cfg!(feature = "devtools") {
1183            return false;
1184        }
1185        let listed = {
1186            let mut s = self.session.state();
1187            let d = &mut s.devtools;
1188            let listed = match Tab::parse(name) {
1189                Some(tab) => {
1190                    d.show(tab);
1191                    true
1192                }
1193                None => {
1194                    d.custom = Some(name.to_string());
1195                    d.lists(name)
1196                }
1197            };
1198            if d.dock == Dock::Off {
1199                d.dock = Dock::Right;
1200            }
1201            listed
1202        };
1203        self.devtools_redraw_others();
1204        listed
1205    }
1206
1207    /// The tab the panel is on, by name: one of its own (`facts`,
1208    /// `events`, `tree`) or a declared tab's — what the strip marks,
1209    /// panel on or off, in any window. A declared name the panel stopped
1210    /// listing answers the panel's own tab the strip falls back to.
1211    /// Unlike [`Self::devtools_shown_tab`], which answers only a declared
1212    /// tab on show in the main window for a data binding's function
1213    /// child, this is the selection itself.
1214    pub fn devtools_current_tab(&self) -> String {
1215        let s = self.session.state();
1216        let d = &s.devtools;
1217        match d.shown() {
1218            Shown::Builtin(t) => t.name().to_string(),
1219            Shown::Custom(i) => d.tabs[i].name.clone(),
1220        }
1221    }
1222
1223    /// The key legend the facts tab shows: `(keys, what they do)`.
1224    pub fn set_devtools_legend(&mut self, legend: &[(&str, &str)]) {
1225        self.session.state().devtools.legend = legend
1226            .iter()
1227            .map(|(k, v)| (k.to_string(), v.to_string()))
1228            .collect();
1229    }
1230
1231    /// Whether this core draws the panel's own window — a frame the host
1232    /// builds nothing into.
1233    pub fn devtools_window(&self) -> bool {
1234        self.dt_window
1235    }
1236
1237    /// The app's own theme source: the one in force while no override is,
1238    /// else the one remembered when the override went on — unless the app
1239    /// has set another since, which shows as the source in force not being
1240    /// the override that was applied. That one is the app's now, and it
1241    /// is what the override is lifted back to.
1242    fn app_theme_source(&self) -> ThemeSource {
1243        match self.dt_theme {
1244            Some((app, applied)) if self.theme_source == applied => app,
1245            _ => self.theme_source,
1246        }
1247    }
1248
1249    /// The id of the panel's window while it is open.
1250    fn devtools_window_id(&self) -> Option<WindowId> {
1251        self.session
1252            .state()
1253            .windows
1254            .live()
1255            .into_iter()
1256            .find(|(_, name)| &**name == DEVTOOLS_WINDOW)
1257            .map(|(id, _)| id)
1258    }
1259}
1260
1261// ---------------------------------------------------------------------------
1262// The hooks.
1263
1264impl Core {
1265    /// What the dock leaves of a window `window` big: the
1266    /// viewport a frame begun at that size lays out into, which
1267    /// `viewport()` reports once the frame has begun and a `resize`
1268    /// reports when it changes. It takes the window's size and the dock's
1269    /// state and nothing of the frame, so it answers *before* the first
1270    /// frame too — what a driver's window-size reading hands a host that
1271    /// sizes its model at setup (Node's `KuiWindow.size()`).
1272    pub fn host_area(&self, window: Size) -> Size {
1273        let r = self.devtools_area(window);
1274        Size::new(r.w, r.h)
1275    }
1276
1277    /// Where the current frame laid the host out, in the window's logical
1278    /// px: [`Core::viewport`] with its origin — `x` the pane's width under
1279    /// a left dock, and zero everywhere else, the whole window with the
1280    /// panel off, in a window of its own, or in any window but the main
1281    /// one. The frame's reading, like `viewport()`, so it is zero before
1282    /// the first frame (the pre-frame answer is `host_area`'s size) and it
1283    /// is the rect the quads of `output()` were drawn against: scaled by
1284    /// `scale()` into their physical px, it is what separates the host's
1285    /// quads from the dock's — all but the root's background, which
1286    /// `devtools_configure_root` gives the window as well as the app
1287    /// container, so it fills the whole window beneath the pane. Backlog
1288    /// F92: the origin reached only the Rust
1289    /// runner, through `devtools_inset`, so a Node test could size itself
1290    /// to the host area but not say that nothing of its own left it.
1291    pub fn host_rect(&self) -> Rect {
1292        self.dt_area
1293    }
1294
1295    /// What a docked pane takes off the main window, in the axis it
1296    /// takes it: the side column's width as `(w, 0)`, the bottom strip's
1297    /// height as `(0, h)`, and zero with the panel off, in a window of
1298    /// its own, or asked of any window but the main one. The pane's
1299    /// extent *as the handle left it*, not as the window clamps it — what
1300    /// a driver adds to the app's minimum window size while the panel is
1301    /// docked, so the floor the app declared is a floor on the app and
1302    /// not on the app less the dock (the pomodoro's report, 2026-09-12:
1303    /// a 620×500 minimum with a 340 px dock left the app 280 px, below
1304    /// the tier it was drawn to fit). Read after a frame, since the
1305    /// handle's drag and the placement buttons land in one.
1306    pub fn devtools_inset(&self) -> Size {
1307        if self.env.window.id != WindowId::MAIN {
1308            return Size::ZERO;
1309        }
1310        let s = self.session.state();
1311        let d = &s.devtools;
1312        if !d.on {
1313            return Size::ZERO;
1314        }
1315        match d.dock {
1316            Dock::Left | Dock::Right => Size::new(d.side_w.max(SIDE_MIN_W), 0.0),
1317            Dock::Bottom => Size::new(0.0, d.bottom_h.max(BOTTOM_MIN_H)),
1318            Dock::Window | Dock::Off => Size::ZERO,
1319        }
1320    }
1321
1322    /// The host's viewport for a frame at `viewport`: the
1323    /// window, less the dock when the panel is docked in the main window.
1324    /// The pane keeps its minimum before the app keeps its own, so a
1325    /// window too small for both squeezes the app.
1326    pub(crate) fn devtools_area(&self, viewport: Size) -> Rect {
1327        let window = Rect::new(0.0, 0.0, viewport.w, viewport.h);
1328        if self.env.window.id != WindowId::MAIN {
1329            return window;
1330        }
1331        let s = self.session.state();
1332        let d = &s.devtools;
1333        if !d.on || !d.dock.docked() {
1334            return window;
1335        }
1336        match d.dock {
1337            Dock::Left => {
1338                let w = pane_w(d.side_w, viewport.w);
1339                Rect::new(w, 0.0, (viewport.w - w).max(0.0), viewport.h)
1340            }
1341            Dock::Right => {
1342                let w = pane_w(d.side_w, viewport.w);
1343                Rect::new(0.0, 0.0, (viewport.w - w).max(0.0), viewport.h)
1344            }
1345            Dock::Bottom => {
1346                let h = pane_h(d.bottom_h, viewport.h);
1347                Rect::new(0.0, 0.0, viewport.w, (viewport.h - h).max(0.0))
1348            }
1349            _ => window,
1350        }
1351    }
1352
1353    /// The origin of the host's viewport in window coordinates: nonzero
1354    /// only under a left dock.
1355    #[inline]
1356    pub(crate) fn dt_shift(&self) -> Vec2 {
1357        Vec2::new(self.dt_area.x, self.dt_area.y)
1358    }
1359
1360    /// The coordinates the host is handed, in its own viewport: a drag's point
1361    /// and parent, a layout's rect and parent, a
1362    /// context menu's and a force click's point. A no-op wherever the
1363    /// dock's origin is the window's.
1364    pub(crate) fn devtools_translate(&self, out: &mut [UiEvent]) {
1365        let shift = self.dt_shift();
1366        if shift == Vec2::ZERO {
1367            return;
1368        }
1369        fn shift_xy(v: &mut Value, shift: Vec2) {
1370            if let Value::Map(entries) = v {
1371                for (k, v) in entries.iter_mut() {
1372                    match (k.as_str(), &mut *v) {
1373                        ("x", Value::Float(x)) => *x -= shift.x as f64,
1374                        ("y", Value::Float(y)) => *y -= shift.y as f64,
1375                        ("parent", nested @ Value::Map(_)) => shift_xy(nested, shift),
1376                        _ => {}
1377                    }
1378                }
1379            }
1380        }
1381        for ev in out {
1382            if ev.origin == OriginId::DEVTOOLS {
1383                continue;
1384            }
1385            if matches!(
1386                ev.kind(),
1387                Some("drag" | "layout" | "contextmenu" | "forceclick" | "button")
1388            ) {
1389                shift_xy(&mut ev.payload, shift);
1390            }
1391        }
1392    }
1393
1394    /// The end of `begin_frame`: the overrides for this window, and the
1395    /// wrap of the host's tree in the main one or the
1396    /// deferred root in the panel's own.
1397    /// Puts the host's own menu mode back after an override (see
1398    /// `dt_menus`); nothing to do when there was none.
1399    fn restore_menus(&mut self) {
1400        if let Some((menus, bar)) = self.dt_menus.take() {
1401            self.set_native_menus(menus);
1402            self.set_native_menu_bar(bar);
1403        }
1404    }
1405
1406    pub(crate) fn devtools_begin_frame(&mut self) {
1407        self.dt_app = None;
1408        self.dt_dock = None;
1409        self.dt_window = false;
1410        self.dt_built = None;
1411        // This frame's declarations start empty whatever the panel's state
1412        // and whichever window this is: `after_frame` moves the main
1413        // window's into the session only while the panel is on, and a
1414        // declaration made every frame must not pile up into
1415        // `duplicate-tab` on the second one.
1416        self.dt_tabs.clear();
1417        let main = self.env.window.id == WindowId::MAIN;
1418        let this = if main {
1419            false
1420        } else {
1421            &*self.window_name() == DEVTOOLS_WINDOW
1422        };
1423        let (on, dock, shown, theme, menus, mirror, want_inspect) = {
1424            let s = self.session.state();
1425            let d = &s.devtools;
1426            // The app's own source, for the override to keep the half it
1427            // leaves alone — and the main window's in the panel's own
1428            // window, which has none of its own.
1429            let app = if this {
1430                d.facts.theme_source
1431            } else {
1432                self.app_theme_source()
1433            };
1434            (
1435                d.on,
1436                d.dock,
1437                d.shown(),
1438                d.theme_override(app, &self.env.system),
1439                d.native_menus,
1440                app,
1441                // The panel's need for the node snapshot, derived every
1442                // frame rather than latched (backlog AR38): the tree tab
1443                // showing, or a pick under way. Off, the O(nodes) copy a
1444                // frame stops; the host's own `set_inspect` is apart.
1445                d.on && (d.shown() == Shown::Builtin(Tab::Tree) || d.pick),
1446            )
1447        };
1448        // A menu of the panel's own — a Facts select's — hangs under a
1449        // field this window draws only while it builds the panel: off,
1450        // hidden, or popped into its own window while this is the main
1451        // one, the field is gone and the menu goes with it, or else the
1452        // rows would stay drawn with nobody to answer them (backlog RG2).
1453        // An app's menu is not the panel's to close.
1454        let builds_panel = on && (this || (main && dock.docked()));
1455        if !builds_panel
1456            && self
1457                .menu
1458                .as_ref()
1459                .is_some_and(|m| m.origin == OriginId::DEVTOOLS)
1460        {
1461            self.close_menu();
1462        }
1463        if !on {
1464            if let Some((app, _)) = self.dt_theme.take() {
1465                self.set_theme_source(app);
1466            }
1467            self.restore_menus();
1468            self.dt_inspect = false;
1469            return;
1470        }
1471        // The theme: the override while there is one, and the app's own
1472        // source — remembered from when the override went on — when it
1473        // is taken away again. The panel's own window mirrors the main
1474        // one's app source, since it has none of its own.
1475        match theme {
1476            Some(src) => {
1477                self.dt_theme = Some((mirror, src));
1478                self.set_theme_source(src);
1479            }
1480            None => {
1481                if self.dt_theme.take().is_some() || this {
1482                    self.set_theme_source(mirror);
1483                }
1484            }
1485        }
1486        // The menus the same way: the host's own mode — what the driver
1487        // said at launch — remembered when the override goes on, and put
1488        // back when it is lifted (`platform` in the select, or the panel
1489        // off), since nothing else would.
1490        match menus {
1491            Some(m) => {
1492                if self.dt_menus.is_none() {
1493                    self.dt_menus = Some((self.native_menus, self.native_menu_bar()));
1494                }
1495                self.set_native_menus(m);
1496                self.set_native_menu_bar(m);
1497            }
1498            None => self.restore_menus(),
1499        }
1500        if this {
1501            // The panel's window: no root until `finish`, so what the host
1502            // builds here builds nothing (every builder door is a no-op
1503            // on an empty tree).
1504            self.dt_window = true;
1505            self.tree.clear();
1506            self.stack.clear();
1507            self.counters.clear();
1508            return;
1509        }
1510        if !main {
1511            return;
1512        }
1513        self.dt_inspect = want_inspect;
1514        if dock.docked() {
1515            self.dt_dock = Some(dock);
1516            self.tree.specs[0] = match dock {
1517                Dock::Bottom => NodeSpec::column(),
1518                _ => NodeSpec::row(),
1519            }
1520            .fill();
1521            // A left dock precedes the app in the root row, so it is built
1522            // now, from the last frame's facts and the tab on show, and
1523            // `finish` skips it.
1524            if dock == Dock::Left {
1525                self.build_panel(Place::Main(dock));
1526                self.dt_built = Some(shown);
1527            }
1528            // By key and not by label: the container is not the host's
1529            // to find through `key_of`.
1530            self.open_with_key(Key::ROOT.str(APP_KEY), NodeSpec::column().fill().clip());
1531            // The host's children are keyed from the root, as if the
1532            // container were not there (ADR 0014's namespace, reused).
1533            self.ns_depth = self.stack.len();
1534            self.ns_key = Key::ROOT;
1535            self.dt_app = Some(self.tree.len() - 1);
1536        }
1537    }
1538
1539    /// `configure_root` while the host's tree is wrapped:
1540    /// what lays out and paints the children goes to the container, what
1541    /// is addressed stays on the root.
1542    pub(crate) fn devtools_configure_root(&mut self, app: usize, spec: NodeSpec) {
1543        let key = self.tree.keys[app];
1544        let mut inner = spec.clone();
1545        inner.layout.width = Sizing::Grow(1.0);
1546        inner.layout.height = Sizing::Grow(1.0);
1547        inner.layout.float = None;
1548        if !(inner.layout.scroll_x || inner.layout.scroll_y) {
1549            inner.layout.clip = true;
1550        }
1551        inner.events = None;
1552        inner.access = None;
1553        // The gradient and the backdrop blur are paint over the children's
1554        // box, so they go with them; the rest of the row is addressed
1555        // (RG115).
1556        inner.interact = spec
1557            .interact
1558            .as_ref()
1559            .filter(|i| i.gradient.is_some() || i.backdrop_blur > 0.0)
1560            .map(|i| {
1561                Box::new(crate::spec::InteractSpec {
1562                    gradient: i.gradient.clone(),
1563                    backdrop_blur: i.backdrop_blur,
1564                    ..Default::default()
1565                })
1566            });
1567        inner.focusable = false;
1568        inner.initial_focus = false;
1569        inner.hoverable = false;
1570        inner.cursor = None;
1571        inner.window = None;
1572        self.ease_spec(key, &mut inner);
1573        self.tree.note(&inner, &crate::tree::NodeContent::Container);
1574        self.tree.specs[app] = inner;
1575
1576        let mut outer = self.tree.specs[0].clone();
1577        outer.style.bg = spec.style.bg;
1578        outer.events = spec.events;
1579        outer.access = spec.access;
1580        outer.focusable = spec.focusable;
1581        outer.initial_focus = spec.initial_focus;
1582        outer.hoverable = spec.hoverable;
1583        outer.disabled = spec.disabled;
1584        outer.cursor = spec.cursor;
1585        outer.window = spec.window;
1586        self.tree.note(&outer, &crate::tree::NodeContent::Container);
1587        self.tree.specs[0] = outer;
1588    }
1589
1590    /// `Ui::finish`, before the menu: closes the app container, builds the
1591    /// panel where it goes, and declares its window when it has one.
1592    pub(crate) fn devtools_finish(&mut self) {
1593        if self.dt_window {
1594            // The deferred root (decision 6): the same push `begin_frame`
1595            // makes, and then the panel is the whole tree.
1596            self.tree.push(
1597                crate::tree::NIL,
1598                Key::ROOT,
1599                OriginId::HOST,
1600                NodeSpec::column().fill(),
1601                crate::tree::NodeContent::Container,
1602            );
1603            self.stack.clear();
1604            self.stack.push(0);
1605            self.counters.clear();
1606            self.counters.push(0);
1607            self.ns_depth = usize::MAX;
1608            self.build_panel(Place::Window);
1609            return;
1610        }
1611        if self.env.window.id != WindowId::MAIN {
1612            return;
1613        }
1614        let (on, dock, shown) = {
1615            let s = self.session.state();
1616            (s.devtools.on, s.devtools.dock, s.devtools.shown())
1617        };
1618        if let Some(_app) = self.dt_app.take() {
1619            // The host's last node closed for it, as `finish_frame` does.
1620            self.stack.truncate(1);
1621            self.counters.truncate(1);
1622            self.ns_depth = usize::MAX;
1623        }
1624        // A left panel is built at `begin_frame`, from the state then, and
1625        // is in the tree for good this frame: turned off, moved or put on
1626        // another tab since — an app's door from its `view` — it is drawn
1627        // as it was, so the next frame is asked for, as the right and
1628        // bottom docks' deferral below asks (backlog RG4). With the idle
1629        // loop quiet, nothing else would draw it.
1630        if let Some(built) = self.dt_built
1631            && (!on || self.dt_dock != Some(dock) || built != shown)
1632        {
1633            self.owe_frame("devtools");
1634        }
1635        if !on {
1636            return;
1637        }
1638        if dock == Dock::Window {
1639            let saved = self.origin;
1640            self.origin = OriginId::DEVTOOLS;
1641            self.declare_window(
1642                DEVTOOLS_WINDOW,
1643                WindowConfig::sized(WINDOW_SIZE.w, WINDOW_SIZE.h),
1644            );
1645            self.origin = saved;
1646        }
1647        if self.dt_built.is_some() {
1648            return;
1649        }
1650        // A docked panel is built into the root this frame began with:
1651        // wrapped for this dock, it goes beside the app container. Turned
1652        // on, or moved to this dock, since `begin_frame` — an app's
1653        // `set_devtools` from its `view` — the root is not laid out for it
1654        // (a column, or a row for another side), and a panel built into it
1655        // anyway sat in the bottom-left corner, 340 wide and half the
1656        // height, until the next frame; so it waits for that frame, and
1657        // asks for it. Undocked, there is nothing to lay out around.
1658        if !dock.docked() || self.dt_dock == Some(dock) {
1659            self.build_panel(Place::Main(dock));
1660        } else {
1661            self.owe_frame("devtools");
1662        }
1663    }
1664
1665    /// Opens the `kui-devtools` node under the current cursor and builds
1666    /// the panel — the dock, the outlines, the picker — inside it, with
1667    /// the session's state taken out for the length of the build.
1668    fn build_panel(&mut self, place: Place) {
1669        let saved_origin = self.origin;
1670        self.origin = OriginId::DEVTOOLS;
1671        // The facts the panel reads: this frame's, from this core, when it
1672        // is the main window; the main window's last, from the session,
1673        // in the panel's own.
1674        let mut state = std::mem::take(&mut self.session.state().devtools);
1675        // The nodes the tree tab lists: this core's last frame in the main
1676        // window, the main window's copy in the panel's own — taken out
1677        // for the build, since the build borrows the core.
1678        let nodes = match place {
1679            Place::Main(dock) => {
1680                // A left dock is built at `begin_frame`, before the host
1681                // has declared this frame's title: it reads the facts the
1682                // last frame left, as the panel's own window does.
1683                if dock != Dock::Left {
1684                    state.facts = self.collect_facts(state.inspect_key);
1685                    state.tabs.clone_from(&self.dt_tabs);
1686                } else {
1687                    // Except the window's size, which is this frame's and
1688                    // is what the pane's width is clamped by.
1689                    state.facts.viewport = self.viewport;
1690                }
1691                std::mem::take(&mut self.inspected)
1692            }
1693            Place::Window => std::mem::take(&mut state.nodes),
1694        };
1695        #[cfg(feature = "devtools")]
1696        {
1697            let mut ui = crate::ui::Ui::wrap(self);
1698            panel::build(&mut ui, &mut state, &nodes, place);
1699        }
1700        match place {
1701            Place::Main(_) => self.inspected = nodes,
1702            Place::Window => state.nodes = nodes,
1703        }
1704        // What the build asked of the windows.
1705        if std::mem::take(&mut state.focus_window)
1706            && let Some(id) = self.devtools_window_id()
1707        {
1708            self.interaction
1709                .window_commands
1710                .push(WindowCommand::Focus(id));
1711        }
1712        let redraw_main = std::mem::take(&mut state.redraw_main) && self.dt_window;
1713        self.session.state().devtools = state;
1714        self.origin = saved_origin;
1715        if redraw_main {
1716            self.devtools_redraw_others();
1717        }
1718    }
1719
1720    /// The end of `finish_frame` in the main window: the facts and the
1721    /// nodes for the panel's own window, the warnings into the stream, the
1722    /// frame count.
1723    pub(crate) fn devtools_after_frame(&mut self) {
1724        if self.env.window.id != WindowId::MAIN || !self.session.state().devtools.on {
1725            return;
1726        }
1727        let tabs = std::mem::take(&mut self.dt_tabs);
1728        let raised = self.warnings_raised();
1729        let facts = self.collect_facts(self.devtools_key());
1730        let mut s = self.session.state();
1731        let d = &mut s.devtools;
1732        d.frames = self.frame_no;
1733        d.facts = facts;
1734        d.tabs = tabs;
1735        // The panel's own window draws the tree from a copy; docked, the
1736        // tab reads this core's list itself and the copy is not kept.
1737        // The first copy is what the panel's window is waiting for: it
1738        // drew its tree tab before there was one, so it is asked again.
1739        let mut wake_panel = false;
1740        if d.dock == Dock::Window && d.shown() == Shown::Builtin(Tab::Tree) {
1741            wake_panel = d.nodes.is_empty() && !self.inspected.is_empty();
1742            d.nodes = self.inspected.clone();
1743        } else if !d.nodes.is_empty() {
1744            d.nodes = Vec::new();
1745        }
1746        let new_warnings_empty = raised.len() <= d.warnings_seen;
1747        if raised.len() > d.warnings_seen {
1748            let new: Vec<(String, String)> = raised[d.warnings_seen..]
1749                .iter()
1750                .map(|w| (w.code.to_string(), w.message.clone()))
1751                .collect();
1752            d.warnings_seen = raised.len();
1753            let frame = d.frames;
1754            for (code, message) in new {
1755                d.push(Entry {
1756                    seq: 0,
1757                    frame,
1758                    kind: EntryKind::Warning,
1759                    window: WindowId::MAIN,
1760                    key: Key::ROOT,
1761                    label: None,
1762                    origin: OriginId::DEVTOOLS,
1763                    payload: Value::map([
1764                        ("kind", Value::str("warning")),
1765                        ("code", Value::str(code)),
1766                        ("message", Value::str(message)),
1767                    ]),
1768                    lines: 0,
1769                });
1770            }
1771        }
1772        // Warnings logged while popped out move the stream there too.
1773        wake_panel |= !new_warnings_empty && d.dock == Dock::Window;
1774        drop(s);
1775        if wake_panel {
1776            self.devtools_redraw_others();
1777        }
1778    }
1779
1780    /// The start of `handle_input`: a chord, or `Escape` while picking, is
1781    /// the panel's and the press goes no further.
1782    pub(crate) fn devtools_intercept(&mut self, ev: &InputEvent) -> bool {
1783        let press = match ev {
1784            InputEvent::KeyDown(press) => press,
1785            // The editor channel's Escape is the same key: a driver sends
1786            // both, and while the picker is up neither is anybody else's.
1787            InputEvent::Key(crate::EditKey::Escape, _) => {
1788                let mut s = self.session.state();
1789                let d = &mut s.devtools;
1790                return d.on && (d.pick || std::mem::take(&mut d.escape_owed));
1791            }
1792            _ => return false,
1793        };
1794        if !self.session.state().devtools.on {
1795            return false;
1796        }
1797        if press.code == KeyCode::Escape {
1798            let mut s = self.session.state();
1799            if s.devtools.pick {
1800                s.devtools.pick = false;
1801                s.devtools.pick_hover = None;
1802                s.devtools.pick_keep_tab = false;
1803                s.devtools.escape_owed = true;
1804                drop(s);
1805                self.devtools_redraw_others();
1806                return true;
1807            }
1808            return false;
1809        }
1810        // Any other press settles the owed half: a driver that never
1811        // sends the editor channel owes nothing.
1812        let inspect = {
1813            let mut s = self.session.state();
1814            s.devtools.escape_owed = false;
1815            s.devtools.inspect_key
1816        };
1817        let Some(what) = chord(press, inspect) else {
1818            return false;
1819        };
1820        self.devtools_act(what);
1821        true
1822    }
1823
1824    /// One action, from a chord or a control, with what it asks of the
1825    /// core done after the state borrow ends.
1826    fn devtools_act(&mut self, what: &str) {
1827        let (acted, focus_main) = {
1828            let mut s = self.session.state();
1829            let acted = s.devtools.act(what);
1830            (acted, std::mem::take(&mut s.devtools.focus_main))
1831        };
1832        if focus_main {
1833            self.interaction
1834                .window_commands
1835                .push(WindowCommand::Focus(WindowId::MAIN));
1836        }
1837        if acted {
1838            self.devtools_redraw_others();
1839        }
1840    }
1841
1842    /// The panel's controls in the batch are acted on and dropped, the
1843    /// panel's window's own `window` events too, and in the panel's
1844    /// window nothing at all is the host's.
1845    pub(crate) fn devtools_consume(&mut self, out: &mut Vec<UiEvent>) {
1846        if out.is_empty() {
1847            return;
1848        }
1849        let on = self.session.state().devtools.on;
1850        if !on && !self.dt_window {
1851            // Off, a panel control can still be in the tree the input
1852            // resolves against — the frame the door owes is not drawn
1853            // yet — and its event is nobody's: not the app's, which
1854            // never declared the origin, and not an action either, since
1855            // the panel it would act on is gone (backlog RG2).
1856            out.retain(|ev| ev.origin != OriginId::DEVTOOLS);
1857            return;
1858        }
1859        let mut actions: Vec<String> = Vec::new();
1860        let mut picks = 0usize;
1861        let mut closed = false;
1862        let mut resize: Option<Vec2> = None;
1863        out.retain(|ev| {
1864            if ev.origin == OriginId::DEVTOOLS {
1865                // A click carries its payload flat; a drag's or a sink's
1866                // rides in `tag`; a select's choice is the menu row's
1867                // `item` (`widgets::select`).
1868                let what = ev
1869                    .payload
1870                    .get("dt")
1871                    .or_else(|| ev.payload.get("tag").and_then(|t| t.get("dt")))
1872                    .or_else(|| ev.payload.get("item").and_then(|t| t.get("dt")))
1873                    .and_then(Value::as_str);
1874                match what {
1875                    Some("picked") => picks += 1,
1876                    Some("tree-key") => {
1877                        // The tree list's sink: a press (not a release, not
1878                        // a repeat of a chord) becomes an action naming
1879                        // the key, for the build to move the cursor by.
1880                        let s = |k: &str| ev.payload.get(k).and_then(Value::as_str);
1881                        // Both modifiers ride every key payload, so each
1882                        // is asked on its own: a chord bubbles to the sink
1883                        // (ADR 0011) and must not move the cursor.
1884                        let held = |k: &str| matches!(ev.payload.get(k), Some(Value::Bool(true)));
1885                        let plain = !held("ctrl") && !held("super");
1886                        if s("phase") == Some("down")
1887                            && plain
1888                            && let Some(code) = s("code")
1889                        {
1890                            actions.push(format!("tree-key:{code}"));
1891                        }
1892                    }
1893                    Some("resize") => {
1894                        let f = |k: &str| ev.payload.get(k).and_then(Value::as_float);
1895                        if let (Some(x), Some(y)) = (f("x"), f("y")) {
1896                            resize = Some(Vec2::new(x as f32, y as f32));
1897                        }
1898                    }
1899                    Some(what) => actions.push(what.to_string()),
1900                    None => {}
1901                }
1902                // An edit's `changed` and the like: the build reads the
1903                // field's text; nothing to do here.
1904                return false;
1905            }
1906            if ev.kind() == Some("window") && ev.payload.get_str("name") == Some(DEVTOOLS_WINDOW) {
1907                closed |= ev.payload.get_str("phase") == Some("closed");
1908                return false;
1909            }
1910            !self.dt_window
1911        });
1912        for what in actions {
1913            self.devtools_act(&what);
1914        }
1915        if let Some(p) = resize {
1916            // The handle is dragged in the main window: the pane's extent
1917            // is what the pointer leaves between the edge and itself.
1918            let vp = self.viewport;
1919            let mut s = self.session.state();
1920            let d = &mut s.devtools;
1921            match d.dock {
1922                Dock::Left => d.side_w = pane_w(p.x, vp.w),
1923                Dock::Right => d.side_w = pane_w(vp.w - p.x, vp.w),
1924                Dock::Bottom => d.bottom_h = pane_h(vp.h - p.y, vp.h),
1925                _ => {}
1926            }
1927        }
1928        if picks > 0 {
1929            // The picker's overlay was pressed: the node it was showing is
1930            // the one picked.
1931            let mut s = self.session.state();
1932            let d = &mut s.devtools;
1933            d.pick = false;
1934            if let Some(k) = d.pick_hover.take() {
1935                if std::mem::take(&mut d.pick_keep_tab) {
1936                    d.selected = Some(k);
1937                    d.reveal = Some(k);
1938                } else {
1939                    d.select(k);
1940                }
1941            }
1942            drop(s);
1943            self.devtools_redraw_others();
1944        }
1945        if closed {
1946            // The user closed the panel's window — unless the panel had
1947            // already moved on (a placement button in that very window
1948            // takes it away, and the close that follows is our own).
1949            let mut s = self.session.state();
1950            if s.devtools.dock == Dock::Window {
1951                s.devtools.dock = Dock::Off;
1952                s.devtools.pick = false;
1953                s.devtools.note("dock: off (window closed)");
1954            }
1955        }
1956    }
1957
1958    /// Every event handed to the host, into the stream — after the
1959    /// window stamp, so the row can say which window.
1960    pub(crate) fn devtools_log(&mut self, out: &[UiEvent]) {
1961        if out.is_empty() {
1962            return;
1963        }
1964        let entries: Vec<Entry> = {
1965            let s = self.session.state();
1966            let d = &s.devtools;
1967            if !d.on || d.paused {
1968                return;
1969            }
1970            let frame = d.frames;
1971            out.iter()
1972                .map(|ev| Entry {
1973                    seq: 0,
1974                    frame,
1975                    kind: EntryKind::Event,
1976                    window: ev.window,
1977                    key: ev.key,
1978                    label: if ev.key == Key::ROOT {
1979                        Some("root".into())
1980                    } else {
1981                        self.label_of(ev.key).map(str::to_string)
1982                    },
1983                    origin: ev.origin,
1984                    payload: redact(&ev.payload),
1985                    lines: 0,
1986                })
1987                .collect()
1988        };
1989        let mut s = self.session.state();
1990        for e in entries {
1991            s.devtools.push(e);
1992        }
1993        let popped = s.devtools.dock == Dock::Window;
1994        drop(s);
1995        if popped && !self.dt_window {
1996            self.devtools_redraw_others();
1997        }
1998    }
1999
2000    /// Asks the *other* window that shows the panel's effects to draw
2001    /// again: the main window from the panel's own, the
2002    /// panel's own from anywhere else. Coalesced against the last command
2003    /// queued, since a hover storm is many events.
2004    fn devtools_redraw_others(&mut self) {
2005        let target = if self.dt_window {
2006            Some(WindowId::MAIN)
2007        } else {
2008            self.devtools_window_id()
2009        };
2010        let Some(id) = target else {
2011            return;
2012        };
2013        let cmds = &mut self.interaction.window_commands;
2014        if cmds.last() != Some(&WindowCommand::Redraw(id)) {
2015            cmds.push(WindowCommand::Redraw(id));
2016        }
2017    }
2018
2019    /// The status block, read from the doors it comes from. `inspect` is
2020    /// the chord into the dock, handed in because the main window's build
2021    /// collects with the panel's state taken out of the session (and
2022    /// `devtools_key` would read the default).
2023    fn collect_facts(&self, inspect: Accel) -> Facts {
2024        let env = &self.env;
2025        let vp = self.viewport;
2026        let scale = self.scale;
2027        let name = |k: Key| match self.label_of(k) {
2028            _ if k == Key::ROOT => "root".to_string(),
2029            Some(l) => l.to_string(),
2030            None => format!("{:08x}", k.0 as u32),
2031        };
2032        let focus = self.focus;
2033        let focus_visible = self.focus_visible;
2034        let region = self.region();
2035        let inspect = inspect.display();
2036        let mods = self.modifiers();
2037        let windows: Vec<String> = self
2038            .windows()
2039            .iter()
2040            .map(|(id, name)| format!("{name}#{}", id.0))
2041            .collect();
2042        let source = match self.theme_source {
2043            ThemeSource::Derived => "derived",
2044            ThemeSource::DerivedWithAccent(_) => "derived + accent",
2045            ThemeSource::Pinned(_) => "pinned",
2046        };
2047        let opt = |s: Option<String>| s.unwrap_or_else(|| "—".into());
2048        let hex = |c: Color| format!("#{:06x}", c.to_hex() >> 8);
2049        let rows: Vec<(&'static str, String)> = vec![
2050            ("appearance", env.system.appearance.name().into()),
2051            ("motion", env.system.motion.name().into()),
2052            ("locale", opt(env.system.locale.map(|l| l.to_string()))),
2053            ("assistive", env.system.assistive.name().into()),
2054            (
2055                "theme",
2056                format!("{} · {source}", self.theme.appearance.name()),
2057            ),
2058            ("accent", hex(self.theme.accent)),
2059            (
2060                "menus",
2061                if self.native_menus { "native" } else { "drawn" }.into(),
2062            ),
2063            (
2064                "window",
2065                format!(
2066                    "#{}{}{}{}{} · {}",
2067                    env.window.id.0,
2068                    if env.window.custom_chrome {
2069                        " custom-chrome"
2070                    } else {
2071                        ""
2072                    },
2073                    if env.window.maximized {
2074                        " maximized"
2075                    } else {
2076                        ""
2077                    },
2078                    if env.window.fullscreen {
2079                        " fullscreen"
2080                    } else {
2081                        ""
2082                    },
2083                    if env.window.always_on_top {
2084                        " on-top"
2085                    } else {
2086                        ""
2087                    },
2088                    windows.join(" "),
2089                ),
2090            ),
2091            (
2092                "viewport",
2093                format!(
2094                    "{}{}×{} @{scale} · {}",
2095                    // The app's, then the window's when the dock has
2096                    // taken some of it.
2097                    if self.dt_area.w != vp.w || self.dt_area.h != vp.h {
2098                        format!("{}×{} of ", self.dt_area.w.round(), self.dt_area.h.round())
2099                    } else {
2100                        String::new()
2101                    },
2102                    vp.w.round(),
2103                    vp.h.round(),
2104                    opt(env.refresh_hz.map(|hz| format!("{hz:.0} Hz"))),
2105                ),
2106            ),
2107            (
2108                "keyboard",
2109                if env.focused {
2110                    "this window"
2111                } else {
2112                    "elsewhere"
2113                }
2114                .into(),
2115            ),
2116            (
2117                "focus",
2118                match focus.map(name) {
2119                    Some(l) if focus_visible => format!("{l} · ring"),
2120                    Some(l) => l,
2121                    None => "—".into(),
2122                },
2123            ),
2124            (
2125                "region",
2126                match region.map(name) {
2127                    Some(l) => format!("{l} · {inspect} leaves"),
2128                    None => format!("main · {inspect} enters the dock"),
2129                },
2130            ),
2131            ("modifiers", {
2132                let mut m = Vec::new();
2133                if mods.shift {
2134                    m.push("shift");
2135                }
2136                if mods.ctrl {
2137                    m.push("ctrl");
2138                }
2139                if mods.alt {
2140                    m.push("alt");
2141                }
2142                if mods.super_key {
2143                    m.push("super");
2144                }
2145                if m.is_empty() {
2146                    "—".into()
2147                } else {
2148                    m.join("+")
2149                }
2150            }),
2151            (
2152                "audio",
2153                format!("{} · {} live", env.audio.device.name(), env.audio.live),
2154            ),
2155            ("nodes", format!("{}", self.inspected.len())),
2156            ("fonts", {
2157                // Which installed faces the three generic families are on
2158                // this machine (backlog C32), so a wrong one says so on
2159                // screen.
2160                let [sans, serif, mono] = self.default_font_families();
2161                format!("{sans} · {serif} · {mono}")
2162            }),
2163        ];
2164        let mut origins: Vec<&OriginId> = self.tokens.keys().collect();
2165        origins.sort_by_key(|o| o.0);
2166        let mut tokens = Vec::new();
2167        for origin in origins {
2168            let table = &self.tokens[origin];
2169            for (i, (name, _)) in table.colors().iter().enumerate() {
2170                let (light, dark) = table.halves(i as u16, &self.theme);
2171                tokens.push(TokenFact {
2172                    origin: *origin,
2173                    name: name.clone(),
2174                    kind: crate::tokens::TokenKind::Color,
2175                    light,
2176                    dark,
2177                    resolved: table.resolve_color(i as u16, &self.theme),
2178                    length: 0.0,
2179                    recipe: table.recipe(i as u16),
2180                });
2181            }
2182            for (name, v) in table.lengths() {
2183                tokens.push(TokenFact {
2184                    origin: *origin,
2185                    name: name.clone(),
2186                    kind: crate::tokens::TokenKind::Length,
2187                    light: Color::TRANSPARENT,
2188                    dark: Color::TRANSPARENT,
2189                    resolved: Color::TRANSPARENT,
2190                    length: *v,
2191                    recipe: None,
2192                });
2193            }
2194        }
2195        Facts {
2196            title: self
2197                .window_title
2198                .clone()
2199                .unwrap_or_else(|| "kui".to_string()),
2200            rows,
2201            tokens,
2202            theme_source: self.app_theme_source(),
2203            app_appearance: self.app_theme_source().resolve(&env.system).appearance,
2204            viewport: vp,
2205            focus,
2206            focus_visible,
2207            region,
2208            hovered: self.interaction.hovered(),
2209            pressed: self.interaction.pressed_key(),
2210            cursor: self.interaction.cursor(),
2211        }
2212    }
2213}
2214
2215/// A `Value` as one line of data: `{kind: click, n: 3}`.
2216pub fn fmt_value(v: &Value) -> String {
2217    let mut out = String::new();
2218    write_value(v, &mut out);
2219    out
2220}
2221
2222fn write_value(v: &Value, out: &mut String) {
2223    match v {
2224        Value::Null => out.push_str("null"),
2225        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
2226        Value::Int(i) => out.push_str(&i.to_string()),
2227        Value::Float(f) => {
2228            if f.fract() == 0.0 && f.abs() < 1e9 {
2229                out.push_str(&format!("{f:.0}"));
2230            } else {
2231                out.push_str(&format!("{f:.2}"));
2232            }
2233        }
2234        Value::Str(s) => {
2235            if s.chars().any(|c| c.is_whitespace() || c == ',' || c == '}') || s.is_empty() {
2236                out.push_str(&format!("{s:?}"));
2237            } else {
2238                out.push_str(s);
2239            }
2240        }
2241        Value::List(items) => {
2242            out.push('[');
2243            for (i, item) in items.iter().enumerate() {
2244                if i > 0 {
2245                    out.push_str(", ");
2246                }
2247                write_value(item, out);
2248            }
2249            out.push(']');
2250        }
2251        Value::Map(entries) => {
2252            out.push('{');
2253            for (i, (k, v)) in entries.iter().enumerate() {
2254                if i > 0 {
2255                    out.push_str(", ");
2256                }
2257                out.push_str(k);
2258                out.push_str(": ");
2259                write_value(v, out);
2260            }
2261            out.push('}');
2262        }
2263    }
2264}
2265
2266/// How many lines `v` takes as an indented tree: a map's entries and a
2267/// list's items are one each, nested ones under theirs; a scalar is one
2268/// line, and a map or list is its entries alone (its own line is the
2269/// row's summary).
2270pub(crate) fn lines(v: &Value) -> u16 {
2271    fn count(v: &Value) -> usize {
2272        match v {
2273            Value::Map(entries) => entries.iter().map(|(_, v)| 1 + nested(v)).sum::<usize>(),
2274            Value::List(items) => items.iter().map(|v| 1 + nested(v)).sum::<usize>(),
2275            _ => 1,
2276        }
2277    }
2278    fn nested(v: &Value) -> usize {
2279        match v {
2280            Value::Map(_) | Value::List(_) => count(v),
2281            _ => 0,
2282        }
2283    }
2284    count(v).min(u16::MAX as usize) as u16
2285}
2286
2287/// A side pane's width for a window `vw` wide: what the handle left,
2288/// within the pane's floor and what the app needs — the floor winning
2289/// when the window has room for neither.
2290fn pane_w(side_w: f32, vw: f32) -> f32 {
2291    side_w.min((vw - APP_MIN).max(SIDE_MIN_W)).max(SIDE_MIN_W)
2292}
2293
2294fn pane_h(bottom_h: f32, vh: f32) -> f32 {
2295    bottom_h
2296        .min((vh - APP_MIN).max(BOTTOM_MIN_H))
2297        .max(BOTTOM_MIN_H)
2298}
2299
2300/// Where a build is drawing the panel.
2301#[derive(Clone, Copy, PartialEq, Eq)]
2302enum Place {
2303    /// The main window: the dock (or nothing, for `Window` and `Off`)
2304    /// plus the outlines and the picker.
2305    Main(Dock),
2306    /// The panel's own window: the panel is the whole tree.
2307    Window,
2308}
2309
2310/// A payload as the stream keeps it: a paste the pasteboard marked
2311/// concealed — a password from a password manager — keeps its
2312/// markers and its length and loses its text, which the events tab would
2313/// otherwise show in plain view and hold for the stream's lifetime.
2314fn redact(payload: &Value) -> Value {
2315    let concealed = payload.get_bool("concealed") == Some(true);
2316    match payload {
2317        Value::Map(fields) if concealed => Value::Map(
2318            fields
2319                .iter()
2320                .map(|(k, v)| match (k.as_str(), v) {
2321                    ("text", Value::Str(t)) => (
2322                        k.clone(),
2323                        Value::str(format!("‹concealed, {} chars›", t.chars().count())),
2324                    ),
2325                    _ => (k.clone(), v.clone()),
2326                })
2327                .collect(),
2328        ),
2329        _ => payload.clone(),
2330    }
2331}