use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::roles::{Action, Role};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rect {
pub row: i64,
pub column: i64,
pub width: i64,
pub height: i64,
}
impl Rect {
pub fn new(row: i64, column: i64, width: i64, height: i64) -> Self {
Self {
row,
column,
width,
height,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
pub enum Observation<T> {
Known {
value: T,
evidence: String,
},
Absent {
reason: String,
},
Unknown {
reason: String,
},
Unsupported {
capability: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NodeGeometryObservations {
pub displayed: Observation<bool>,
pub intended_rect: Observation<Rect>,
pub visible_rect: Observation<Rect>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PointerHitRegion {
pub rect: Rect,
pub recipient_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PointerHitGrid {
pub regions: Vec<PointerHitRegion>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Checked {
Flag(bool),
Mixed(MixedState),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MixedState {
#[serde(rename = "mixed")]
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Orientation {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CursorShape {
Block,
Underline,
Bar,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct State {
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub focused: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checked: Option<Checked>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expanded: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modal: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub busy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hidden: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offscreen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub readonly: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multiline: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orientation: Option<Orientation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_in_set: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub set_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scroll_offset: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scroll_extent: Option<i64>,
}
impl State {
pub fn is_empty(&self) -> bool {
*self == State::default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct TextRange {
pub start_offset: i64,
pub end_offset: i64,
pub rect: Rect,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Node {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
pub role: Role,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounds: Option<Rect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<State>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extended: Option<BTreeMap<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<Vec<Action>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labelled_by: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub described_by: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_ranges: Option<Vec<TextRange>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub occlusion: Option<Occlusion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub p: Option<Provenance>,
#[serde(skip_serializing_if = "Option::is_none")]
pub px: Option<BTreeMap<String, Provenance>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geometry: Option<NodeGeometryObservations>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Occlusion {
Known,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Provenance {
Annotation,
Recognizer,
Framework,
Correlation,
Heuristic,
}
impl Node {
pub fn new(id: impl Into<String>, role: Role, name: impl Into<String>) -> Self {
Self {
id: id.into(),
parent_id: None,
role,
name: name.into(),
description: None,
value: None,
bounds: None,
state: None,
extended: None,
actions: None,
labelled_by: None,
described_by: None,
text_ranges: None,
test_id: None,
framework_type: None,
occlusion: None,
p: None,
px: None,
geometry: None,
}
}
pub fn with_framework_type(mut self, framework_type: impl Into<String>) -> Self {
self.framework_type = Some(framework_type.into());
self
}
pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
self.parent_id = Some(parent_id.into());
self
}
pub fn with_bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
pub fn with_state(mut self, state: State) -> Self {
self.state = if state.is_empty() { None } else { Some(state) };
self
}
pub fn with_extended(mut self, extended: BTreeMap<String, Value>) -> Self {
self.extended = Some(extended);
self
}
pub fn with_actions(mut self, actions: Vec<Action>) -> Self {
self.actions = Some(actions);
self
}
pub fn with_test_id(mut self, test_id: impl Into<String>) -> Self {
self.test_id = Some(test_id.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Cursor {
pub row: i64,
pub column: i64,
pub visible: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub shape: Option<CursorShape>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Snapshot {
pub v: u8,
pub session_id: String,
pub revision: i64,
pub columns: i64,
pub rows: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<Cursor>,
pub root_ids: Vec<String>,
pub nodes: Vec<Node>,
#[serde(skip_serializing_if = "Option::is_none")]
pub coordinate_space: Option<Observation<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hit_grid: Option<Observation<PointerHitGrid>>,
}
impl Snapshot {
pub fn new(columns: i64, rows: i64) -> Self {
Self {
v: 1,
session_id: String::new(),
revision: 0,
columns,
rows,
cursor: None,
root_ids: Vec::new(),
nodes: Vec::new(),
coordinate_space: None,
hit_grid: None,
}
}
pub fn new_v2(columns: i64, rows: i64) -> Self {
Self {
v: 2,
coordinate_space: Some(Observation::Known {
value: "viewport-cells".into(),
evidence: "adapter".into(),
}),
hit_grid: Some(Observation::Unsupported {
capability: "pointer-hit-grid".into(),
reason: "framework-unobservable".into(),
}),
..Self::new(columns, rows)
}
}
pub fn push(&mut self, node: Node) {
if node.parent_id.is_none() {
self.root_ids.push(node.id.clone());
}
self.nodes.push(node);
}
}