#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![doc(html_root_url = "https://docs.smix.dev/smix-screen")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
pub type Bounds = Rect;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElementSummary {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
pub bounds: Rect,
pub enabled: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScreenDescription {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub screenshot: Option<String>,
pub elements: Vec<ElementSummary>,
pub front_app: String,
pub summary: String,
pub captured_at: f64,
}
#[must_use]
pub fn collect_visible_summaries(tree: &A11yNode, limit: usize) -> Vec<ElementSummary> {
let mut out: Vec<ElementSummary> = Vec::new();
fn walk(n: &A11yNode, limit: usize, out: &mut Vec<ElementSummary>) {
if out.len() >= limit {
return;
}
if n.enabled && n.visible {
out.push(summarize_node(n));
}
for c in &n.children {
if out.len() >= limit {
return;
}
walk(c, limit, out);
}
}
walk(tree, limit, &mut out);
out
}
pub const DEFAULT_VISIBLE_LIMIT: usize = 1000;
#[must_use]
pub fn summarize_node(node: &A11yNode) -> ElementSummary {
let name = node
.label
.clone()
.or_else(|| node.title.clone())
.or_else(|| node.text.clone())
.or_else(|| node.value.clone())
.or_else(|| node.placeholder_value.clone());
let text = match (&node.text, &name) {
(Some(t), Some(n)) if t != n => Some(t.clone()),
_ => None,
};
ElementSummary {
role: node.role,
name,
id: node.identifier.clone(),
text,
bounds: node.bounds,
enabled: node.enabled,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Role {
Button,
Link,
TextField,
SecureTextField,
SearchField,
Switch,
Toggle,
CheckBox,
Radio,
Image,
StaticText,
Tab,
TabBar,
NavigationBar,
Cell,
Alert,
Dialog,
Slider,
ProgressBar,
Picker,
Menu,
MenuItem,
ScrollView,
SegmentedControl,
Table,
CollectionView,
WebView,
Keyboard,
}
impl Role {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Role::Button => "button",
Role::Link => "link",
Role::TextField => "textField",
Role::SecureTextField => "secureTextField",
Role::SearchField => "searchField",
Role::Switch => "switch",
Role::Toggle => "toggle",
Role::CheckBox => "checkBox",
Role::Radio => "radio",
Role::Image => "image",
Role::StaticText => "staticText",
Role::Tab => "tab",
Role::TabBar => "tabBar",
Role::NavigationBar => "navigationBar",
Role::Cell => "cell",
Role::Alert => "alert",
Role::Dialog => "dialog",
Role::Slider => "slider",
Role::ProgressBar => "progressBar",
Role::Picker => "picker",
Role::Menu => "menu",
Role::MenuItem => "menuItem",
Role::ScrollView => "scrollView",
Role::SegmentedControl => "segmentedControl",
Role::Table => "table",
Role::CollectionView => "collectionView",
Role::WebView => "webView",
Role::Keyboard => "keyboard",
}
}
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[must_use]
pub fn role_from_raw_type(raw_type: &str) -> Option<Role> {
Some(match raw_type {
"button" => Role::Button,
"link" => Role::Link,
"textField" => Role::TextField,
"secureTextField" => Role::SecureTextField,
"searchField" => Role::SearchField,
"switch" => Role::Switch,
"toggle" => Role::Toggle,
"checkBox" => Role::CheckBox,
"radioButton" => Role::Radio,
"image" => Role::Image,
"staticText" => Role::StaticText,
"tabBar" => Role::TabBar,
"navigationBar" => Role::NavigationBar,
"cell" => Role::Cell,
"alert" => Role::Alert,
"dialog" => Role::Dialog,
"slider" => Role::Slider,
"progressIndicator" => Role::ProgressBar,
"picker" => Role::Picker,
"menu" => Role::Menu,
"menuItem" => Role::MenuItem,
"scrollView" => Role::ScrollView,
"segmentedControl" => Role::SegmentedControl,
"table" => Role::Table,
"collectionView" => Role::CollectionView,
"webView" => Role::WebView,
"keyboard" => Role::Keyboard,
_ => return None,
})
}
pub fn derive_roles_recursive(node: &mut A11yNode) {
derive_roles_inner(node, false);
}
fn derive_roles_inner(node: &mut A11yNode, inside_tab_bar: bool) {
if node.role.is_none() {
node.role = if inside_tab_bar && node.raw_type == "button" {
Some(Role::Tab)
} else {
role_from_raw_type(&node.raw_type)
};
}
let child_inside = inside_tab_bar || node.raw_type == "tabBar";
for child in &mut node.children {
derive_roles_inner(child, child_inside);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A11yNode {
pub raw_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
pub bounds: Rect,
pub enabled: bool,
pub selected: bool,
pub has_focus: bool,
pub visible: bool,
#[serde(default)]
pub children: Vec<A11yNode>,
}
#[inline]
#[must_use]
pub fn is_visible_enough(node: &A11yNode, tree: &A11yNode) -> bool {
let b = node.bounds;
if b.w <= 0.0 || b.h <= 0.0 {
return false;
}
let root = tree.bounds;
if root.w <= 0.0 || root.h <= 0.0 {
return true; }
let x1 = b.x.max(root.x);
let y1 = b.y.max(root.y);
let x2 = (b.x + b.w).min(root.x + root.w);
let y2 = (b.y + b.h).min(root.y + root.h);
x2 > x1 && y2 > y1
}
#[inline]
#[must_use]
pub fn visible_area(node: &A11yNode, tree: &A11yNode) -> f64 {
let b = node.bounds;
if b.w <= 0.0 || b.h <= 0.0 {
return 0.0;
}
let root = tree.bounds;
if root.w <= 0.0 || root.h <= 0.0 {
return b.w * b.h;
}
let x1 = b.x.max(root.x);
let y1 = b.y.max(root.y);
let x2 = (b.x + b.w).min(root.x + root.w);
let y2 = (b.y + b.h).min(root.y + root.h);
if x2 <= x1 || y2 <= y1 {
return 0.0;
}
(x2 - x1) * (y2 - y1)
}