#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
feature = "win"
))]
use crate::runner::Reaction;
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
feature = "win"
))]
use crate::runtime::EventOutcome;
pub(crate) struct DriverCore<B, A> {
app: A,
backend: B,
exited: bool,
}
impl<B, A> DriverCore<B, A> {
pub(crate) fn new(app: A, backend: B) -> Self {
Self {
app,
backend,
exited: false,
}
}
pub(crate) fn app(&self) -> &A {
&self.app
}
pub(crate) fn app_mut(&mut self) -> &mut A {
&mut self.app
}
pub(crate) fn backend(&self) -> &B {
&self.backend
}
#[cfg(any(feature = "tui", feature = "gtk", feature = "win"))]
pub(crate) fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
pub(crate) fn exited(&self) -> bool {
self.exited
}
#[cfg(feature = "tui")]
pub(crate) fn mark_exited(&mut self) {
self.exited = true;
}
pub(crate) fn parts_mut(&mut self) -> (&mut B, &mut A) {
(&mut self.backend, &mut self.app)
}
}
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
feature = "win"
))]
impl<B, A> DriverCore<B, A> {
pub(crate) fn apply_outcome(
&mut self,
outcome: EventOutcome,
render: impl FnOnce(&mut B, &A),
) -> Reaction {
match outcome {
EventOutcome::Continue => Reaction::Continue,
EventOutcome::Redraw => {
render(&mut self.backend, &self.app);
Reaction::Redraw
}
EventOutcome::RedrawAfter(d) => Reaction::RedrawAfter(d),
EventOutcome::Exit => {
self.exited = true;
Reaction::Exit
}
}
}
}
#[cfg(any(all(feature = "macos", target_os = "macos"), feature = "win"))]
mod text_run_query {
use super::DriverCore;
use crate::testing::TextRun;
use crate::Rect;
pub(crate) trait TextRunSource {
fn text_runs(&self) -> &[TextRun];
}
#[cfg(all(feature = "macos", target_os = "macos"))]
impl TextRunSource for crate::macos::backend::MacBackend {
fn text_runs(&self) -> &[TextRun] {
self.text_runs()
}
}
#[cfg(feature = "win")]
impl TextRunSource for crate::win::backend::WinBackend {
fn text_runs(&self) -> &[TextRun] {
self.text_runs()
}
}
impl<B: TextRunSource, A> DriverCore<B, A> {
pub(crate) fn painted_texts(&self) -> Vec<&str> {
self.backend()
.text_runs()
.iter()
.map(|r| r.text.as_str())
.collect()
}
pub(crate) fn screen_contains(&self, needle: &str) -> bool {
self.backend()
.text_runs()
.iter()
.any(|r| r.text.contains(needle))
}
pub(crate) fn find_bounds(&self, needle: &str) -> Option<Rect> {
self.backend()
.text_runs()
.iter()
.find(|r| r.text.contains(needle))
.map(|r| r.bounds)
}
pub(crate) fn find(&self, needle: &str) -> Option<(f32, f32)> {
self.find_bounds(needle)
.map(|b| (b.x + b.width / 2.0, b.y + b.height / 2.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeTextBackend {
runs: Vec<TextRun>,
}
impl TextRunSource for FakeTextBackend {
fn text_runs(&self) -> &[TextRun] {
&self.runs
}
}
fn fake_text_core() -> DriverCore<FakeTextBackend, ()> {
DriverCore::new(
(),
FakeTextBackend {
runs: vec![TextRun {
text: "hello world".into(),
bounds: Rect::new(10.0, 20.0, 40.0, 8.0),
}],
},
)
}
#[test]
fn painted_texts_lists_every_run() {
let core = fake_text_core();
assert_eq!(core.painted_texts(), vec!["hello world"]);
}
#[test]
fn screen_contains_matches_a_substring() {
let core = fake_text_core();
assert!(core.screen_contains("world"));
assert!(!core.screen_contains("nope"));
}
#[test]
fn find_bounds_and_find_locate_the_matching_run() {
let core = fake_text_core();
assert_eq!(
core.find_bounds("hello"),
Some(Rect::new(10.0, 20.0, 40.0, 8.0))
);
assert_eq!(core.find("hello"), Some((30.0, 24.0)));
assert_eq!(core.find_bounds("nope"), None);
assert_eq!(core.find("nope"), None);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeApp(u32);
struct FakeBackend;
#[test]
fn accessors_read_and_write_through() {
let mut core: DriverCore<FakeBackend, FakeApp> = DriverCore::new(FakeApp(1), FakeBackend);
assert_eq!(core.app().0, 1);
core.app_mut().0 = 2;
assert_eq!(core.app().0, 2);
let _: &FakeBackend = core.backend();
assert!(
!core.exited(),
"a freshly constructed core must not report exited"
);
}
#[test]
#[cfg(any(
feature = "tui",
feature = "gtk",
all(feature = "win", target_os = "windows")
))]
fn backend_mut_gives_mutable_access() {
let mut core: DriverCore<FakeApp, FakeApp> = DriverCore::new(FakeApp(1), FakeApp(0));
core.backend_mut().0 = 42;
assert_eq!(core.backend().0, 42);
}
#[test]
#[cfg(feature = "tui")]
fn mark_exited_latches_without_going_through_apply_outcome() {
let mut core: DriverCore<FakeBackend, FakeApp> = DriverCore::new(FakeApp(0), FakeBackend);
assert!(!core.exited());
core.mark_exited();
assert!(core.exited());
}
#[test]
fn parts_mut_gives_disjoint_mutable_access_to_both_fields() {
let mut core: DriverCore<FakeApp, FakeApp> = DriverCore::new(FakeApp(1), FakeApp(2));
let (backend, app) = core.parts_mut();
backend.0 = 10;
app.0 = 20;
assert_eq!(core.backend().0, 10);
assert_eq!(core.app().0, 20);
}
#[test]
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
fn apply_outcome_continue_is_a_no_op() {
let mut core: DriverCore<FakeBackend, FakeApp> = DriverCore::new(FakeApp(0), FakeBackend);
let mut rendered = 0;
let reaction = core.apply_outcome(EventOutcome::Continue, |_, _| rendered += 1);
assert_eq!(reaction, Reaction::Continue);
assert_eq!(rendered, 0, "Continue must never invoke the render closure");
assert!(!core.exited());
}
#[test]
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
fn apply_outcome_redraw_renders_and_reports_redraw() {
let mut core: DriverCore<FakeBackend, FakeApp> = DriverCore::new(FakeApp(0), FakeBackend);
let mut rendered = 0;
let reaction = core.apply_outcome(EventOutcome::Redraw, |_, _| rendered += 1);
assert_eq!(reaction, Reaction::Redraw);
assert_eq!(
rendered, 1,
"Redraw must invoke the render closure exactly once"
);
assert!(!core.exited());
}
#[test]
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
fn apply_outcome_exit_latches_exited_without_rendering() {
let mut core: DriverCore<FakeBackend, FakeApp> = DriverCore::new(FakeApp(0), FakeBackend);
let mut rendered = 0;
let reaction = core.apply_outcome(EventOutcome::Exit, |_, _| rendered += 1);
assert_eq!(reaction, Reaction::Exit);
assert_eq!(rendered, 0, "Exit must never invoke the render closure");
assert!(
core.exited(),
"Exit must latch `exited` so a later dispatch short-circuits"
);
}
}