victauri_core/snapshot.rs
1//! DOM snapshot and window state types for webview introspection.
2
3use serde::{Deserialize, Serialize};
4
5/// Current state of a Tauri window including geometry, visibility, and loaded URL.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct WindowState {
8 /// Tauri window label (e.g. "main", "notification").
9 pub label: String,
10 /// Window title bar text.
11 pub title: String,
12 /// URL currently loaded in the webview.
13 pub url: String,
14 /// Whether the window is visible on screen.
15 pub visible: bool,
16 /// Whether the window currently has input focus.
17 pub focused: bool,
18 /// Whether the window is maximized.
19 pub maximized: bool,
20 /// Whether the window is minimized.
21 pub minimized: bool,
22 /// Whether the window is in fullscreen mode.
23 pub fullscreen: bool,
24 /// Window position as (x, y) in screen coordinates.
25 pub position: (i32, i32),
26 /// Window dimensions as (width, height) in pixels.
27 pub size: (u32, u32),
28}
29
30/// A point-in-time snapshot of the DOM accessible tree from a specific webview.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct DomSnapshot {
33 /// Label of the webview this snapshot was taken from.
34 pub webview_label: String,
35 /// Top-level accessible elements in the DOM tree.
36 pub elements: Vec<DomElement>,
37 /// Maps ref IDs to CSS selectors for element lookup.
38 pub ref_map: std::collections::BTreeMap<String, String>,
39}
40
41/// A single element in the accessible DOM tree with semantic metadata and ref handle.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct DomElement {
44 /// Unique ref handle for this element (e.g. "e3"), used to target interactions.
45 pub ref_id: String,
46 /// HTML tag name (e.g. "div", "button").
47 pub tag: String,
48 /// ARIA role if present (e.g. "button", "navigation").
49 pub role: Option<String>,
50 /// Accessible name derived from aria-label, text content, or other heuristics.
51 pub name: Option<String>,
52 /// Visible text content of the element.
53 pub text: Option<String>,
54 /// Form input value, if applicable.
55 pub value: Option<String>,
56 /// Whether the element is interactive (not disabled).
57 pub enabled: bool,
58 /// Whether the element is visible in the viewport.
59 pub visible: bool,
60 /// Whether the element can receive keyboard focus.
61 pub focusable: bool,
62 /// Pixel-level bounding rectangle, if available.
63 pub bounds: Option<ElementBounds>,
64 /// Nested child elements forming the accessible subtree.
65 pub children: Vec<Self>,
66 /// Raw HTML attributes on the element.
67 pub attributes: std::collections::BTreeMap<String, String>,
68}
69
70/// Pixel-level bounding rectangle of a DOM element relative to the viewport.
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct ElementBounds {
73 /// Left edge offset from viewport origin.
74 pub x: f64,
75 /// Top edge offset from viewport origin.
76 pub y: f64,
77 /// Element width in CSS pixels.
78 pub width: f64,
79 /// Element height in CSS pixels.
80 pub height: f64,
81}
82
83impl DomSnapshot {
84 /// Renders the snapshot as indented accessible text (roles, names, and ref handles).
85 ///
86 /// # Examples
87 ///
88 /// ```
89 /// use victauri_core::{DomSnapshot, DomElement};
90 /// use std::collections::BTreeMap;
91 ///
92 /// let snapshot = DomSnapshot {
93 /// webview_label: "main".to_string(),
94 /// elements: vec![DomElement {
95 /// ref_id: "e1".to_string(),
96 /// tag: "button".to_string(),
97 /// role: Some("button".to_string()),
98 /// name: Some("Submit".to_string()),
99 /// text: None,
100 /// value: None,
101 /// enabled: true,
102 /// visible: true,
103 /// focusable: true,
104 /// bounds: None,
105 /// children: vec![],
106 /// attributes: BTreeMap::new(),
107 /// }],
108 /// ref_map: BTreeMap::new(),
109 /// };
110 /// let text = snapshot.to_accessible_text(0);
111 /// assert!(text.contains("button"));
112 /// assert!(text.contains("Submit"));
113 /// assert!(text.contains("[ref=e1]"));
114 /// ```
115 #[must_use]
116 pub fn to_accessible_text(&self, indent: usize) -> String {
117 let mut output = String::new();
118 for element in &self.elements {
119 Self::format_element(&mut output, element, indent);
120 }
121 output
122 }
123
124 fn format_element(output: &mut String, element: &DomElement, indent: usize) {
125 if !element.visible {
126 return;
127 }
128
129 let prefix = " ".repeat(indent);
130
131 let role_str = element.role.as_deref().unwrap_or(&element.tag);
132 let name_str = element
133 .name
134 .as_ref()
135 .map(|n| format!(" \"{n}\""))
136 .unwrap_or_default();
137 let ref_str = if element.focusable || element.tag == "button" || element.tag == "input" {
138 format!(" [ref={}]", element.ref_id)
139 } else {
140 String::new()
141 };
142
143 let line = format!("{prefix}- {role_str}{name_str}{ref_str}\n");
144 output.push_str(&line);
145
146 for child in &element.children {
147 Self::format_element(output, child, indent + 1);
148 }
149 }
150}