use crate::backend::Backend;
use crate::runner::{AppLogic, Reaction};
use crate::shell::{ShellApp, ShellConfig};
use crate::testing::driver_core::DriverCore;
use crate::testing::{
Anchor, ConformanceDriver, DriverInput, FrameInventory, LogicalViewport, PixelClickConformance,
};
use crate::{ButtonMask, Key, Modifiers, MouseButton, NamedKey, Point, UiEvent};
use super::backend::MacBackend;
use super::headless::BitmapSurface;
use super::run::{dispatch_event, render_frame};
use super::text::make_font;
pub fn driver_with_shell<A: ShellApp + 'static>(
app: A,
config: ShellConfig,
width: u32,
height: u32,
) -> MacDriver<impl AppLogic> {
let adapter = crate::shell_adapter::build_shell_adapter(app, config);
MacDriver::new(adapter, width, height)
}
pub struct MacDriver<A: AppLogic> {
core: DriverCore<MacBackend, A>,
surface: BitmapSurface,
width: u32,
height: u32,
}
impl<A: AppLogic> MacDriver<A> {
pub fn new(app: A, width: u32, height: u32) -> Self {
let surface = BitmapSurface::new(width, height);
let mut backend = MacBackend::new();
backend.set_painted_text_recording(true);
if let Some(font) = make_font("Menlo", 14.0) {
backend.set_current_font(font);
}
crate::Backend::set_menu_style(&mut backend, crate::backend::MenuStyle::Custom);
backend.begin_frame(crate::Viewport::new(width as f32, height as f32, 1.0));
let mut app = app;
app.setup(&mut backend);
let mut driver = Self {
core: DriverCore::new(app, backend),
surface,
width,
height,
};
driver.render();
driver
}
pub fn render(&mut self) {
let viewport = crate::Viewport::new(self.width as f32, self.height as f32, 1.0);
let context_ptr = self.surface.context_ptr();
let (backend, app) = self.core.parts_mut();
render_frame(backend, app, viewport, context_ptr);
}
pub fn tick(&mut self) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let reaction = {
let (backend, app) = self.core.parts_mut();
app.tick(backend)
};
let viewport = crate::Viewport::new(self.width as f32, self.height as f32, 1.0);
let context_ptr = self.surface.context_ptr();
self.core.apply_outcome(reaction.into(), |backend, app| {
render_frame(backend, app, viewport, context_ptr);
})
}
pub fn dispatch(&mut self, event: UiEvent) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let outcome = {
let caret_visible = self.core.backend().caret_visible_handle();
let caret_pause = self.core.backend().caret_blink_pause_handle();
let (backend, app) = self.core.parts_mut();
dispatch_event(event, backend, app, &caret_visible, &caret_pause)
};
let viewport = crate::Viewport::new(self.width as f32, self.height as f32, 1.0);
let context_ptr = self.surface.context_ptr();
self.core.apply_outcome(outcome, |backend, app| {
render_frame(backend, app, viewport, context_ptr);
})
}
pub fn press(&mut self, key: Key) -> Reaction {
DriverInput::press(self, key)
}
pub fn type_char(&mut self, c: char) -> Reaction {
DriverInput::type_char(self, c)
}
pub fn press_named(&mut self, key: NamedKey) -> Reaction {
DriverInput::press_named(self, key)
}
pub fn ctrl_char(&mut self, c: char) -> Reaction {
DriverInput::ctrl_char(self, c)
}
pub fn click(&mut self, x: f32, y: f32) -> Reaction {
DriverInput::click(self, x, y)
}
pub fn mouse_down(&mut self, x: f32, y: f32) -> Reaction {
self.dispatch(UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
modifiers: Modifiers::default(),
})
}
pub fn mouse_move(&mut self, x: f32, y: f32) -> Reaction {
self.dispatch(UiEvent::MouseMoved {
position: Point::new(x, y),
buttons: ButtonMask {
left: true,
..ButtonMask::default()
},
})
}
pub fn mouse_up(&mut self, x: f32, y: f32) -> Reaction {
self.dispatch(UiEvent::MouseUp {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
})
}
pub fn drag(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) -> Reaction {
DriverInput::drag(self, x0, y0, x1, y1)
}
pub fn exited(&self) -> bool {
self.core.exited()
}
pub fn app(&self) -> &A {
self.core.app()
}
pub fn app_mut(&mut self) -> &mut A {
self.core.app_mut()
}
pub fn backend(&self) -> &MacBackend {
self.core.backend()
}
pub fn surface(&self) -> &BitmapSurface {
&self.surface
}
pub fn pixel(&self, x: u32, y: u32) -> (u8, u8, u8, u8) {
self.surface.pixel(x, y)
}
pub fn painted_texts(&self) -> Vec<&str> {
self.core.painted_texts()
}
pub fn screen_contains(&self, needle: &str) -> bool {
self.core.screen_contains(needle)
}
pub fn find_bounds(&self, needle: &str) -> Option<crate::Rect> {
self.core.find_bounds(needle)
}
pub fn find(&self, needle: &str) -> Option<(f32, f32)> {
self.core.find(needle)
}
}
impl<A: AppLogic> DriverInput for MacDriver<A> {
fn dispatch(&mut self, event: UiEvent) -> Reaction {
self.dispatch(event)
}
fn mouse_down(&mut self, x: f32, y: f32) -> Reaction {
self.mouse_down(x, y)
}
fn mouse_move(&mut self, x: f32, y: f32) -> Reaction {
self.mouse_move(x, y)
}
fn mouse_up(&mut self, x: f32, y: f32) -> Reaction {
self.mouse_up(x, y)
}
}
impl<A: AppLogic> PixelClickConformance for MacDriver<A> {
const NAME: &'static str = "MacDriver";
fn find_bounds(&self, needle: &str) -> Option<crate::Rect> {
self.find_bounds(needle)
}
fn find(&self, needle: &str) -> Option<(f32, f32)> {
self.find(needle)
}
fn conformance_line_height(&self) -> f32 {
crate::Backend::line_height(self.core.backend())
}
}
impl<A: AppLogic> ConformanceDriver for MacDriver<A> {
type App = A;
fn new_fixture(app: Self::App, viewport: LogicalViewport) -> Self {
const NOMINAL_CHAR_WIDTH: u32 = 8;
const NOMINAL_LINE_HEIGHT: u32 = 16;
MacDriver::new(
app,
viewport.cols * NOMINAL_CHAR_WIDTH,
viewport.rows * NOMINAL_LINE_HEIGHT,
)
}
fn backend_caps(&self) -> crate::BackendCaps {
crate::Backend::backend_caps(self.core.backend())
}
fn press_named(&mut self, key: NamedKey) {
MacDriver::press_named(self, key);
}
fn type_char(&mut self, c: char) {
MacDriver::type_char(self, c);
}
fn ctrl_char(&mut self, c: char) {
MacDriver::ctrl_char(self, c);
}
fn click_text_at(&mut self, needle: &str, at: Anchor) {
PixelClickConformance::click_text_at(self, needle, at)
}
fn drag_text(&mut self, from: &str, to: &str) {
PixelClickConformance::drag_text(self, from, to)
}
fn scroll_at(&mut self, needle: &str, lines: i32) {
PixelClickConformance::scroll_at(self, needle, lines)
}
fn inventory(&self) -> FrameInventory {
FrameInventory {
text_runs: self.core.backend().text_runs().to_vec(),
zones: self.core.backend().zones().to_vec(),
}
}
fn screen_has(&self, needle: &str) -> bool {
self.screen_contains(needle)
}
fn exited(&self) -> bool {
MacDriver::exited(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::types::{Color, WidgetId};
use crate::Rect;
const W: u32 = 120;
const H: u32 = 24;
const KNOWN_BG: Color = Color::rgb(0, 255, 0);
struct StatusBarApp;
impl AppLogic for StatusBarApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "known-pixel".to_string(),
fg: Color::rgb(255, 255, 255),
bg: KNOWN_BG,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
Reaction::Continue
}
}
#[test]
fn find_locates_status_bar_segment_by_text_and_reads_back_known_pixel() {
let driver = MacDriver::new(StatusBarApp, W, H);
let bounds = driver
.find_bounds("known-pixel")
.expect("find_bounds should locate the painted segment");
assert_eq!(
(bounds.x, bounds.y),
(
crate::primitives::status_bar::PIXEL_EDGE_INSET
+ crate::primitives::status_bar::PIXEL_SEGMENT_PADDING,
0.0
),
"the only segment's label should start `PIXEL_EDGE_INSET + \
PIXEL_SEGMENT_PADDING` in from the bar's left edge (issue #1155), \
not flush at its origin"
);
let (x, y) = driver
.find("known-pixel")
.expect("find should locate the painted segment label");
assert!(x >= bounds.x && x <= bounds.x + bounds.width);
assert!(y >= bounds.y && y <= bounds.y + bounds.height);
let (r, g, b, _a) = dominant_pixel(&driver, bounds);
assert_eq!(
(r, g, b),
(KNOWN_BG.r, KNOWN_BG.g, KNOWN_BG.b),
"dominant colour within find_bounds() should be the segment's bg, got ({r}, {g}, {b})"
);
assert!(driver.screen_contains("known-pixel"));
assert!(!driver.screen_contains("no such label"));
}
struct PanicsOnceThenPaints {
panicked_yet: std::cell::Cell<bool>,
}
impl AppLogic for PanicsOnceThenPaints {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
if !self.panicked_yet.replace(true) {
panic!("PanicsOnceThenPaints: deliberate panic for #922 coverage");
}
backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "known-pixel".to_string(),
fg: Color::rgb(255, 255, 255),
bg: KNOWN_BG,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
Reaction::Continue
}
}
#[test]
fn render_frame_survives_a_panicking_app_render_and_the_next_frame_still_paints() {
let mut driver = MacDriver::new(
PanicsOnceThenPaints {
panicked_yet: std::cell::Cell::new(false),
},
W,
H,
);
driver.render();
let bounds = driver
.find_bounds("known-pixel")
.expect("frame 2 (after a caught panic in frame 1) must still paint");
let (r, g, b, _a) = dominant_pixel(&driver, bounds);
assert_eq!(
(r, g, b),
(KNOWN_BG.r, KNOWN_BG.g, KNOWN_BG.b),
"frame 2's dominant colour within find_bounds() must be the segment's bg — the \
BeginDraw/EndDraw-equivalent frame-scope bracket must have stayed balanced \
despite frame 1's panic, got ({r}, {g}, {b})"
);
}
fn dominant_pixel<A: AppLogic>(driver: &MacDriver<A>, bounds: Rect) -> (u8, u8, u8, u8) {
use std::collections::HashMap;
let mut counts: HashMap<(u8, u8, u8, u8), u32> = HashMap::new();
for py in bounds.y as u32..(bounds.y + bounds.height) as u32 {
for px in bounds.x as u32..(bounds.x + bounds.width) as u32 {
*counts.entry(driver.pixel(px, py)).or_insert(0) += 1;
}
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(color, _)| color)
.expect("bounds should contain at least one pixel")
}
struct ToggleStatusBarApp {
on: bool,
}
impl ToggleStatusBarApp {
const OFF_BG: Color = Color::rgb(40, 40, 40);
const ON_BG: Color = Color::rgb(0, 200, 0);
fn bar(&self) -> StatusBar {
StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "Toggle".to_string(),
fg: Color::rgb(255, 255, 255),
bg: if self.on { Self::ON_BG } else { Self::OFF_BG },
bold: false,
action_id: Some(WidgetId::new("toggle")),
}],
right_segments: vec![],
}
}
}
impl AppLogic for ToggleStatusBarApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&self.bar(),
&crate::InteractionState::new(),
);
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
if let UiEvent::MouseDown { .. } = event {
self.on = !self.on;
return Reaction::Redraw;
}
Reaction::Continue
}
}
#[test]
fn click_toggles_segment_colour() {
let mut driver = MacDriver::new(ToggleStatusBarApp { on: false }, W, H);
let bounds = driver
.find_bounds("Toggle")
.expect("Toggle segment should be painted");
let (r0, g0, b0, _) = dominant_pixel(&driver, bounds);
assert_eq!(
(r0, g0, b0),
(
ToggleStatusBarApp::OFF_BG.r,
ToggleStatusBarApp::OFF_BG.g,
ToggleStatusBarApp::OFF_BG.b
)
);
let (x, y) = driver.find("Toggle").expect("Toggle label painted");
let reaction = driver.click(x, y);
assert_eq!(reaction, Reaction::Redraw, "click should trigger a redraw");
let bounds = driver
.find_bounds("Toggle")
.expect("Toggle segment still painted after click");
let (r1, g1, b1, _) = dominant_pixel(&driver, bounds);
assert_eq!(
(r1, g1, b1),
(
ToggleStatusBarApp::ON_BG.r,
ToggleStatusBarApp::ON_BG.g,
ToggleStatusBarApp::ON_BG.b
),
"after the click the segment should read back the ON colour"
);
}
#[test]
fn type_char_reaches_app_handle() {
struct QuitsOnQ {
quit: bool,
}
impl AppLogic for QuitsOnQ {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
if let UiEvent::KeyPressed {
key: Key::Char('q'),
..
} = event
{
self.quit = true;
return Reaction::Exit;
}
Reaction::Continue
}
}
let mut driver = MacDriver::new(QuitsOnQ { quit: false }, W, H);
assert!(!driver.exited());
driver.type_char('q');
assert!(driver.app().quit, "app.handle should have run");
assert!(driver.exited(), "'q' should make the app exit");
}
struct ActivityBarApp {
focused: bool,
seen: Vec<UiEvent>,
}
impl AppLogic for ActivityBarApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
use crate::primitives::activity_bar::{ActivityBar, ActivityItem};
backend.draw_activity_bar(
Rect::new(0.0, 0.0, W as f32, H as f32),
&ActivityBar {
id: WidgetId::new("bar"),
top_items: vec![ActivityItem {
id: WidgetId::new("panel:explorer"),
icon: "E".into(),
tooltip: "Explorer".into(),
is_active: true,
is_keyboard_selected: self.focused,
}],
bottom_items: Vec::new(),
active_accent: None,
selection_bg: None,
is_keyboard_focused: self.focused,
},
None,
);
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.seen.push(event);
Reaction::Continue
}
}
#[test]
fn keypress_redirects_to_focused_activity_bar() {
let mut driver = MacDriver::new(
ActivityBarApp {
focused: true,
seen: Vec::new(),
},
W,
H,
);
driver.type_char('j');
driver.press_named(crate::NamedKey::Enter);
let keys: Vec<String> = driver
.app()
.seen
.iter()
.filter_map(|ev| match ev {
UiEvent::ActivityBar(id, crate::ActivityBarEvent::KeyPressed { key, .. }) => {
Some(format!("{}:{key}", id.as_str()))
}
_ => None,
})
.collect();
assert_eq!(
keys,
vec!["bar:j".to_string(), "bar:Enter".to_string()],
"focused bar must receive normalised ActivityBar key events, got {:?}",
driver.app().seen
);
assert!(
!driver
.app()
.seen
.iter()
.any(|ev| matches!(ev, UiEvent::KeyPressed { .. })),
"no raw KeyPressed should leak through while the bar is focused"
);
}
#[test]
fn keypress_untouched_when_no_activity_bar_is_focused() {
let mut driver = MacDriver::new(
ActivityBarApp {
focused: false,
seen: Vec::new(),
},
W,
H,
);
driver.type_char('j');
assert!(
driver.app().seen.iter().any(|ev| matches!(
ev,
UiEvent::KeyPressed {
key: Key::Char('j'),
..
}
)),
"unfocused bar must not intercept: {:?}",
driver.app().seen
);
}
const SELECTABLE_LINE: &str = "hello world";
const SELECTABLE_BG: Color = Color::rgb(10, 10, 10);
struct SelectableApp;
impl AppLogic for SelectableApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let bounds = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.draw_status_bar_interactive(
bounds,
&StatusBar {
id: WidgetId::new("bg"),
left_segments: vec![StatusBarSegment {
text: format!(" {SELECTABLE_LINE} "),
fg: Color::rgb(255, 255, 255),
bg: SELECTABLE_BG,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
backend.register_text_region(crate::TextRegion {
id: WidgetId::new("bg"),
bounds,
lines: vec![SELECTABLE_LINE.to_string()],
});
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
Reaction::Continue
}
}
#[test]
fn drag_select_paints_highlight_then_ctrl_c_copies_to_clipboard() {
let _pasteboard = crate::macos::lock_real_pasteboard();
let mut driver = MacDriver::new(SelectableApp, W, H);
let before = driver.pixel(2, 2);
driver.drag(0.0, 4.0, (W - 4) as f32, 4.0);
assert!(
driver.backend().active_text_selection().is_some(),
"dragging across the registered TextRegion must produce an active selection"
);
let after = driver.pixel(2, 2);
assert_ne!(
before, after,
"apply_selection_highlight must have painted over the background by the time \
the drag finishes: before={before:?} after={after:?}"
);
assert!(
after.2 > before.2 + 20,
"the highlight is translucent blue, so the blue channel must rise noticeably \
at a selected pixel: before={before:?} after={after:?}"
);
driver.ctrl_char('c');
assert!(
driver.backend().active_text_selection().is_none(),
"Ctrl-C must clear the selection after copying it"
);
assert_eq!(
driver
.backend()
.services()
.clipboard()
.read_text()
.as_deref(),
Some(SELECTABLE_LINE),
"Ctrl-C must copy the dragged text to the real OS clipboard"
);
}
struct DeferredWorkApp {
pending: std::cell::Cell<bool>,
drained: std::cell::Cell<bool>,
}
impl AppLogic for DeferredWorkApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
if !self.drained.get() {
return;
}
backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&StatusBar {
id: WidgetId::new("status"),
left_segments: vec![StatusBarSegment {
text: "dialog-open".to_string(),
fg: Color::rgb(255, 255, 255),
bg: KNOWN_BG,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.pending.set(true);
Reaction::Continue
}
fn tick(&mut self, _backend: &mut dyn Backend) -> Reaction {
if self.pending.replace(false) {
self.drained.set(true);
return Reaction::Redraw;
}
Reaction::Continue
}
}
#[test]
fn tick_drains_deferred_work_queued_with_no_further_input() {
let mut driver = MacDriver::new(
DeferredWorkApp {
pending: std::cell::Cell::new(false),
drained: std::cell::Cell::new(false),
},
W,
H,
);
assert!(!driver.screen_contains("dialog-open"));
let reaction = driver.dispatch(UiEvent::KeyPressed {
key: Key::Char('o'),
modifiers: Modifiers::default(),
repeat: false,
});
assert_eq!(
reaction,
Reaction::Continue,
"queuing deferred work must not force an immediate redraw — that's the whole \
reason this pattern needs `tick` at all"
);
assert!(
!driver.screen_contains("dialog-open"),
"the deferred work must not already be drained by dispatch itself"
);
let tick_reaction = driver.tick();
assert_eq!(
tick_reaction,
Reaction::Redraw,
"tick must drain the queued work and force a redraw"
);
assert!(
driver.screen_contains("dialog-open"),
"tick's Redraw must actually repaint the drained work's effect, not just report it"
);
}
}