use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::element::{ElementData, Rect, StateSet, Toggled};
use crate::error::{Error, Result};
use crate::event_provider::Subscription;
use crate::provider::Provider;
use crate::role::Role;
use crate::shell::ShellSurfaceKind;
pub const MOCK_SHELL_PID: u32 = 4242;
const FIRST_SHELL_NODE: usize = 13;
const SHELL_SURFACES: [(ShellSurfaceKind, usize); 2] = [
(ShellSurfaceKind::Taskbar, FIRST_SHELL_NODE),
(ShellSurfaceKind::Desktop, FIRST_SHELL_NODE + 3),
];
type MockElementSpec<'a> = (
Role,
Option<&'a str>, Option<&'a str>, Option<&'a str>, Option<Rect>,
Vec<&'a str>, StateSet,
Option<f64>, Option<f64>, Option<f64>, Option<&'a str>, Option<HashMap<String, serde_json::Value>>, );
pub type ActionLogEntry = (u64, String, Option<String>);
pub struct MockProvider {
nodes: Mutex<Vec<MockNode>>,
actions: Mutex<Vec<ActionLogEntry>>,
}
impl MockProvider {
pub fn actions(&self) -> Vec<ActionLogEntry> {
self.actions
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn clear_actions(&self) {
self.actions
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
fn record(&self, el: &ElementData, action: &str, data: Option<String>) -> Result<()> {
{
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get(el.handle as usize) {
if node.closed {
return Err(Error::Platform {
code: -1,
message: format!(
"element handle {} belongs to a closed window; cannot act on it",
el.handle
),
});
}
}
}
self.actions
.lock()
.unwrap_or_else(|e| e.into_inner())
.push((el.handle, action.to_string(), data));
Ok(())
}
fn live_window(&self, el: &ElementData) -> Result<()> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
let node = nodes
.get(el.handle as usize)
.ok_or_else(|| Error::Platform {
code: -1,
message: format!("stale window handle: no live node {}", el.handle),
})?;
if node.closed {
return Err(Error::Platform {
code: -1,
message: format!(
"window handle {} belongs to a closed window; cannot act on it",
el.handle
),
});
}
Ok(())
}
}
#[derive(Clone)]
struct MockNode {
data: ElementData,
children: Vec<usize>,
parent: Option<usize>,
closed: bool,
}
impl Provider for MockProvider {
fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
match element {
None => {
if nodes.is_empty() {
return Ok(vec![]);
}
Ok(vec![nodes[0].data.clone()])
}
Some(el) => {
let idx = el.handle as usize;
if idx >= nodes.len() || nodes[idx].closed {
return Ok(vec![]);
}
Ok(nodes[idx]
.children
.iter()
.filter(|&&i| !nodes[i].closed)
.map(|&i| nodes[i].data.clone())
.collect())
}
}
}
fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
let idx = element.handle as usize;
if idx >= nodes.len() || nodes[idx].closed {
return Ok(None);
}
Ok(nodes[idx].parent.map(|i| nodes[i].data.clone()))
}
fn list_apps(&self) -> Result<Vec<ElementData>> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
Ok(nodes
.iter()
.filter(|node| node.data.role == Role::Application && !node.closed)
.map(|node| node.data.clone())
.collect())
}
fn app_roots(&self, app: &ElementData) -> Result<Vec<ElementData>> {
if app.pid.is_none() {
return Ok(vec![app.clone()]);
}
Ok(self
.list_apps()?
.into_iter()
.filter(|root| root.pid == app.pid)
.collect())
}
fn focused_app(&self) -> Result<ElementData> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if nodes.is_empty() {
return Err(Error::selector_not_matched("focused application"));
}
Ok(nodes[0].data.clone())
}
fn list_shell_surfaces(&self) -> Result<Vec<(ShellSurfaceKind, ElementData)>> {
let nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
Ok(SHELL_SURFACES
.iter()
.map(|(kind, idx)| (*kind, nodes[*idx].data.clone()))
.collect())
}
fn press(&self, el: &ElementData) -> Result<()> {
self.record(el, "press", None)
}
fn focus(&self, el: &ElementData) -> Result<()> {
self.record(el, "focus", None)
}
fn blur(&self, el: &ElementData) -> Result<()> {
self.record(el, "blur", None)
}
fn toggle(&self, el: &ElementData) -> Result<()> {
self.record(el, "toggle", None)
}
fn select(&self, el: &ElementData) -> Result<()> {
self.record(el, "select", None)
}
fn expand(&self, el: &ElementData) -> Result<()> {
self.record(el, "expand", None)
}
fn collapse(&self, el: &ElementData) -> Result<()> {
self.record(el, "collapse", None)
}
fn show_menu(&self, el: &ElementData) -> Result<()> {
self.record(el, "show_menu", None)
}
fn increment(&self, el: &ElementData) -> Result<()> {
self.record(el, "increment", None)
}
fn decrement(&self, el: &ElementData) -> Result<()> {
self.record(el, "decrement", None)
}
fn scroll_into_view(&self, el: &ElementData) -> Result<()> {
self.record(el, "scroll_into_view", None)
}
fn set_value(&self, el: &ElementData, value: &str) -> Result<()> {
self.record(el, "set_value", Some(value.to_string()))
}
fn set_numeric_value(&self, el: &ElementData, v: f64) -> Result<()> {
self.record(el, "set_numeric_value", Some(format!("{v}")))
}
fn type_text(&self, el: &ElementData, text: &str) -> Result<()> {
self.record(el, "type_text", Some(text.to_string()))
}
fn set_text_selection(&self, el: &ElementData, start: u32, end: u32) -> Result<()> {
self.record(el, "set_text_selection", Some(format!("{start}..{end}")))
}
fn perform_action(&self, el: &ElementData, action: &str) -> Result<()> {
match action {
"activate" => self.activate(el),
"minimize" => self.minimize(el),
"maximize" => self.maximize(el),
"enter_fullscreen" => self.enter_fullscreen(el),
"restore" => self.restore(el),
"close" => self.close(el),
"move_to" => Err(Error::InvalidActionData {
message: "perform_action(\"move_to\") requires coordinates; call move_to(x, y)"
.to_string(),
}),
"resize_to" => Err(Error::InvalidActionData {
message: "perform_action(\"resize_to\") requires dimensions; call \
resize_to(width, height)"
.to_string(),
}),
_ => self.record(el, action, None),
}
}
fn activate(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
self.record(el, "activate", None)
}
fn minimize(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
node.data.states.minimized = Some(true);
node.data.states.maximized = Some(false);
node.data.states.fullscreen = Some(false);
node.data.states.visible = false;
}
}
self.record(el, "minimize", None)
}
fn maximize(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
node.data.states.maximized = Some(true);
node.data.states.minimized = Some(false);
node.data.states.visible = true;
}
}
self.record(el, "maximize", None)
}
fn enter_fullscreen(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
node.data.states.fullscreen = Some(true);
node.data.states.minimized = Some(false);
node.data.states.maximized = Some(false);
node.data.states.visible = true;
}
}
self.record(el, "enter_fullscreen", None)
}
fn restore(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
node.data.states.minimized = Some(false);
node.data.states.maximized = Some(false);
node.data.states.fullscreen = Some(false);
node.data.states.visible = true;
}
}
self.record(el, "restore", None)
}
fn close(&self, el: &ElementData) -> Result<()> {
self.live_window(el)?;
self.record(el, "close", None)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
node.closed = true;
node.data.states.visible = false;
let mut stack: Vec<usize> = node.children.clone();
while let Some(idx) = stack.pop() {
if let Some(child) = nodes.get_mut(idx) {
child.closed = true;
stack.extend(child.children.iter().copied());
}
}
}
}
Ok(())
}
fn move_to(&self, el: &ElementData, x: i32, y: i32) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
if let Some(b) = node.data.bounds.as_mut() {
b.x = x;
b.y = y;
}
}
}
self.record(el, "move_to", Some(format!("{x},{y}")))
}
fn resize_to(&self, el: &ElementData, width: u32, height: u32) -> Result<()> {
self.live_window(el)?;
{
let mut nodes = self.nodes.lock().unwrap_or_else(|e| e.into_inner());
if let Some(node) = nodes.get_mut(el.handle as usize) {
if let Some(b) = node.data.bounds.as_mut() {
b.width = width;
b.height = height;
}
}
}
self.record(el, "resize_to", Some(format!("{width}x{height}")))
}
fn subscribe(&self, _el: &ElementData) -> Result<Subscription> {
Err(Error::Platform {
code: -1,
message: "MockProvider does not support subscribe".to_string(),
})
}
}
pub fn build_provider() -> Arc<MockProvider> {
use serde_json::json;
let elements: Vec<MockElementSpec> = vec![
(
Role::Application,
Some("TestApp"),
None,
Some("Test application"),
Some(Rect {
x: 0,
y: 0,
width: 1920,
height: 1080,
}),
vec![],
StateSet::default(),
None,
None,
None,
Some("app-root"),
Some(HashMap::from([(
"ax_role".to_string(),
json!("AXApplication"),
)])),
),
(
Role::Window,
Some("Main Window"),
None,
None,
Some(Rect {
x: 100,
y: 50,
width: 800,
height: 600,
}),
vec![
"activate",
"minimize",
"maximize",
"enter_fullscreen",
"restore",
"close",
"move_to",
"resize_to",
],
StateSet {
focused: true,
active: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::Toolbar,
Some("Navigation"),
None,
None,
None,
vec![],
StateSet::default(),
None,
None,
None,
None,
None,
),
(
Role::Button,
Some("Back"),
None,
Some("Go back"),
Some(Rect {
x: 110,
y: 60,
width: 50,
height: 30,
}),
vec!["press", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
None,
None,
None,
Some("btn-back"),
None,
),
(
Role::Button,
Some("Forward"),
None,
None,
Some(Rect {
x: 170,
y: 60,
width: 50,
height: 30,
}),
vec!["press", "focus"],
StateSet {
enabled: false,
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::Group,
Some("Content"),
None,
None,
None,
vec![],
StateSet::default(),
None,
None,
None,
None,
None,
),
(
Role::TextField,
Some("Search"),
Some("hello"),
Some("Search field"),
Some(Rect {
x: 200,
y: 120,
width: 300,
height: 25,
}),
vec!["focus", "set_value", "type_text"],
StateSet {
editable: true,
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::CheckBox,
Some("Agree"),
None,
None,
None,
vec!["press", "focus"],
StateSet {
checked: Some(Toggled::On),
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::Slider,
Some("Volume"),
Some("75"),
None,
None,
vec!["increment", "decrement", "set_value", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
Some(75.0),
Some(0.0),
Some(100.0),
None,
None,
),
(
Role::StaticText,
Some("Status"),
Some("Loading..."),
None,
None,
vec![],
StateSet {
visible: false,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::List,
Some("Items"),
None,
None,
None,
vec![],
StateSet {
expanded: Some(true),
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::ListItem,
Some("Item 1"),
None,
None,
None,
vec!["select", "focus"],
StateSet {
selected: true,
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::ListItem,
Some("Item 2"),
None,
None,
None,
vec!["select", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
(
Role::Toolbar,
Some("Taskbar"),
None,
None,
Some(Rect {
x: 0,
y: 1040,
width: 1920,
height: 40,
}),
vec![],
StateSet::default(),
None,
None,
None,
Some("Shell_TrayWnd"),
None,
),
(
Role::Button,
Some("Show Hidden Icons"),
None,
Some("Open the tray overflow"),
Some(Rect {
x: 1700,
y: 1045,
width: 30,
height: 30,
}),
vec!["press", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
None,
None,
None,
Some("systray-chevron"),
None,
),
(
Role::Button,
Some("Volume"),
None,
None,
Some(Rect {
x: 1740,
y: 1045,
width: 30,
height: 30,
}),
vec!["press", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
None,
None,
None,
Some("SystemTrayIcon"),
None,
),
(
Role::List,
Some("Desktop"),
None,
None,
None,
vec![],
StateSet::default(),
None,
None,
None,
Some("Progman"),
None,
),
(
Role::ListItem,
Some("Trash"),
None,
None,
None,
vec!["select", "focus"],
StateSet {
focusable: true,
..StateSet::default()
},
None,
None,
None,
None,
None,
),
];
let children_map: Vec<Vec<usize>> = vec![
vec![1], vec![2, 5], vec![3, 4], vec![], vec![], vec![6, 7, 8, 9, 10], vec![], vec![], vec![], vec![], vec![11, 12], vec![], vec![], vec![14, 15], vec![], vec![], vec![17], vec![], ];
let parent_map: Vec<Option<usize>> = vec![
None,
Some(0),
Some(1),
Some(2),
Some(2),
Some(1),
Some(5),
Some(5),
Some(5),
Some(5),
Some(5),
Some(10),
Some(10),
None,
Some(13),
Some(13),
None,
Some(16),
];
let mut nodes = Vec::with_capacity(elements.len());
for (i, (role, name, value, desc, bounds, actions, states, nv, minv, maxv, sid, raw)) in
elements.into_iter().enumerate()
{
let pid = if i >= FIRST_SHELL_NODE {
MOCK_SHELL_PID
} else {
1234
};
let data = ElementData {
role,
name: name.map(String::from),
value: value.map(String::from),
description: desc.map(String::from),
bounds,
actions: actions.iter().map(|s| s.to_string()).collect(),
states,
numeric_value: nv,
min_value: minv,
max_value: maxv,
stable_id: sid.map(String::from),
pid: Some(pid),
raw: raw.unwrap_or_default(),
handle: i as u64,
};
nodes.push(MockNode {
data,
children: children_map[i].clone(),
parent: parent_map[i],
closed: false,
});
}
Arc::new(MockProvider {
nodes: Mutex::new(nodes),
actions: Mutex::new(Vec::new()),
})
}
pub fn build_split_provider() -> Arc<MockProvider> {
let provider = build_provider();
{
let mut nodes = provider.nodes.lock().unwrap_or_else(|e| e.into_inner());
let root_index = nodes.len();
let window_index = root_index + 1;
let mut root = nodes[0].clone();
root.data.handle = root_index as u64;
root.data.stable_id = Some("second-app-root".into());
root.children = vec![window_index];
let mut window = nodes[1].clone();
window.data.handle = window_index as u64;
window.data.stable_id = Some("second-window".into());
window.data.name = Some("Second Window".into());
window.children.clear();
window.parent = Some(root_index);
nodes.extend([root, window]);
}
provider
}
pub fn disconnected_subscription() -> Subscription {
use crate::event_provider::{CancelHandle, EventReceiver};
let (tx, rx) = std::sync::mpsc::channel::<crate::event::Event>();
drop(tx); Subscription::new(EventReceiver::new(rx), CancelHandle::noop())
}