use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, RwLock};
use bevy::prelude::*;
use objc2_core_foundation::CGPoint;
use objc2_core_graphics::CGDirectDisplayID;
use stdext::prelude::RwLockExt;
use crate::errors::Error;
use crate::events::Event;
use crate::manager::app::MockApplicationApi;
use crate::manager::{
Application, Display, MockProcessApi, MockWindowApi, MockWindowManagerApi, Origin, Size,
Window, origin_to,
};
use crate::platform::{Modifiers, Pid, ProcessSerialNumber, WinID, WorkspaceId};
use super::*;
pub(crate) struct MockAppData {
pub(crate) psn: ProcessSerialNumber,
pub(crate) bundle_id: String,
pub(crate) name: String,
pub(crate) focused_window_id: Option<WinID>,
pub(crate) is_frontmost: bool,
pub(crate) connection: Option<crate::platform::ConnID>,
}
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct MockWindowData {
pub(crate) id: WinID,
pub(crate) pid: Pid,
pub(crate) frame: IRect,
pub(crate) title: String,
pub(crate) minimized: bool,
pub(crate) workspace_id: WorkspaceId,
pub(crate) visible: bool,
pub(crate) role: String,
pub(crate) subrole: String,
pub(crate) identifier: String,
pub(crate) is_full_screen: bool,
pub(crate) border_radius: Option<f64>,
pub(crate) horizontal_padding: i32,
pub(crate) vertical_padding: i32,
pub(crate) child_role: bool,
}
impl Default for MockWindowData {
fn default() -> Self {
Self {
id: 0,
pid: 0,
frame: IRect::default(),
title: String::new(),
minimized: false,
workspace_id: 0,
visible: true,
role: "AXWindow".to_string(),
subrole: "AXStandardWindow".to_string(),
identifier: "testid".to_string(),
is_full_screen: false,
border_radius: None,
horizontal_padding: 0,
vertical_padding: 0,
child_role: false,
}
}
}
struct MockDisplayData {
id: u32,
bounds: IRect,
workspaces: Vec<WorkspaceId>,
}
struct MockStateInner {
apps: HashMap<Pid, MockAppData>,
windows: HashMap<WinID, MockWindowData>,
displays: HashMap<u32, MockDisplayData>,
fullscreen_spaces: HashSet<WorkspaceId>,
active_display_id: u32,
cursor_position: Origin,
event_queue: VecDeque<Event>,
}
#[derive(Clone)]
pub struct MockState {
inner: Arc<RwLock<MockStateInner>>,
}
impl MockState {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(MockStateInner {
apps: HashMap::new(),
windows: HashMap::new(),
displays: HashMap::new(),
fullscreen_spaces: HashSet::new(),
active_display_id: 0,
cursor_position: Origin::ZERO,
event_queue: VecDeque::new(),
})),
}
}
pub(crate) fn window_visible(&self, window_id: WinID, visible: bool) {
let mut state = self.inner.force_write();
let window = state.windows.get_mut(&window_id).expect("finding window");
window.visible = visible;
}
pub fn spawn_app(&self, pid: Pid, bundle_id: &str, name: &str) {
let mut inner = self.inner.force_write();
inner.apps.insert(
pid,
MockAppData {
psn: ProcessSerialNumber {
high: 0,
low: pid.cast_unsigned(),
},
bundle_id: bundle_id.to_string(),
name: name.to_string(),
focused_window_id: None,
is_frontmost: true,
connection: Some(0),
},
);
}
pub fn spawn_window(
&self,
pid: Pid,
workspace_id: WorkspaceId,
id: WinID,
frame: IRect,
) -> Window {
let mut inner = self.inner.force_write();
inner.windows.insert(
id,
MockWindowData {
id,
pid,
frame,
title: format!("Window {id}"),
workspace_id,
..default()
},
);
self.create_window(id)
}
pub fn focus_window(&self, id: WinID) {
let mut inner = self.inner.force_write();
if let Some(win) = inner.windows.get(&id) {
let pid = win.pid;
if let Some(app) = inner.apps.get_mut(&pid) {
app.focused_window_id = Some(id);
let psn = app.psn;
inner
.event_queue
.push_back(Event::ApplicationFrontSwitched { psn });
inner
.event_queue
.push_back(Event::WindowFocused { window_id: id });
}
}
}
pub fn add_display(&mut self, id: u32, bounds: IRect, workspaces: Vec<WorkspaceId>) {
let mut inner = self.inner.force_write();
if inner.displays.is_empty() {
inner.active_display_id = id;
}
inner.displays.insert(
id,
MockDisplayData {
id,
bounds,
workspaces,
},
);
}
#[allow(unused)]
pub fn remove_display(&self, id: u32) {
let mut inner = self.inner.force_write();
inner.displays.remove(&id);
if inner.active_display_id == id {
inner.active_display_id = inner.displays.keys().copied().next().unwrap_or(0);
}
}
pub fn active_display(&self) -> CGDirectDisplayID {
self.inner.force_read().active_display_id
}
pub(crate) fn activate_workspace(
&self,
display_id: u32,
workspace_id: WorkspaceId,
fullscreen: bool,
) {
let mut inner = self.inner.force_write();
{
let display = inner
.displays
.get_mut(&display_id)
.expect("finding display");
display.workspaces.retain(|id| *id != workspace_id);
display.workspaces.insert(0, workspace_id);
}
if fullscreen {
inner.fullscreen_spaces.insert(workspace_id);
} else {
inner.fullscreen_spaces.remove(&workspace_id);
}
}
pub fn drain_events(&self) -> Vec<Event> {
let mut inner = self.inner.force_write();
inner.event_queue.drain(..).collect()
}
pub fn update_window<F>(&self, id: WinID, f: F)
where
F: FnOnce(&mut MockWindowData),
{
let mut inner = self.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
f(w);
}
}
#[allow(unused)]
pub fn update_app(&self, pid: Pid, f: impl FnOnce(&mut MockAppData)) {
let mut inner = self.inner.force_write();
if let Some(a) = inner.apps.get_mut(&pid) {
f(a);
}
}
#[allow(unused)]
pub fn os_move_window(&self, id: WinID, origin: Origin) {
let mut inner = self.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
let size = w.frame.size();
w.frame.min = origin;
w.frame.max = origin + size;
inner
.event_queue
.push_back(Event::WindowMoved { window_id: id });
}
}
#[allow(unused)]
pub fn os_resize_window(&self, id: WinID, size: Size) {
let mut inner = self.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
w.frame.max = w.frame.min + size;
inner
.event_queue
.push_back(Event::WindowResized { window_id: id });
}
}
#[allow(unused)]
pub fn os_minimize_window(&self, id: WinID, minimized: bool) {
let mut inner = self.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
w.minimized = minimized;
if minimized {
inner
.event_queue
.push_back(Event::WindowMinimized { window_id: id });
} else {
inner
.event_queue
.push_back(Event::WindowDeminimized { window_id: id });
}
}
}
#[allow(unused)]
pub fn simulate_click(&self, point: Origin) {
let mut inner = self.inner.force_write();
let point = CGPoint::new(point.x.into(), point.y.into());
inner.event_queue.push_back(Event::MouseDown {
point,
modifiers: Modifiers::empty(),
});
inner.event_queue.push_back(Event::MouseUp {
point,
modifiers: Modifiers::empty(),
});
}
#[allow(unused)]
pub fn simulate_window_click(&self, id: WinID) {
let inner = self.inner.force_read();
if let Some(w) = inner.windows.get(&id) {
let center = w.frame.center();
drop(inner);
self.simulate_click(center);
}
}
#[allow(unused)]
pub fn simulate_drag(&self, start: Origin, end: Origin) {
let mut inner = self.inner.force_write();
let start_p = CGPoint::new(start.x.into(), start.y.into());
let end_p = CGPoint::new(end.x.into(), end.y.into());
inner.event_queue.push_back(Event::MouseDown {
point: start_p,
modifiers: Modifiers::empty(),
});
inner.event_queue.push_back(Event::MouseDragged {
point: end_p,
modifiers: Modifiers::empty(),
});
inner.event_queue.push_back(Event::MouseUp {
point: end_p,
modifiers: Modifiers::empty(),
});
}
pub fn cursor_position(&self) -> IVec2 {
self.inner.force_read().cursor_position
}
#[allow(clippy::too_many_lines)]
pub fn create_window(&self, id: WinID) -> Window {
let mut mw = MockWindowApi::new();
mw.expect_id().return_const(id);
let s = self.clone();
mw.expect_pid().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.pid)
.ok_or(Error::InvalidWindow)
});
let s = self.clone();
mw.expect_frame().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.frame)
.unwrap_or_default()
});
let s = self.clone();
mw.expect_resize().returning(move |size| {
let mut inner = s.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
w.frame.max = w.frame.min + size;
}
});
let s_move = self.clone();
mw.expect_reposition().returning(move |origin| {
let mut inner = s_move.inner.force_write();
if let Some(w) = inner.windows.get_mut(&id) {
let size = w.frame.size();
w.frame.min = origin;
w.frame.max = origin + size;
}
});
let s = self.clone();
mw.expect_focus_with_raise().returning(move |_psn| {
s.focus_window(id);
});
let s = self.clone();
mw.expect_title().returning(move || {
Ok(s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.title.clone())
.unwrap_or_default())
});
let s = self.clone();
mw.expect_is_minimized().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.is_some_and(|w| w.minimized)
});
let s = self.clone();
mw.expect_update_frame().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.frame)
.ok_or(Error::InvalidWindow)
});
let s = self.clone();
mw.expect_identifier().returning(move || {
Ok(s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.identifier.clone())
.unwrap_or_default())
});
let s = self.clone();
mw.expect_role().returning(move || {
Ok(s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.role.clone())
.unwrap_or_default())
});
let s = self.clone();
mw.expect_subrole().returning(move || {
Ok(s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.subrole.clone())
.unwrap_or_default())
});
let s = self.clone();
mw.expect_child_role().returning(move || {
Ok(s.inner
.force_read()
.windows
.get(&id)
.is_some_and(|w| w.child_role))
});
let s = self.clone();
mw.expect_horizontal_padding().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.horizontal_padding)
.unwrap_or_default()
});
let s = self.clone();
mw.expect_vertical_padding().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.map(|w| w.vertical_padding)
.unwrap_or_default()
});
let s = self.clone();
mw.expect_is_full_screen().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.is_some_and(|w| w.is_full_screen)
});
let s = self.clone();
mw.expect_border_radius().returning(move || {
s.inner
.force_read()
.windows
.get(&id)
.and_then(|w| w.border_radius)
});
mw.expect_element().return_const(None);
mw.expect_raise_without_focus().return_const(());
mw.expect_focus_without_raise().return_const(());
mw.expect_set_padding().return_const(());
Window::new(Box::new(mw))
}
pub fn create_application(&self, pid: Pid) -> Application {
let mut ma = MockApplicationApi::new();
let s = self.clone();
ma.expect_pid().return_const(pid);
ma.expect_psn()
.returning(move || s.inner.force_read().apps.get(&pid).map(|a| a.psn).unwrap());
let s = self.clone();
ma.expect_focused_window_id().returning(move || {
s.inner
.force_read()
.apps
.get(&pid)
.and_then(|a| a.focused_window_id)
.ok_or(Error::InvalidWindow)
});
let s = self.clone();
ma.expect_bundle_id().returning(move || {
s.inner
.force_read()
.apps
.get(&pid)
.map(|a| a.bundle_id.clone())
});
let name = self
.inner
.force_read()
.apps
.get(&pid)
.map(|a| a.name.clone())
.unwrap();
ma.expect_name().return_const(name);
let s = self.clone();
ma.expect_is_frontmost().returning(move || {
s.inner
.force_read()
.apps
.get(&pid)
.is_some_and(|a| a.is_frontmost)
});
let s = self.clone();
ma.expect_connection().returning(move || {
s.inner
.force_read()
.apps
.get(&pid)
.and_then(|a| a.connection)
});
ma.expect_observe().returning(|| Ok(true));
ma.expect_observe_window().returning(|_| Ok(true));
ma.expect_unobserve_window().return_const(());
ma.expect_window_list().returning(|_| Vec::new());
Application::new(Box::new(ma))
}
pub fn create_window_manager(&self) -> MockWindowManagerApi {
let mut wm = MockWindowManagerApi::new();
let s = self.clone();
wm.expect_active_display_id()
.returning(move || Ok(s.inner.force_read().active_display_id));
let s = self.clone();
wm.expect_active_display_space().returning(move |id| {
s.inner
.force_read()
.displays
.get(&id)
.map(|d| d.workspaces[0])
.ok_or(Error::InvalidWindow)
});
let s = self.clone();
wm.expect_is_fullscreen_space()
.returning(move |display_id| {
let inner = s.inner.force_read();
inner
.displays
.get(&display_id)
.and_then(|display| display.workspaces.first())
.is_some_and(|workspace_id| inner.fullscreen_spaces.contains(workspace_id))
});
let s = self.clone();
wm.expect_present_displays().returning(move || {
s.inner
.force_read()
.displays
.values()
.map(|d| {
(
Display::new(d.id, d.bounds, TEST_MENUBAR_HEIGHT),
d.workspaces.clone(),
)
})
.collect()
});
let s = self.clone();
wm.expect_find_existing_application_windows()
.returning(move |app, spaces, _config| {
let pid = app.pid();
let mut windows = s
.inner
.force_read()
.windows
.values()
.filter_map(|w| {
(w.pid == pid && spaces.contains(&w.workspace_id))
.then_some(s.create_window(w.id))
})
.collect::<Vec<_>>();
windows.sort_unstable_by_key(|window| window.id());
Ok((windows, vec![]))
});
let s = self.clone();
wm.expect_windows_in_workspace()
.returning(move |workspace_id| {
let mut windows = s
.inner
.force_read()
.windows
.values()
.filter_map(|w| (w.workspace_id == workspace_id).then_some(w.id))
.collect::<Vec<_>>();
windows.sort_unstable();
Ok(windows)
});
let s = self.clone();
wm.expect_windows_on_screen().returning(move || {
let windows = s
.inner
.force_read()
.windows
.iter()
.filter_map(|(id, window)| window.visible.then_some(id))
.copied()
.collect::<Vec<_>>();
Some(windows)
});
let s = self.clone();
wm.expect_warp_mouse()
.returning(move |origin| s.inner.force_write().cursor_position = origin);
let s = self.clone();
wm.expect_cursor_position()
.returning(move || Some(origin_to(s.inner.force_read().cursor_position)));
wm.expect_get_associated_windows().return_const(vec![]);
wm.expect_find_window_at_point().return_const(Ok(0));
wm
}
pub fn create_process(&self, pid: Pid) -> MockProcessApi {
let mut mp = MockProcessApi::new();
let s = self.clone();
let name = self
.inner
.force_read()
.apps
.get(&pid)
.map(|a| a.name.clone())
.unwrap();
mp.expect_name().return_const(name);
mp.expect_pid().return_const(pid);
mp.expect_psn()
.returning(move || s.inner.force_read().apps.get(&pid).map(|a| a.psn).unwrap());
mp.expect_is_observable().returning(|| true);
mp.expect_application().return_const(None);
mp.expect_ready().return_const(true);
mp.expect_force_manage().return_const(());
mp
}
}