Skip to main content

argui_inspect/
lib.rs

1use std::{cell::RefCell, collections::VecDeque, rc::Rc, time::Duration};
2
3use argui_core::{Point, Rect};
4
5mod frames;
6mod memory;
7pub use memory::MemorySnapshot;
8mod trace;
9pub use frames::FrameCursor;
10
11use trace::TraceDocument;
12pub use trace::{TRACE_VERSION, TraceError};
13
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15pub struct InspectNodeId(pub u64);
16
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub enum StyleProperty {
19    Background,
20    Border,
21    Opacity,
22    Overflow,
23    Transform,
24    Layer,
25    Effects,
26    Width,
27    Height,
28}
29
30impl StyleProperty {
31    pub const ALL: [Self; 9] = [
32        Self::Background,
33        Self::Border,
34        Self::Opacity,
35        Self::Overflow,
36        Self::Transform,
37        Self::Layer,
38        Self::Effects,
39        Self::Width,
40        Self::Height,
41    ];
42
43    pub const fn label(self) -> &'static str {
44        match self {
45            Self::Background => "background",
46            Self::Border => "border",
47            Self::Opacity => "opacity",
48            Self::Overflow => "overflow",
49            Self::Transform => "transform",
50            Self::Layer => "layer",
51            Self::Effects => "effects",
52            Self::Width => "width",
53            Self::Height => "height",
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub enum StyleUnit {
60    #[default]
61    Auto,
62    Px,
63    Percent,
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq)]
67pub struct StyleLength {
68    pub value: f32,
69    pub unit: StyleUnit,
70}
71
72#[derive(Clone, Debug, PartialEq)]
73pub struct StyleField {
74    pub label: String,
75    pub value: f32,
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub enum StyleValue {
80    Length(StyleLength),
81    Number(f32),
82    Srgba([f32; 4]),
83    Parameters(Vec<StyleField>),
84    Choice(String),
85    Summary(String),
86}
87
88impl StyleValue {
89    #[must_use]
90    pub fn fields(&self) -> Vec<StyleField> {
91        match self {
92            Self::Length(length) if length.unit != StyleUnit::Auto => vec![StyleField {
93                label: match length.unit {
94                    StyleUnit::Px => "px",
95                    StyleUnit::Percent => "%",
96                    StyleUnit::Auto => unreachable!(),
97                }
98                .into(),
99                value: if length.unit == StyleUnit::Percent {
100                    length.value * 100.0
101                } else {
102                    length.value
103                },
104            }],
105            Self::Number(value) => vec![StyleField {
106                label: "value".into(),
107                value: *value,
108            }],
109            Self::Srgba(values) => ["r", "g", "b", "a"]
110                .into_iter()
111                .zip(values)
112                .map(|(label, value)| StyleField {
113                    label: label.into(),
114                    value: *value,
115                })
116                .collect(),
117            Self::Parameters(fields) => fields.clone(),
118            Self::Length(_) | Self::Choice(_) | Self::Summary(_) => Vec::new(),
119        }
120    }
121
122    pub fn set_field(&mut self, index: usize, value: f32) -> bool {
123        match self {
124            Self::Length(length) if index == 0 && length.unit != StyleUnit::Auto => {
125                length.value = if length.unit == StyleUnit::Percent {
126                    value / 100.0
127                } else {
128                    value
129                };
130                true
131            }
132            Self::Number(current) if index == 0 => {
133                *current = value;
134                true
135            }
136            Self::Srgba(values) => values.get_mut(index).is_some_and(|current| {
137                *current = value.clamp(0.0, 1.0);
138                true
139            }),
140            Self::Parameters(fields) => fields.get_mut(index).is_some_and(|field| {
141                field.value = value;
142                true
143            }),
144            _ => false,
145        }
146    }
147
148    #[must_use]
149    pub fn summary(&self) -> String {
150        match self {
151            Self::Length(length) => match length.unit {
152                StyleUnit::Auto => "auto".into(),
153                StyleUnit::Px => format!("{:.2}px", length.value),
154                StyleUnit::Percent => format!("{:.2}%", length.value * 100.0),
155            },
156            Self::Number(value) => format!("{value:.3}"),
157            Self::Srgba([red, green, blue, alpha]) => {
158                format!("srgba({red:.3}, {green:.3}, {blue:.3}, {alpha:.3})")
159            }
160            Self::Parameters(fields) => format!("{} parameters", fields.len()),
161            Self::Choice(value) | Self::Summary(value) => value.clone(),
162        }
163    }
164}
165
166#[derive(Clone, Debug, PartialEq)]
167pub struct PropertySnapshot {
168    pub property: StyleProperty,
169    pub authored: bool,
170    pub value: StyleValue,
171}
172
173#[derive(Clone, Debug, PartialEq)]
174pub struct PortalSnapshot {
175    pub layer: String,
176    pub anchor: Option<String>,
177    pub requested: Option<String>,
178    pub resolved: Option<String>,
179    pub available_size: argui_core::Size,
180    pub constrained_width: bool,
181    pub constrained_height: bool,
182}
183
184#[derive(Clone, Debug, PartialEq)]
185pub struct NodeSnapshot {
186    pub id: InspectNodeId,
187    pub parent: Option<InspectNodeId>,
188    pub depth: usize,
189    pub key: Option<String>,
190    pub kind: String,
191    pub summary: Option<String>,
192    pub bounds: Rect,
193    pub clip: Option<Rect>,
194    pub z_index: i32,
195    pub portal: Option<PortalSnapshot>,
196    pub visible: bool,
197    pub painted: bool,
198    pub interactive: bool,
199    pub child_count: usize,
200    pub properties: Vec<PropertySnapshot>,
201}
202
203#[derive(Clone, Debug, Default, PartialEq)]
204pub struct TreeSnapshot {
205    pub revision: u64,
206    pub nodes: Vec<NodeSnapshot>,
207}
208
209#[derive(Clone, Debug, Default, Eq, PartialEq)]
210pub struct AdapterRecord {
211    pub name: String,
212    pub vendor: u32,
213    pub device: u32,
214    pub device_type: String,
215    pub driver: String,
216    pub driver_info: String,
217    pub backend: String,
218    pub features: String,
219    pub timestamp_queries: bool,
220    pub max_texture_dimension_2d: u32,
221    pub max_buffer_size: u64,
222    pub max_storage_buffer_binding_size: u64,
223    pub max_bind_groups: u32,
224}
225
226#[derive(Clone, Debug, Default, PartialEq)]
227pub struct GpuPassRecord {
228    pub label: String,
229    pub start: Duration,
230    pub duration: Duration,
231    pub pixels: u64,
232    pub object_domain: Option<String>,
233    pub object_id: Option<u64>,
234}
235
236#[derive(Clone, Debug, Default, PartialEq)]
237pub struct GpuFrameRecord {
238    pub sequence: u64,
239    pub total: Duration,
240    pub passes: Vec<GpuPassRecord>,
241}
242
243#[derive(Clone, Debug, Default, PartialEq)]
244pub struct FrameRecord {
245    pub interval: Duration,
246    pub model: Duration,
247    pub surface: Duration,
248    pub tree: Duration,
249    pub layout: Duration,
250    pub paint: Duration,
251    pub render_cpu: Duration,
252    pub resize_events: u32,
253    pub update: Invalidation,
254    pub layers: usize,
255    pub passes: usize,
256    pub offscreen_pixels: u64,
257    pub cached_layers: usize,
258    pub damaged_pixels: u64,
259    pub textures: usize,
260    pub reused_textures: usize,
261    pub texture_bytes: u64,
262    pub vector_atlas_bytes: u64,
263    pub vector_atlas_entries: usize,
264    pub vector_atlas_hits: usize,
265    pub vector_rasterizations: usize,
266    pub adapter: AdapterRecord,
267    pub gpu: Option<GpuFrameRecord>,
268}
269
270impl FrameRecord {
271    #[must_use]
272    pub fn total_cpu(&self) -> Duration {
273        self.model + self.surface + self.tree + self.layout + self.paint + self.render_cpu
274    }
275}
276
277#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
278pub enum Invalidation {
279    #[default]
280    None,
281    Paint,
282    Layout,
283}
284
285#[derive(Clone, Debug)]
286struct InspectorState {
287    memory: Option<MemorySnapshot>,
288    memory_requested: bool,
289    tree: TreeSnapshot,
290    frames: VecDeque<FrameRecord>,
291    capacity: usize,
292    selected: Option<InspectNodeId>,
293    hovered: Option<InspectNodeId>,
294    overrides: Vec<StyleOverride>,
295    paused: bool,
296    recording: bool,
297    gpu_profiling: bool,
298    frame_sequence: u64,
299    frame_epoch: u64,
300}
301
302#[derive(Clone, Debug)]
303struct StyleOverride {
304    node: InspectNodeId,
305    property: StyleProperty,
306    enabled: bool,
307    value: Option<StyleValue>,
308}
309
310#[derive(Clone, Debug)]
311pub struct InspectorHandle(Rc<RefCell<InspectorState>>);
312
313impl InspectorHandle {
314    #[must_use]
315    pub fn new(capacity: usize) -> Self {
316        Self(Rc::new(RefCell::new(InspectorState {
317            memory: None,
318            memory_requested: false,
319            tree: TreeSnapshot::default(),
320            frames: VecDeque::with_capacity(capacity),
321            capacity,
322            selected: None,
323            hovered: None,
324            overrides: Vec::new(),
325            paused: false,
326            recording: true,
327            gpu_profiling: true,
328            frame_sequence: 0,
329            frame_epoch: 0,
330        })))
331    }
332
333    pub fn publish_tree(&self, tree: TreeSnapshot) {
334        self.0.borrow_mut().tree = tree;
335    }
336
337    #[must_use]
338    pub fn tree(&self) -> TreeSnapshot {
339        self.0.borrow().tree.clone()
340    }
341
342    pub fn with_tree<T>(&self, read: impl FnOnce(&TreeSnapshot) -> T) -> T {
343        read(&self.0.borrow().tree)
344    }
345
346    #[must_use]
347    pub fn node(&self, id: InspectNodeId) -> Option<NodeSnapshot> {
348        self.with_tree(|tree| tree.nodes.iter().find(|node| node.id == id).cloned())
349    }
350
351    pub fn record_ui(&self, record: FrameRecord) {
352        let mut state = self.0.borrow_mut();
353        if state.paused || !state.recording || state.capacity == 0 {
354            return;
355        }
356        if state.frames.len() == state.capacity {
357            state.frames.pop_front();
358        }
359        state.frames.push_back(record);
360        state.frame_sequence = state.frame_sequence.wrapping_add(1);
361    }
362
363    pub fn record_render(&self, record: FrameRecord) {
364        let mut state = self.0.borrow_mut();
365        if state.paused || !state.recording || state.capacity == 0 {
366            return;
367        }
368        if let Some(frame) = state.frames.back_mut() {
369            frame.render_cpu = record.render_cpu;
370            frame.layers = record.layers;
371            frame.passes = record.passes;
372            frame.offscreen_pixels = record.offscreen_pixels;
373            frame.cached_layers = record.cached_layers;
374            frame.damaged_pixels = record.damaged_pixels;
375            frame.textures = record.textures;
376            frame.reused_textures = record.reused_textures;
377            frame.texture_bytes = record.texture_bytes;
378            frame.vector_atlas_bytes = record.vector_atlas_bytes;
379            frame.vector_atlas_entries = record.vector_atlas_entries;
380            frame.vector_atlas_hits = record.vector_atlas_hits;
381            frame.vector_rasterizations = record.vector_rasterizations;
382            frame.adapter = record.adapter;
383            frame.gpu = record.gpu;
384        } else {
385            state.frames.push_back(record);
386            state.frame_sequence = state.frame_sequence.wrapping_add(1);
387        }
388    }
389
390    #[must_use]
391    pub fn frames(&self) -> Vec<FrameRecord> {
392        self.0.borrow().frames.iter().cloned().collect()
393    }
394
395    pub fn clear_frames(&self) {
396        let mut state = self.0.borrow_mut();
397        state.frames.clear();
398        state.frame_epoch = state.frame_epoch.wrapping_add(1);
399    }
400
401    pub fn set_paused(&self, paused: bool) {
402        self.0.borrow_mut().paused = paused;
403    }
404
405    #[must_use]
406    pub fn paused(&self) -> bool {
407        self.0.borrow().paused
408    }
409
410    pub fn set_recording(&self, recording: bool) {
411        self.0.borrow_mut().recording = recording;
412    }
413
414    #[must_use]
415    pub fn recording(&self) -> bool {
416        let state = self.0.borrow();
417        state.recording && !state.paused
418    }
419
420    pub fn select(&self, node: Option<InspectNodeId>) {
421        self.0.borrow_mut().selected = node;
422    }
423
424    #[must_use]
425    pub fn selected(&self) -> Option<InspectNodeId> {
426        self.0.borrow().selected
427    }
428
429    pub fn set_hovered(&self, node: Option<InspectNodeId>) {
430        self.0.borrow_mut().hovered = node;
431    }
432
433    #[must_use]
434    pub fn highlighted(&self) -> Option<InspectNodeId> {
435        self.0.borrow().hovered
436    }
437
438    /// Returns the visually foremost inspected node containing `point`.
439    /// Later nodes win equal stacking levels, matching retained paint order;
440    /// deeper nodes win inside the same branch.
441    #[must_use]
442    pub fn hit_test(&self, point: Point, viewport: Rect) -> Option<InspectNodeId> {
443        self.hit_stack(point, viewport).into_iter().next()
444    }
445
446    /// Returns every inspected node under `point`, ordered from the most useful
447    /// visual target to structural fallbacks.
448    #[must_use]
449    pub fn hit_stack(&self, point: Point, viewport: Rect) -> Vec<InspectNodeId> {
450        if !viewport.contains(point) {
451            return Vec::new();
452        }
453        let state = self.0.borrow();
454        let mut nodes = state
455            .tree
456            .nodes
457            .iter()
458            .enumerate()
459            .filter(|(_, node)| {
460                node.visible
461                    && node.bounds.contains(point)
462                    && node.clip.is_none_or(|clip| clip.contains(point))
463            })
464            .collect::<Vec<_>>();
465        nodes.sort_by_key(|(order, node)| {
466            (
467                node.painted || node.interactive,
468                node.z_index,
469                node.child_count != 0,
470                node.depth,
471                *order,
472            )
473        });
474        nodes.into_iter().rev().map(|(_, node)| node.id).collect()
475    }
476
477    pub fn toggle(&self, node: InspectNodeId, property: StyleProperty) -> bool {
478        let mut state = self.0.borrow_mut();
479        if let Some(entry) = state
480            .overrides
481            .iter_mut()
482            .find(|entry| entry.node == node && entry.property == property)
483        {
484            entry.enabled = !entry.enabled;
485            entry.enabled
486        } else {
487            state.overrides.push(StyleOverride {
488                node,
489                property,
490                enabled: false,
491                value: None,
492            });
493            false
494        }
495    }
496
497    #[must_use]
498    pub fn property_enabled(&self, node: InspectNodeId, property: StyleProperty) -> Option<bool> {
499        self.0.borrow().overrides.iter().find_map(|entry| {
500            (entry.node == node && entry.property == property).then_some(entry.enabled)
501        })
502    }
503
504    pub fn set_property_value(
505        &self,
506        node: InspectNodeId,
507        property: StyleProperty,
508        value: StyleValue,
509    ) {
510        let mut state = self.0.borrow_mut();
511        if let Some(entry) = state
512            .overrides
513            .iter_mut()
514            .find(|entry| entry.node == node && entry.property == property)
515        {
516            entry.enabled = true;
517            entry.value = Some(value);
518        } else {
519            state.overrides.push(StyleOverride {
520                node,
521                property,
522                enabled: true,
523                value: Some(value),
524            });
525        }
526    }
527
528    #[must_use]
529    pub fn property_value(
530        &self,
531        node: InspectNodeId,
532        property: StyleProperty,
533    ) -> Option<StyleValue> {
534        self.0
535            .borrow()
536            .overrides
537            .iter()
538            .find(|entry| entry.node == node && entry.property == property)
539            .and_then(|entry| entry.value.clone())
540    }
541
542    pub fn clear_overrides(&self) {
543        self.0.borrow_mut().overrides.clear();
544    }
545
546    /// Nodes with edited or disabled properties; allows hosts to retain untouched subtrees.
547    #[must_use]
548    pub fn overridden_nodes(&self) -> std::collections::HashSet<InspectNodeId> {
549        self.0
550            .borrow()
551            .overrides
552            .iter()
553            .map(|entry| entry.node)
554            .collect()
555    }
556
557    /// Restore the authored value and enabled state of a single property.
558    pub fn clear_property_override(&self, node: InspectNodeId, property: StyleProperty) {
559        self.0
560            .borrow_mut()
561            .overrides
562            .retain(|entry| entry.node != node || entry.property != property);
563    }
564
565    pub fn trace_json(&self) -> Result<String, TraceError> {
566        let state = self.0.borrow();
567        serde_json::to_string_pretty(&TraceDocument::capture(
568            state.tree.revision,
569            state.tree.nodes.len(),
570            state.selected,
571            &state.frames,
572        ))
573        .map_err(|error| TraceError::InvalidJson(error.to_string()))
574    }
575
576    pub fn import_trace_json(&self, json: &str) -> Result<(), TraceError> {
577        let document: TraceDocument = serde_json::from_str(json)
578            .map_err(|error| TraceError::InvalidJson(error.to_string()))?;
579        document.validate_version()?;
580        let mut state = self.0.borrow_mut();
581        let (revision, selected, frames) = document.into_records();
582        state.frames = frames.into();
583        state.frame_epoch = state.frame_epoch.wrapping_add(1);
584        state.tree.revision = revision;
585        state.selected = selected;
586        Ok(())
587    }
588}
589
590impl Default for InspectorHandle {
591    fn default() -> Self {
592        Self::new(300)
593    }
594}