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, ¶ms, &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 inner.interact = None;
1554 inner.focusable = false;
1555 inner.initial_focus = false;
1556 inner.hoverable = false;
1557 inner.cursor = None;
1558 inner.window = None;
1559 self.ease_spec(key, &mut inner);
1560 self.tree.note(&inner, &crate::tree::NodeContent::Container);
1561 self.tree.specs[app] = inner;
1562
1563 let mut outer = self.tree.specs[0].clone();
1564 outer.style.bg = spec.style.bg;
1565 outer.events = spec.events;
1566 outer.access = spec.access;
1567 outer.focusable = spec.focusable;
1568 outer.initial_focus = spec.initial_focus;
1569 outer.hoverable = spec.hoverable;
1570 outer.disabled = spec.disabled;
1571 outer.cursor = spec.cursor;
1572 outer.window = spec.window;
1573 self.tree.note(&outer, &crate::tree::NodeContent::Container);
1574 self.tree.specs[0] = outer;
1575 }
1576
1577 /// `Ui::finish`, before the menu: closes the app container, builds the
1578 /// panel where it goes, and declares its window when it has one.
1579 pub(crate) fn devtools_finish(&mut self) {
1580 if self.dt_window {
1581 // The deferred root (decision 6): the same push `begin_frame`
1582 // makes, and then the panel is the whole tree.
1583 self.tree.push(
1584 crate::tree::NIL,
1585 Key::ROOT,
1586 OriginId::HOST,
1587 NodeSpec::column().fill(),
1588 crate::tree::NodeContent::Container,
1589 );
1590 self.stack.clear();
1591 self.stack.push(0);
1592 self.counters.clear();
1593 self.counters.push(0);
1594 self.ns_depth = usize::MAX;
1595 self.build_panel(Place::Window);
1596 return;
1597 }
1598 if self.env.window.id != WindowId::MAIN {
1599 return;
1600 }
1601 let (on, dock, shown) = {
1602 let s = self.session.state();
1603 (s.devtools.on, s.devtools.dock, s.devtools.shown())
1604 };
1605 if let Some(_app) = self.dt_app.take() {
1606 // The host's last node closed for it, as `finish_frame` does.
1607 self.stack.truncate(1);
1608 self.counters.truncate(1);
1609 self.ns_depth = usize::MAX;
1610 }
1611 // A left panel is built at `begin_frame`, from the state then, and
1612 // is in the tree for good this frame: turned off, moved or put on
1613 // another tab since — an app's door from its `view` — it is drawn
1614 // as it was, so the next frame is asked for, as the right and
1615 // bottom docks' deferral below asks (backlog RG4). With the idle
1616 // loop quiet, nothing else would draw it.
1617 if let Some(built) = self.dt_built
1618 && (!on || self.dt_dock != Some(dock) || built != shown)
1619 {
1620 self.owe_frame("devtools");
1621 }
1622 if !on {
1623 return;
1624 }
1625 if dock == Dock::Window {
1626 let saved = self.origin;
1627 self.origin = OriginId::DEVTOOLS;
1628 self.declare_window(
1629 DEVTOOLS_WINDOW,
1630 WindowConfig::sized(WINDOW_SIZE.w, WINDOW_SIZE.h),
1631 );
1632 self.origin = saved;
1633 }
1634 if self.dt_built.is_some() {
1635 return;
1636 }
1637 // A docked panel is built into the root this frame began with:
1638 // wrapped for this dock, it goes beside the app container. Turned
1639 // on, or moved to this dock, since `begin_frame` — an app's
1640 // `set_devtools` from its `view` — the root is not laid out for it
1641 // (a column, or a row for another side), and a panel built into it
1642 // anyway sat in the bottom-left corner, 340 wide and half the
1643 // height, until the next frame; so it waits for that frame, and
1644 // asks for it. Undocked, there is nothing to lay out around.
1645 if !dock.docked() || self.dt_dock == Some(dock) {
1646 self.build_panel(Place::Main(dock));
1647 } else {
1648 self.owe_frame("devtools");
1649 }
1650 }
1651
1652 /// Opens the `kui-devtools` node under the current cursor and builds
1653 /// the panel — the dock, the outlines, the picker — inside it, with
1654 /// the session's state taken out for the length of the build.
1655 fn build_panel(&mut self, place: Place) {
1656 let saved_origin = self.origin;
1657 self.origin = OriginId::DEVTOOLS;
1658 // The facts the panel reads: this frame's, from this core, when it
1659 // is the main window; the main window's last, from the session,
1660 // in the panel's own.
1661 let mut state = std::mem::take(&mut self.session.state().devtools);
1662 // The nodes the tree tab lists: this core's last frame in the main
1663 // window, the main window's copy in the panel's own — taken out
1664 // for the build, since the build borrows the core.
1665 let nodes = match place {
1666 Place::Main(dock) => {
1667 // A left dock is built at `begin_frame`, before the host
1668 // has declared this frame's title: it reads the facts the
1669 // last frame left, as the panel's own window does.
1670 if dock != Dock::Left {
1671 state.facts = self.collect_facts(state.inspect_key);
1672 state.tabs.clone_from(&self.dt_tabs);
1673 } else {
1674 // Except the window's size, which is this frame's and
1675 // is what the pane's width is clamped by.
1676 state.facts.viewport = self.viewport;
1677 }
1678 std::mem::take(&mut self.inspected)
1679 }
1680 Place::Window => std::mem::take(&mut state.nodes),
1681 };
1682 #[cfg(feature = "devtools")]
1683 {
1684 let mut ui = crate::ui::Ui::wrap(self);
1685 panel::build(&mut ui, &mut state, &nodes, place);
1686 }
1687 match place {
1688 Place::Main(_) => self.inspected = nodes,
1689 Place::Window => state.nodes = nodes,
1690 }
1691 // What the build asked of the windows.
1692 if std::mem::take(&mut state.focus_window)
1693 && let Some(id) = self.devtools_window_id()
1694 {
1695 self.interaction
1696 .window_commands
1697 .push(WindowCommand::Focus(id));
1698 }
1699 let redraw_main = std::mem::take(&mut state.redraw_main) && self.dt_window;
1700 self.session.state().devtools = state;
1701 self.origin = saved_origin;
1702 if redraw_main {
1703 self.devtools_redraw_others();
1704 }
1705 }
1706
1707 /// The end of `finish_frame` in the main window: the facts and the
1708 /// nodes for the panel's own window, the warnings into the stream, the
1709 /// frame count.
1710 pub(crate) fn devtools_after_frame(&mut self) {
1711 if self.env.window.id != WindowId::MAIN || !self.session.state().devtools.on {
1712 return;
1713 }
1714 let tabs = std::mem::take(&mut self.dt_tabs);
1715 let raised = self.warnings_raised();
1716 let facts = self.collect_facts(self.devtools_key());
1717 let mut s = self.session.state();
1718 let d = &mut s.devtools;
1719 d.frames = self.frame_no;
1720 d.facts = facts;
1721 d.tabs = tabs;
1722 // The panel's own window draws the tree from a copy; docked, the
1723 // tab reads this core's list itself and the copy is not kept.
1724 // The first copy is what the panel's window is waiting for: it
1725 // drew its tree tab before there was one, so it is asked again.
1726 let mut wake_panel = false;
1727 if d.dock == Dock::Window && d.shown() == Shown::Builtin(Tab::Tree) {
1728 wake_panel = d.nodes.is_empty() && !self.inspected.is_empty();
1729 d.nodes = self.inspected.clone();
1730 } else if !d.nodes.is_empty() {
1731 d.nodes = Vec::new();
1732 }
1733 let new_warnings_empty = raised.len() <= d.warnings_seen;
1734 if raised.len() > d.warnings_seen {
1735 let new: Vec<(String, String)> = raised[d.warnings_seen..]
1736 .iter()
1737 .map(|w| (w.code.to_string(), w.message.clone()))
1738 .collect();
1739 d.warnings_seen = raised.len();
1740 let frame = d.frames;
1741 for (code, message) in new {
1742 d.push(Entry {
1743 seq: 0,
1744 frame,
1745 kind: EntryKind::Warning,
1746 window: WindowId::MAIN,
1747 key: Key::ROOT,
1748 label: None,
1749 origin: OriginId::DEVTOOLS,
1750 payload: Value::map([
1751 ("kind", Value::str("warning")),
1752 ("code", Value::str(code)),
1753 ("message", Value::str(message)),
1754 ]),
1755 lines: 0,
1756 });
1757 }
1758 }
1759 // Warnings logged while popped out move the stream there too.
1760 wake_panel |= !new_warnings_empty && d.dock == Dock::Window;
1761 drop(s);
1762 if wake_panel {
1763 self.devtools_redraw_others();
1764 }
1765 }
1766
1767 /// The start of `handle_input`: a chord, or `Escape` while picking, is
1768 /// the panel's and the press goes no further.
1769 pub(crate) fn devtools_intercept(&mut self, ev: &InputEvent) -> bool {
1770 let press = match ev {
1771 InputEvent::KeyDown(press) => press,
1772 // The editor channel's Escape is the same key: a driver sends
1773 // both, and while the picker is up neither is anybody else's.
1774 InputEvent::Key(crate::EditKey::Escape, _) => {
1775 let mut s = self.session.state();
1776 let d = &mut s.devtools;
1777 return d.on && (d.pick || std::mem::take(&mut d.escape_owed));
1778 }
1779 _ => return false,
1780 };
1781 if !self.session.state().devtools.on {
1782 return false;
1783 }
1784 if press.code == KeyCode::Escape {
1785 let mut s = self.session.state();
1786 if s.devtools.pick {
1787 s.devtools.pick = false;
1788 s.devtools.pick_hover = None;
1789 s.devtools.pick_keep_tab = false;
1790 s.devtools.escape_owed = true;
1791 drop(s);
1792 self.devtools_redraw_others();
1793 return true;
1794 }
1795 return false;
1796 }
1797 // Any other press settles the owed half: a driver that never
1798 // sends the editor channel owes nothing.
1799 let inspect = {
1800 let mut s = self.session.state();
1801 s.devtools.escape_owed = false;
1802 s.devtools.inspect_key
1803 };
1804 let Some(what) = chord(press, inspect) else {
1805 return false;
1806 };
1807 self.devtools_act(what);
1808 true
1809 }
1810
1811 /// One action, from a chord or a control, with what it asks of the
1812 /// core done after the state borrow ends.
1813 fn devtools_act(&mut self, what: &str) {
1814 let (acted, focus_main) = {
1815 let mut s = self.session.state();
1816 let acted = s.devtools.act(what);
1817 (acted, std::mem::take(&mut s.devtools.focus_main))
1818 };
1819 if focus_main {
1820 self.interaction
1821 .window_commands
1822 .push(WindowCommand::Focus(WindowId::MAIN));
1823 }
1824 if acted {
1825 self.devtools_redraw_others();
1826 }
1827 }
1828
1829 /// The panel's controls in the batch are acted on and dropped, the
1830 /// panel's window's own `window` events too, and in the panel's
1831 /// window nothing at all is the host's.
1832 pub(crate) fn devtools_consume(&mut self, out: &mut Vec<UiEvent>) {
1833 if out.is_empty() {
1834 return;
1835 }
1836 let on = self.session.state().devtools.on;
1837 if !on && !self.dt_window {
1838 // Off, a panel control can still be in the tree the input
1839 // resolves against — the frame the door owes is not drawn
1840 // yet — and its event is nobody's: not the app's, which
1841 // never declared the origin, and not an action either, since
1842 // the panel it would act on is gone (backlog RG2).
1843 out.retain(|ev| ev.origin != OriginId::DEVTOOLS);
1844 return;
1845 }
1846 let mut actions: Vec<String> = Vec::new();
1847 let mut picks = 0usize;
1848 let mut closed = false;
1849 let mut resize: Option<Vec2> = None;
1850 out.retain(|ev| {
1851 if ev.origin == OriginId::DEVTOOLS {
1852 // A click carries its payload flat; a drag's or a sink's
1853 // rides in `tag`; a select's choice is the menu row's
1854 // `item` (`widgets::select`).
1855 let what = ev
1856 .payload
1857 .get("dt")
1858 .or_else(|| ev.payload.get("tag").and_then(|t| t.get("dt")))
1859 .or_else(|| ev.payload.get("item").and_then(|t| t.get("dt")))
1860 .and_then(Value::as_str);
1861 match what {
1862 Some("picked") => picks += 1,
1863 Some("tree-key") => {
1864 // The tree list's sink: a press (not a release, not
1865 // a repeat of a chord) becomes an action naming
1866 // the key, for the build to move the cursor by.
1867 let s = |k: &str| ev.payload.get(k).and_then(Value::as_str);
1868 // Both modifiers ride every key payload, so each
1869 // is asked on its own: a chord bubbles to the sink
1870 // (ADR 0011) and must not move the cursor.
1871 let held = |k: &str| matches!(ev.payload.get(k), Some(Value::Bool(true)));
1872 let plain = !held("ctrl") && !held("super");
1873 if s("phase") == Some("down")
1874 && plain
1875 && let Some(code) = s("code")
1876 {
1877 actions.push(format!("tree-key:{code}"));
1878 }
1879 }
1880 Some("resize") => {
1881 let f = |k: &str| ev.payload.get(k).and_then(Value::as_float);
1882 if let (Some(x), Some(y)) = (f("x"), f("y")) {
1883 resize = Some(Vec2::new(x as f32, y as f32));
1884 }
1885 }
1886 Some(what) => actions.push(what.to_string()),
1887 None => {}
1888 }
1889 // An edit's `changed` and the like: the build reads the
1890 // field's text; nothing to do here.
1891 return false;
1892 }
1893 if ev.kind() == Some("window") && ev.payload.get_str("name") == Some(DEVTOOLS_WINDOW) {
1894 closed |= ev.payload.get_str("phase") == Some("closed");
1895 return false;
1896 }
1897 !self.dt_window
1898 });
1899 for what in actions {
1900 self.devtools_act(&what);
1901 }
1902 if let Some(p) = resize {
1903 // The handle is dragged in the main window: the pane's extent
1904 // is what the pointer leaves between the edge and itself.
1905 let vp = self.viewport;
1906 let mut s = self.session.state();
1907 let d = &mut s.devtools;
1908 match d.dock {
1909 Dock::Left => d.side_w = pane_w(p.x, vp.w),
1910 Dock::Right => d.side_w = pane_w(vp.w - p.x, vp.w),
1911 Dock::Bottom => d.bottom_h = pane_h(vp.h - p.y, vp.h),
1912 _ => {}
1913 }
1914 }
1915 if picks > 0 {
1916 // The picker's overlay was pressed: the node it was showing is
1917 // the one picked.
1918 let mut s = self.session.state();
1919 let d = &mut s.devtools;
1920 d.pick = false;
1921 if let Some(k) = d.pick_hover.take() {
1922 if std::mem::take(&mut d.pick_keep_tab) {
1923 d.selected = Some(k);
1924 d.reveal = Some(k);
1925 } else {
1926 d.select(k);
1927 }
1928 }
1929 drop(s);
1930 self.devtools_redraw_others();
1931 }
1932 if closed {
1933 // The user closed the panel's window — unless the panel had
1934 // already moved on (a placement button in that very window
1935 // takes it away, and the close that follows is our own).
1936 let mut s = self.session.state();
1937 if s.devtools.dock == Dock::Window {
1938 s.devtools.dock = Dock::Off;
1939 s.devtools.pick = false;
1940 s.devtools.note("dock: off (window closed)");
1941 }
1942 }
1943 }
1944
1945 /// Every event handed to the host, into the stream — after the
1946 /// window stamp, so the row can say which window.
1947 pub(crate) fn devtools_log(&mut self, out: &[UiEvent]) {
1948 if out.is_empty() {
1949 return;
1950 }
1951 let entries: Vec<Entry> = {
1952 let s = self.session.state();
1953 let d = &s.devtools;
1954 if !d.on || d.paused {
1955 return;
1956 }
1957 let frame = d.frames;
1958 out.iter()
1959 .map(|ev| Entry {
1960 seq: 0,
1961 frame,
1962 kind: EntryKind::Event,
1963 window: ev.window,
1964 key: ev.key,
1965 label: if ev.key == Key::ROOT {
1966 Some("root".into())
1967 } else {
1968 self.label_of(ev.key).map(str::to_string)
1969 },
1970 origin: ev.origin,
1971 payload: redact(&ev.payload),
1972 lines: 0,
1973 })
1974 .collect()
1975 };
1976 let mut s = self.session.state();
1977 for e in entries {
1978 s.devtools.push(e);
1979 }
1980 let popped = s.devtools.dock == Dock::Window;
1981 drop(s);
1982 if popped && !self.dt_window {
1983 self.devtools_redraw_others();
1984 }
1985 }
1986
1987 /// Asks the *other* window that shows the panel's effects to draw
1988 /// again: the main window from the panel's own, the
1989 /// panel's own from anywhere else. Coalesced against the last command
1990 /// queued, since a hover storm is many events.
1991 fn devtools_redraw_others(&mut self) {
1992 let target = if self.dt_window {
1993 Some(WindowId::MAIN)
1994 } else {
1995 self.devtools_window_id()
1996 };
1997 let Some(id) = target else {
1998 return;
1999 };
2000 let cmds = &mut self.interaction.window_commands;
2001 if cmds.last() != Some(&WindowCommand::Redraw(id)) {
2002 cmds.push(WindowCommand::Redraw(id));
2003 }
2004 }
2005
2006 /// The status block, read from the doors it comes from. `inspect` is
2007 /// the chord into the dock, handed in because the main window's build
2008 /// collects with the panel's state taken out of the session (and
2009 /// `devtools_key` would read the default).
2010 fn collect_facts(&self, inspect: Accel) -> Facts {
2011 let env = &self.env;
2012 let vp = self.viewport;
2013 let scale = self.scale;
2014 let name = |k: Key| match self.label_of(k) {
2015 _ if k == Key::ROOT => "root".to_string(),
2016 Some(l) => l.to_string(),
2017 None => format!("{:08x}", k.0 as u32),
2018 };
2019 let focus = self.focus;
2020 let focus_visible = self.focus_visible;
2021 let region = self.region();
2022 let inspect = inspect.display();
2023 let mods = self.modifiers();
2024 let windows: Vec<String> = self
2025 .windows()
2026 .iter()
2027 .map(|(id, name)| format!("{name}#{}", id.0))
2028 .collect();
2029 let source = match self.theme_source {
2030 ThemeSource::Derived => "derived",
2031 ThemeSource::DerivedWithAccent(_) => "derived + accent",
2032 ThemeSource::Pinned(_) => "pinned",
2033 };
2034 let opt = |s: Option<String>| s.unwrap_or_else(|| "—".into());
2035 let hex = |c: Color| format!("#{:06x}", c.to_hex() >> 8);
2036 let rows: Vec<(&'static str, String)> = vec![
2037 ("appearance", env.system.appearance.name().into()),
2038 ("motion", env.system.motion.name().into()),
2039 ("locale", opt(env.system.locale.map(|l| l.to_string()))),
2040 ("assistive", env.system.assistive.name().into()),
2041 (
2042 "theme",
2043 format!("{} · {source}", self.theme.appearance.name()),
2044 ),
2045 ("accent", hex(self.theme.accent)),
2046 (
2047 "menus",
2048 if self.native_menus { "native" } else { "drawn" }.into(),
2049 ),
2050 (
2051 "window",
2052 format!(
2053 "#{}{}{}{}{} · {}",
2054 env.window.id.0,
2055 if env.window.custom_chrome {
2056 " custom-chrome"
2057 } else {
2058 ""
2059 },
2060 if env.window.maximized {
2061 " maximized"
2062 } else {
2063 ""
2064 },
2065 if env.window.fullscreen {
2066 " fullscreen"
2067 } else {
2068 ""
2069 },
2070 if env.window.always_on_top {
2071 " on-top"
2072 } else {
2073 ""
2074 },
2075 windows.join(" "),
2076 ),
2077 ),
2078 (
2079 "viewport",
2080 format!(
2081 "{}{}×{} @{scale} · {}",
2082 // The app's, then the window's when the dock has
2083 // taken some of it.
2084 if self.dt_area.w != vp.w || self.dt_area.h != vp.h {
2085 format!("{}×{} of ", self.dt_area.w.round(), self.dt_area.h.round())
2086 } else {
2087 String::new()
2088 },
2089 vp.w.round(),
2090 vp.h.round(),
2091 opt(env.refresh_hz.map(|hz| format!("{hz:.0} Hz"))),
2092 ),
2093 ),
2094 (
2095 "keyboard",
2096 if env.focused {
2097 "this window"
2098 } else {
2099 "elsewhere"
2100 }
2101 .into(),
2102 ),
2103 (
2104 "focus",
2105 match focus.map(name) {
2106 Some(l) if focus_visible => format!("{l} · ring"),
2107 Some(l) => l,
2108 None => "—".into(),
2109 },
2110 ),
2111 (
2112 "region",
2113 match region.map(name) {
2114 Some(l) => format!("{l} · {inspect} leaves"),
2115 None => format!("main · {inspect} enters the dock"),
2116 },
2117 ),
2118 ("modifiers", {
2119 let mut m = Vec::new();
2120 if mods.shift {
2121 m.push("shift");
2122 }
2123 if mods.ctrl {
2124 m.push("ctrl");
2125 }
2126 if mods.alt {
2127 m.push("alt");
2128 }
2129 if mods.super_key {
2130 m.push("super");
2131 }
2132 if m.is_empty() {
2133 "—".into()
2134 } else {
2135 m.join("+")
2136 }
2137 }),
2138 (
2139 "audio",
2140 format!("{} · {} live", env.audio.device.name(), env.audio.live),
2141 ),
2142 ("nodes", format!("{}", self.inspected.len())),
2143 ("fonts", {
2144 // Which installed faces the three generic families are on
2145 // this machine (backlog C32), so a wrong one says so on
2146 // screen.
2147 let [sans, serif, mono] = self.default_font_families();
2148 format!("{sans} · {serif} · {mono}")
2149 }),
2150 ];
2151 let mut origins: Vec<&OriginId> = self.tokens.keys().collect();
2152 origins.sort_by_key(|o| o.0);
2153 let mut tokens = Vec::new();
2154 for origin in origins {
2155 let table = &self.tokens[origin];
2156 for (i, (name, _)) in table.colors().iter().enumerate() {
2157 let (light, dark) = table.halves(i as u16, &self.theme);
2158 tokens.push(TokenFact {
2159 origin: *origin,
2160 name: name.clone(),
2161 kind: crate::tokens::TokenKind::Color,
2162 light,
2163 dark,
2164 resolved: table.resolve_color(i as u16, &self.theme),
2165 length: 0.0,
2166 recipe: table.recipe(i as u16),
2167 });
2168 }
2169 for (name, v) in table.lengths() {
2170 tokens.push(TokenFact {
2171 origin: *origin,
2172 name: name.clone(),
2173 kind: crate::tokens::TokenKind::Length,
2174 light: Color::TRANSPARENT,
2175 dark: Color::TRANSPARENT,
2176 resolved: Color::TRANSPARENT,
2177 length: *v,
2178 recipe: None,
2179 });
2180 }
2181 }
2182 Facts {
2183 title: self
2184 .window_title
2185 .clone()
2186 .unwrap_or_else(|| "kui".to_string()),
2187 rows,
2188 tokens,
2189 theme_source: self.app_theme_source(),
2190 app_appearance: self.app_theme_source().resolve(&env.system).appearance,
2191 viewport: vp,
2192 focus,
2193 focus_visible,
2194 region,
2195 hovered: self.interaction.hovered(),
2196 pressed: self.interaction.pressed_key(),
2197 cursor: self.interaction.cursor(),
2198 }
2199 }
2200}
2201
2202/// A `Value` as one line of data: `{kind: click, n: 3}`.
2203pub fn fmt_value(v: &Value) -> String {
2204 let mut out = String::new();
2205 write_value(v, &mut out);
2206 out
2207}
2208
2209fn write_value(v: &Value, out: &mut String) {
2210 match v {
2211 Value::Null => out.push_str("null"),
2212 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
2213 Value::Int(i) => out.push_str(&i.to_string()),
2214 Value::Float(f) => {
2215 if f.fract() == 0.0 && f.abs() < 1e9 {
2216 out.push_str(&format!("{f:.0}"));
2217 } else {
2218 out.push_str(&format!("{f:.2}"));
2219 }
2220 }
2221 Value::Str(s) => {
2222 if s.chars().any(|c| c.is_whitespace() || c == ',' || c == '}') || s.is_empty() {
2223 out.push_str(&format!("{s:?}"));
2224 } else {
2225 out.push_str(s);
2226 }
2227 }
2228 Value::List(items) => {
2229 out.push('[');
2230 for (i, item) in items.iter().enumerate() {
2231 if i > 0 {
2232 out.push_str(", ");
2233 }
2234 write_value(item, out);
2235 }
2236 out.push(']');
2237 }
2238 Value::Map(entries) => {
2239 out.push('{');
2240 for (i, (k, v)) in entries.iter().enumerate() {
2241 if i > 0 {
2242 out.push_str(", ");
2243 }
2244 out.push_str(k);
2245 out.push_str(": ");
2246 write_value(v, out);
2247 }
2248 out.push('}');
2249 }
2250 }
2251}
2252
2253/// How many lines `v` takes as an indented tree: a map's entries and a
2254/// list's items are one each, nested ones under theirs; a scalar is one
2255/// line, and a map or list is its entries alone (its own line is the
2256/// row's summary).
2257pub(crate) fn lines(v: &Value) -> u16 {
2258 fn count(v: &Value) -> usize {
2259 match v {
2260 Value::Map(entries) => entries.iter().map(|(_, v)| 1 + nested(v)).sum::<usize>(),
2261 Value::List(items) => items.iter().map(|v| 1 + nested(v)).sum::<usize>(),
2262 _ => 1,
2263 }
2264 }
2265 fn nested(v: &Value) -> usize {
2266 match v {
2267 Value::Map(_) | Value::List(_) => count(v),
2268 _ => 0,
2269 }
2270 }
2271 count(v).min(u16::MAX as usize) as u16
2272}
2273
2274/// A side pane's width for a window `vw` wide: what the handle left,
2275/// within the pane's floor and what the app needs — the floor winning
2276/// when the window has room for neither.
2277fn pane_w(side_w: f32, vw: f32) -> f32 {
2278 side_w.min((vw - APP_MIN).max(SIDE_MIN_W)).max(SIDE_MIN_W)
2279}
2280
2281fn pane_h(bottom_h: f32, vh: f32) -> f32 {
2282 bottom_h
2283 .min((vh - APP_MIN).max(BOTTOM_MIN_H))
2284 .max(BOTTOM_MIN_H)
2285}
2286
2287/// Where a build is drawing the panel.
2288#[derive(Clone, Copy, PartialEq, Eq)]
2289enum Place {
2290 /// The main window: the dock (or nothing, for `Window` and `Off`)
2291 /// plus the outlines and the picker.
2292 Main(Dock),
2293 /// The panel's own window: the panel is the whole tree.
2294 Window,
2295}
2296
2297/// A payload as the stream keeps it: a paste the pasteboard marked
2298/// concealed — a password from a password manager — keeps its
2299/// markers and its length and loses its text, which the events tab would
2300/// otherwise show in plain view and hold for the stream's lifetime.
2301fn redact(payload: &Value) -> Value {
2302 let concealed = payload.get_bool("concealed") == Some(true);
2303 match payload {
2304 Value::Map(fields) if concealed => Value::Map(
2305 fields
2306 .iter()
2307 .map(|(k, v)| match (k.as_str(), v) {
2308 ("text", Value::Str(t)) => (
2309 k.clone(),
2310 Value::str(format!("‹concealed, {} chars›", t.chars().count())),
2311 ),
2312 _ => (k.clone(), v.clone()),
2313 })
2314 .collect(),
2315 ),
2316 _ => payload.clone(),
2317 }
2318}