Skip to main content

car_browser/
models.rs

1//! Core browser domain types.
2//!
3//! These types represent the perception boundary: what the AI can see
4//! and how it can interact with web content.
5
6use serde::{Deserialize, Serialize};
7
8/// An element in the accessibility tree.
9///
10/// This is the primary unit of perception. Each node represents something
11/// a screen reader (e.g., VoiceOver) would announce to a blind user.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct A11yNode {
14    /// Unique identifier for this node within the page.
15    pub node_id: String,
16    /// ARIA role or inferred role from element type.
17    pub role: String,
18    /// Accessible name (aria-label or text content).
19    pub name: Option<String>,
20    /// Current value (for form inputs).
21    pub value: Option<String>,
22    /// Bounding box in viewport coordinates.
23    pub bounds: Bounds,
24    /// IDs of child nodes.
25    pub children: Vec<String>,
26    /// Whether this element can receive focus.
27    pub focusable: bool,
28    /// Whether this element currently has focus.
29    pub focused: bool,
30    /// Whether this element is disabled.
31    pub disabled: bool,
32}
33
34/// Rectangle bounds in viewport coordinates.
35#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
36pub struct Bounds {
37    pub x: f64,
38    pub y: f64,
39    pub width: f64,
40    pub height: f64,
41}
42
43impl Bounds {
44    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
45        Self {
46            x,
47            y,
48            width,
49            height,
50        }
51    }
52
53    /// Get the center point of the bounds.
54    pub fn center(&self) -> (f64, f64) {
55        (self.x + self.width / 2.0, self.y + self.height / 2.0)
56    }
57
58    /// Check if the bounds contain a point.
59    pub fn contains(&self, x: f64, y: f64) -> bool {
60        x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
61    }
62
63    /// Check if this bounds overlaps with another.
64    pub fn overlaps(&self, other: &Bounds) -> bool {
65        self.x < other.x + other.width
66            && self.x + self.width > other.x
67            && self.y < other.y + other.height
68            && self.y + self.height > other.y
69    }
70
71    /// Compute intersection-over-union with another bounds rectangle.
72    pub fn iou(&self, other: &Bounds) -> f64 {
73        let x1 = self.x.max(other.x);
74        let y1 = self.y.max(other.y);
75        let x2 = (self.x + self.width).min(other.x + other.width);
76        let y2 = (self.y + self.height).min(other.y + other.height);
77
78        if x2 <= x1 || y2 <= y1 {
79            return 0.0;
80        }
81
82        let intersection = (x2 - x1) * (y2 - y1);
83        let self_area = self.width * self.height;
84        let other_area = other.width * other.height;
85        let union = self_area + other_area - intersection;
86
87        if union <= 0.0 {
88            0.0
89        } else {
90            intersection / union
91        }
92    }
93}
94
95/// Viewport dimensions.
96#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
97pub struct Viewport {
98    pub width: u32,
99    pub height: u32,
100    pub device_pixel_ratio: f64,
101}
102
103/// Keyboard modifier keys.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum Modifier {
107    Shift,
108    Control,
109    Alt,
110    Meta,
111}
112
113/// Conditions to wait for.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(tag = "type", rename_all = "snake_case")]
116pub enum WaitCondition {
117    /// Wait for the URL to change.
118    UrlChanged,
119    /// Wait for specific text to appear in the accessibility tree.
120    A11yContainsText { text: String },
121    /// Wait for a specific element to exist in the accessibility tree.
122    ElementWithName {
123        name_contains: String,
124        #[serde(skip_serializing_if = "Option::is_none")]
125        role: Option<String>,
126    },
127    /// Wait for page load to complete.
128    PageLoaded,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_bounds_center() {
137        let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
138        let (cx, cy) = bounds.center();
139        assert_eq!(cx, 125.0);
140        assert_eq!(cy, 215.0);
141    }
142
143    #[test]
144    fn test_bounds_contains() {
145        let bounds = Bounds::new(100.0, 200.0, 50.0, 30.0);
146        assert!(bounds.contains(125.0, 215.0));
147        assert!(!bounds.contains(50.0, 215.0));
148    }
149
150    #[test]
151    fn test_bounds_iou() {
152        let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
153        let b = Bounds::new(5.0, 5.0, 10.0, 10.0);
154        let iou = a.iou(&b);
155        // intersection = 5*5 = 25, union = 100 + 100 - 25 = 175
156        assert!((iou - 25.0 / 175.0).abs() < 0.001);
157    }
158
159    #[test]
160    fn test_bounds_no_overlap() {
161        let a = Bounds::new(0.0, 0.0, 10.0, 10.0);
162        let b = Bounds::new(20.0, 20.0, 10.0, 10.0);
163        assert_eq!(a.iou(&b), 0.0);
164        assert!(!a.overlaps(&b));
165    }
166}