1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::roles::{Action, Role};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct Rect {
17 pub row: i64,
19 pub column: i64,
21 pub width: i64,
23 pub height: i64,
25}
26
27impl Rect {
28 pub fn new(row: i64, column: i64, width: i64, height: i64) -> Self {
30 Self {
31 row,
32 column,
33 width,
34 height,
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
42pub enum Observation<T> {
43 Known {
45 value: T,
47 evidence: String,
49 },
50 Absent {
52 reason: String,
54 },
55 Unknown {
57 reason: String,
59 },
60 Unsupported {
62 capability: String,
64 reason: String,
66 },
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72pub struct NodeGeometryObservations {
73 pub displayed: Observation<bool>,
75 pub intended_rect: Observation<Rect>,
77 pub visible_rect: Observation<Rect>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct PointerHitRegion {
85 pub rect: Rect,
87 pub recipient_id: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct PointerHitGrid {
95 pub regions: Vec<PointerHitRegion>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(untagged)]
102pub enum Checked {
103 Flag(bool),
105 Mixed(MixedState),
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum MixedState {
112 #[serde(rename = "mixed")]
114 Mixed,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "lowercase")]
120pub enum Orientation {
121 Horizontal,
123 Vertical,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "lowercase")]
130pub enum CursorShape {
131 Block,
133 Underline,
135 Bar,
137}
138
139#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct State {
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub disabled: Option<bool>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub focused: Option<bool>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub selected: Option<bool>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub checked: Option<Checked>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub expanded: Option<bool>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub modal: Option<bool>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub busy: Option<bool>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub hidden: Option<bool>,
167 #[serde(skip_serializing_if = "Option::is_none")]
171 pub offscreen: Option<bool>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub readonly: Option<bool>,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub multiline: Option<bool>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub orientation: Option<Orientation>,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub level: Option<i64>,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub position_in_set: Option<i64>,
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub set_size: Option<i64>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub scroll_offset: Option<i64>,
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub scroll_extent: Option<i64>,
196}
197
198impl State {
199 pub fn is_empty(&self) -> bool {
201 *self == State::default()
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct TextRange {
209 pub start_offset: i64,
211 pub end_offset: i64,
213 pub rect: Rect,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase", deny_unknown_fields)]
220pub struct Node {
221 pub id: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub parent_id: Option<String>,
226 pub role: Role,
228 pub name: String,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub description: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub value: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub bounds: Option<Rect>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub state: Option<State>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub extended: Option<BTreeMap<String, Value>>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub actions: Option<Vec<Action>>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub labelled_by: Option<Vec<String>>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub described_by: Option<Vec<String>>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub text_ranges: Option<Vec<TextRange>>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub test_id: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
264 pub framework_type: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
269 pub occlusion: Option<Occlusion>,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub p: Option<Provenance>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub px: Option<BTreeMap<String, Provenance>>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub geometry: Option<NodeGeometryObservations>,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum Occlusion {
285 Known,
287 Unknown,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "lowercase")]
294pub enum Provenance {
295 Annotation,
297 Recognizer,
299 Framework,
301 Correlation,
303 Heuristic,
305}
306
307impl Node {
308 pub fn new(id: impl Into<String>, role: Role, name: impl Into<String>) -> Self {
310 Self {
311 id: id.into(),
312 parent_id: None,
313 role,
314 name: name.into(),
315 description: None,
316 value: None,
317 bounds: None,
318 state: None,
319 extended: None,
320 actions: None,
321 labelled_by: None,
322 described_by: None,
323 text_ranges: None,
324 test_id: None,
325 framework_type: None,
326 occlusion: None,
327 p: None,
328 px: None,
329 geometry: None,
330 }
331 }
332
333 pub fn with_framework_type(mut self, framework_type: impl Into<String>) -> Self {
336 self.framework_type = Some(framework_type.into());
337 self
338 }
339
340 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
342 self.parent_id = Some(parent_id.into());
343 self
344 }
345
346 pub fn with_bounds(mut self, bounds: Rect) -> Self {
348 self.bounds = Some(bounds);
349 self
350 }
351
352 pub fn with_state(mut self, state: State) -> Self {
354 self.state = if state.is_empty() { None } else { Some(state) };
355 self
356 }
357
358 pub fn with_extended(mut self, extended: BTreeMap<String, Value>) -> Self {
360 self.extended = Some(extended);
361 self
362 }
363
364 pub fn with_actions(mut self, actions: Vec<Action>) -> Self {
366 self.actions = Some(actions);
367 self
368 }
369
370 pub fn with_test_id(mut self, test_id: impl Into<String>) -> Self {
372 self.test_id = Some(test_id.into());
373 self
374 }
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(deny_unknown_fields)]
380pub struct Cursor {
381 pub row: i64,
383 pub column: i64,
385 pub visible: bool,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub shape: Option<CursorShape>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase", deny_unknown_fields)]
395pub struct Snapshot {
396 pub v: u8,
398 pub session_id: String,
400 pub revision: i64,
402 pub columns: i64,
404 pub rows: i64,
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub cursor: Option<Cursor>,
409 pub root_ids: Vec<String>,
411 pub nodes: Vec<Node>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub coordinate_space: Option<Observation<String>>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub hit_grid: Option<Observation<PointerHitGrid>>,
419}
420
421impl Snapshot {
422 pub fn new(columns: i64, rows: i64) -> Self {
425 Self {
426 v: 1,
427 session_id: String::new(),
428 revision: 0,
429 columns,
430 rows,
431 cursor: None,
432 root_ids: Vec::new(),
433 nodes: Vec::new(),
434 coordinate_space: None,
435 hit_grid: None,
436 }
437 }
438
439 pub fn new_v2(columns: i64, rows: i64) -> Self {
441 Self {
442 v: 2,
443 coordinate_space: Some(Observation::Known {
444 value: "viewport-cells".into(),
445 evidence: "adapter".into(),
446 }),
447 hit_grid: Some(Observation::Unsupported {
448 capability: "pointer-hit-grid".into(),
449 reason: "framework-unobservable".into(),
450 }),
451 ..Self::new(columns, rows)
452 }
453 }
454
455 pub fn push(&mut self, node: Node) {
457 if node.parent_id.is_none() {
458 self.root_ids.push(node.id.clone());
459 }
460 self.nodes.push(node);
461 }
462}