use alloc::collections::VecDeque;
use alloc::string::String;
use retroglyph_core::backend::Headless;
use retroglyph_core::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use retroglyph_core::grid::Pos;
use retroglyph_core::terminal::Terminal;
use crate::interact::Interaction;
use crate::ui::Ui;
pub struct WidgetHarness<Id> {
term: Terminal<Headless>,
interaction: Interaction<Id>,
queued: VecDeque<Event>,
}
impl<Id> WidgetHarness<Id> {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
Self {
term: Terminal::new(Headless::new(width, height)),
interaction: Interaction::new(),
queued: VecDeque::new(),
}
}
pub fn push_event(&mut self, event: Event) {
self.queued.push_back(event);
}
pub fn click(&mut self, x: u16, y: u16) {
self.click_button(x, y, MouseButton::Left);
}
pub fn click_button(&mut self, x: u16, y: u16, button: MouseButton) {
let position = Pos::new(x, y);
for kind in [MouseEventKind::Down(button), MouseEventKind::Up(button)] {
self.push_event(Event::Mouse(MouseEvent::new(
kind,
position,
KeyModifiers::NONE,
)));
}
}
pub fn mouse_move(&mut self, x: u16, y: u16) {
self.push_event(Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(x, y),
KeyModifiers::NONE,
)));
}
pub fn key(&mut self, code: KeyCode) {
self.key_with(code, KeyModifiers::NONE);
}
pub fn key_with(&mut self, code: KeyCode, modifiers: KeyModifiers) {
self.push_event(Event::Key(KeyEvent::new(code, modifiers)));
}
#[must_use]
pub const fn interaction(&self) -> &Interaction<Id> {
&self.interaction
}
#[must_use]
pub fn view(&self) -> String {
self.term.backend().format_view()
}
#[must_use]
pub fn readable_view(&self) -> String {
self.view().replace(Headless::SPACE_GLYPH, " ")
}
}
impl<Id: Copy + PartialEq> WidgetHarness<Id> {
pub fn step<R>(&mut self, f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R) -> R {
self.interaction.begin_frame();
for event in self.queued.drain(..) {
let _ = self.interaction.handle_event(&event);
}
let result = f(&mut Ui::new(
&mut self.term.surface(),
&mut self.interaction,
));
self.interaction.end_frame();
let Ok(()) = self.term.present();
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use retroglyph_core::color::Style;
use retroglyph_core::grid::Rect;
use crate::interact::{Response, Sense};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Id {
Save,
Cancel,
}
fn register(harness: &mut WidgetHarness<Id>, area: Rect) {
let _ = harness.step(|ui| ui.interaction().interact(area, Id::Save, Sense::click()));
}
fn interact(harness: &mut WidgetHarness<Id>, area: Rect) -> Response<Id> {
harness.step(|ui| ui.interaction().interact(area, Id::Save, Sense::click()))
}
#[test]
fn click_resolves_one_step_after_it_was_queued() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.click(2, 0);
let _ = interact(&mut harness, area); let response = interact(&mut harness, area);
assert!(response.clicked());
}
#[test]
fn click_button_queues_a_non_left_button() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.click_button(2, 0, MouseButton::Right);
let _ = interact(&mut harness, area);
let response = interact(&mut harness, area);
assert!(!response.clicked());
}
#[test]
fn mouse_move_updates_hover_without_a_click() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.mouse_move(2, 0);
let _ = interact(&mut harness, area);
let response = interact(&mut harness, area);
assert!(response.hovered());
assert!(!response.clicked());
}
#[test]
fn key_feeds_a_press_with_no_modifiers() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.key(KeyCode::Tab);
let response = harness.step(|ui| ui.interaction().interact(area, Id::Save, Sense::click()));
assert!(response.focused());
}
#[test]
fn step_presents_before_view_reflects_it() {
let mut harness = WidgetHarness::<Id>::new(3, 1);
assert!(harness.view().starts_with('ยท'));
harness.step(|ui| {
ui.surface().put((0, 0), '@', Style::default());
});
assert!(harness.view().starts_with('@'));
}
#[test]
fn readable_view_converts_the_middle_dot_back_to_a_literal_space() {
let mut harness = WidgetHarness::<Id>::new(12, 1);
harness.step(|ui| {
ui.surface().print((0, 0), "Save Game", Style::default());
});
assert!(!harness.view().contains("Save Game"));
assert!(harness.readable_view().contains("Save Game"));
}
#[test]
fn push_event_queues_an_arbitrary_event() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.push_event(Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0),
KeyModifiers::NONE,
)));
harness.push_event(Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
Pos::new(2, 0),
KeyModifiers::NONE,
)));
let _ = interact(&mut harness, area);
let response = interact(&mut harness, area);
assert!(response.clicked());
}
#[test]
fn interaction_exposes_the_underlying_context() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let area = Rect::new(0, 0, 7, 1);
register(&mut harness, area);
harness.mouse_move(2, 0);
let _ = interact(&mut harness, area);
let _ = interact(&mut harness, area);
assert_eq!(harness.interaction().hovered(), Some(Id::Save));
}
#[test]
fn distinct_ids_track_independently() {
let mut harness = WidgetHarness::<Id>::new(10, 3);
let save_area = Rect::new(0, 0, 4, 1);
let cancel_area = Rect::new(5, 0, 4, 1);
let frame = |harness: &mut WidgetHarness<Id>| {
harness.step(|ui| {
let save = ui
.interaction()
.interact(save_area, Id::Save, Sense::click());
let cancel = ui
.interaction()
.interact(cancel_area, Id::Cancel, Sense::click());
(save, cancel)
})
};
let _ = frame(&mut harness);
harness.click(6, 0);
let _ = frame(&mut harness);
let (save, cancel) = frame(&mut harness);
assert!(!save.clicked());
assert!(cancel.clicked());
}
}