Skip to main content

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}