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 #[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 #[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 #[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 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}