#[cfg(any(
feature = "tui",
feature = "gtk",
all(feature = "macos", target_os = "macos"),
feature = "win"
))]
pub(crate) mod driver_core;
pub mod a11y;
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
use std::cell::RefCell;
use crate::runner::{AppLogic, Reaction};
use crate::{Key, Modifiers, NamedKey, Point, Rect, ScrollDelta, UiEvent, WidgetId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LogicalViewport {
pub cols: u32,
pub rows: u32,
}
impl LogicalViewport {
pub const fn new(cols: u32, rows: u32) -> Self {
Self { cols, rows }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Anchor {
Center,
LeftEdge,
RightEdge,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextRun {
pub text: String,
pub bounds: Rect,
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
thread_local! {
static TEXT_RUN_SINK: RefCell<Option<Vec<TextRun>>> = const { RefCell::new(None) };
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
pub(crate) fn install_text_run_sink() -> Option<Vec<TextRun>> {
TEXT_RUN_SINK.with(|s| s.borrow_mut().replace(Vec::new()))
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
pub(crate) fn take_text_run_sink(previous: Option<Vec<TextRun>>) -> Vec<TextRun> {
TEXT_RUN_SINK.with(|s| {
let mut slot = s.borrow_mut();
let recorded = slot.take().unwrap_or_default();
*slot = previous;
recorded
})
}
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
pub(crate) fn text_run_sink_active() -> bool {
TEXT_RUN_SINK.with(|s| s.borrow().is_some())
}
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
pub(crate) fn record_text_run(text: &str, bounds: Rect) {
if text.trim().is_empty() {
return;
}
TEXT_RUN_SINK.with(|s| {
if let Some(sink) = s.borrow_mut().as_mut() {
sink.push(TextRun {
text: text.to_string(),
bounds,
});
}
});
}
#[derive(Debug, Clone, PartialEq)]
pub struct ZoneRec {
pub id: WidgetId,
pub bounds: Rect,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FrameInventory {
pub text_runs: Vec<TextRun>,
pub zones: Vec<ZoneRec>,
}
impl FrameInventory {
pub fn text_runs(&self) -> &[TextRun] {
&self.text_runs
}
pub fn zones(&self) -> &[ZoneRec] {
&self.zones
}
pub fn screen_has(&self, needle: &str) -> bool {
self.text_runs.iter().any(|r| r.text.contains(needle))
}
pub fn absent(&self, needle: &str) -> bool {
!self.screen_has(needle)
}
pub fn count(&self, needle: &str) -> usize {
self.text_runs
.iter()
.filter(|r| r.text.contains(needle))
.count()
}
fn locate(&self, needle: &str) -> Option<Rect> {
self.text_runs
.iter()
.find(|r| r.text.contains(needle))
.map(|r| r.bounds)
}
fn zone_bounds(&self, id: &WidgetId) -> Option<Rect> {
self.zones.iter().find(|z| &z.id == id).map(|z| z.bounds)
}
pub fn left_of(&self, a: &str, b: &str) -> bool {
match (self.locate(a), self.locate(b)) {
(Some(ra), Some(rb)) => ra.x + ra.width <= rb.x,
_ => false,
}
}
pub fn above(&self, a: &str, b: &str) -> bool {
match (self.locate(a), self.locate(b)) {
(Some(ra), Some(rb)) => ra.y + ra.height <= rb.y,
_ => false,
}
}
pub fn same_row(&self, a: &str, b: &str) -> bool {
match (self.locate(a), self.locate(b)) {
(Some(ra), Some(rb)) => ra.y < rb.y + rb.height && rb.y < ra.y + ra.height,
_ => false,
}
}
pub fn inside(&self, needle: &str, zone_id: &WidgetId) -> bool {
match (self.locate(needle), self.zone_bounds(zone_id)) {
(Some(r), Some(z)) => {
r.x >= z.x
&& r.y >= z.y
&& r.x + r.width <= z.x + z.width
&& r.y + r.height <= z.y + z.height
}
_ => false,
}
}
}
pub trait DriverInput: Sized {
fn dispatch(&mut self, event: UiEvent) -> Reaction;
fn mouse_down(&mut self, x: f32, y: f32) -> Reaction;
fn mouse_move(&mut self, x: f32, y: f32) -> Reaction;
fn mouse_up(&mut self, x: f32, y: f32) -> Reaction;
fn press(&mut self, key: Key) -> Reaction {
self.dispatch(UiEvent::KeyPressed {
key,
modifiers: Modifiers::default(),
repeat: false,
})
}
fn type_char(&mut self, c: char) -> Reaction {
self.press(Key::Char(c))
}
fn press_named(&mut self, key: NamedKey) -> Reaction {
self.press(Key::Named(key))
}
fn ctrl_char(&mut self, c: char) -> Reaction {
self.dispatch(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers: Modifiers {
ctrl: true,
..Modifiers::default()
},
repeat: false,
})
}
fn click(&mut self, x: f32, y: f32) -> Reaction {
self.mouse_down(x, y)
}
fn drag(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) -> Reaction {
self.mouse_down(x0, y0);
self.mouse_move(x1, y1);
self.mouse_up(x1, y1)
}
}
pub trait PixelClickConformance: DriverInput {
const NAME: &'static str;
fn find_bounds(&self, needle: &str) -> Option<Rect>;
fn find(&self, needle: &str) -> Option<(f32, f32)>;
fn conformance_line_height(&self) -> f32;
fn click_text_at(&mut self, needle: &str, at: Anchor) {
let bounds = self
.find_bounds(needle)
.unwrap_or_else(|| panic!("{}: {needle:?} not painted", Self::NAME));
let y = bounds.y + bounds.height / 2.0;
let x = match at {
Anchor::Center => bounds.x + bounds.width / 2.0,
Anchor::LeftEdge => bounds.x + 1.0,
Anchor::RightEdge => bounds.x + bounds.width - 1.0,
};
self.click(x, y);
}
fn drag_text(&mut self, from: &str, to: &str) {
let (x0, y0) = self
.find(from)
.unwrap_or_else(|| panic!("{}: {from:?} not painted", Self::NAME));
let (x1, y1) = self
.find(to)
.unwrap_or_else(|| panic!("{}: {to:?} not painted", Self::NAME));
self.drag(x0, y0, x1, y1);
}
fn scroll_at(&mut self, needle: &str, lines: i32) {
let (x, y) = self
.find(needle)
.unwrap_or_else(|| panic!("{}: {needle:?} not painted", Self::NAME));
let line_height = self.conformance_line_height();
self.dispatch(UiEvent::Scroll {
widget: None,
delta: ScrollDelta::new(0.0, lines as f32 * line_height),
position: Point::new(x, y),
});
}
}
pub trait ConformanceDriver: Sized {
type App: AppLogic;
fn new_fixture(app: Self::App, viewport: LogicalViewport) -> Self;
fn press_named(&mut self, key: NamedKey);
fn type_char(&mut self, c: char);
fn ctrl_char(&mut self, c: char);
fn type_text(&mut self, s: &str) {
for c in s.chars() {
self.type_char(c);
}
}
fn click_text(&mut self, needle: &str) {
self.click_text_at(needle, Anchor::Center);
}
fn click_text_at(&mut self, needle: &str, at: Anchor);
fn drag_text(&mut self, from: &str, to: &str);
fn scroll_at(&mut self, needle: &str, lines: i32);
fn backend_caps(&self) -> crate::BackendCaps;
fn inventory(&self) -> FrameInventory;
fn screen_has(&self, needle: &str) -> bool;
fn exited(&self) -> bool;
}
#[cfg(test)]
thread_local! {
static TREE_PAINTS_OWN_SCROLLBAR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) struct TreePaintsOwnScrollbar(bool);
#[cfg(test)]
impl TreePaintsOwnScrollbar {
pub(crate) fn set(value: bool) -> Self {
Self(TREE_PAINTS_OWN_SCROLLBAR.with(|c| c.replace(value)))
}
}
#[cfg(test)]
impl Drop for TreePaintsOwnScrollbar {
fn drop(&mut self) {
TREE_PAINTS_OWN_SCROLLBAR.with(|c| c.set(self.0));
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RecordingBackend {
pub calls: Vec<&'static str>,
pub viewport: crate::Viewport,
pub line_height: f32,
pub char_width: f32,
pub chrome_line_height: Option<f32>,
pub focus: crate::focus::FocusManager,
pub invalidation_log: crate::backend::InvalidationLog,
}
impl Default for RecordingBackend {
fn default() -> Self {
Self {
calls: Vec::new(),
viewport: crate::Viewport {
width: 80.0,
height: 24.0,
scale: 1.0,
},
line_height: 1.0,
char_width: 1.0,
chrome_line_height: None,
focus: crate::focus::FocusManager::new(),
invalidation_log: crate::backend::InvalidationLog::new(),
}
}
}
impl RecordingBackend {
pub fn new() -> Self {
Self::default()
}
pub fn with_viewport(viewport: crate::Viewport, line_height: f32, char_width: f32) -> Self {
Self {
viewport,
line_height,
char_width,
..Self::default()
}
}
fn record(&mut self, name: &'static str) {
self.calls.push(name);
}
}
fn empty_tab_bar_layout() -> crate::TabBarLayout {
crate::TabBarLayout {
bar_width: 0.0,
bar_height: 0.0,
visible_tabs: Vec::new(),
visible_segments: Vec::new(),
scroll_left: None,
scroll_right: None,
hit_regions: Vec::new(),
resolved_scroll_offset: 0,
}
}
impl crate::backend::sealed::Sealed for RecordingBackend {}
impl crate::Backend for RecordingBackend {
fn viewport(&self) -> crate::Viewport {
self.viewport
}
fn begin_frame(&mut self, _v: crate::Viewport) {}
fn end_frame(&mut self) {}
fn poll_events(&mut self) -> Vec<UiEvent> {
Vec::new()
}
fn wait_events(&mut self, _t: std::time::Duration) -> Vec<UiEvent> {
Vec::new()
}
fn waker(&self) -> std::sync::Arc<dyn Fn(crate::UserPayload) + Send + Sync> {
std::sync::Arc::new(|_payload| {})
}
fn request_frame_in(&self, _delay: std::time::Duration) {
}
fn register_accelerator(&mut self, _a: &crate::Accelerator) {}
fn unregister_accelerator(&mut self, _id: &crate::AcceleratorId) {}
fn invalidate_rect(&mut self, rect: crate::event::Rect) {
self.invalidation_log.push(rect);
}
fn invalidation_log(&self) -> &crate::backend::InvalidationLog {
&self.invalidation_log
}
fn modal_stack_handle(&self) -> std::rc::Rc<std::cell::RefCell<crate::ModalStack>> {
unimplemented!(
"RecordingBackend has no shared ModalStack — drive a real backend \
(quadraui::tui::testing::TuiDriver) if your test needs one"
)
}
fn focus_manager(&self) -> &crate::focus::FocusManager {
&self.focus
}
fn draw_focus_ring(&mut self, _r: Rect) {
self.record("draw_focus_ring");
}
fn drag_state_handle(&self) -> std::rc::Rc<std::cell::RefCell<crate::DragState>> {
unimplemented!(
"RecordingBackend has no shared DragState — drive a real backend \
(quadraui::tui::testing::TuiDriver) if your test needs one"
)
}
fn services(&self) -> &dyn crate::backend::PlatformServices {
unimplemented!(
"RecordingBackend has no PlatformServices — drive a real backend \
(quadraui::tui::testing::TuiDriver) if your test needs one"
)
}
fn backend_caps(&self) -> crate::backend::BackendCaps {
crate::backend::BackendCaps::empty()
}
fn line_height(&self) -> f32 {
self.line_height
}
fn chrome_line_height(&self) -> f32 {
self.chrome_line_height.unwrap_or(self.line_height)
}
fn char_width(&self) -> f32 {
self.char_width
}
fn list_char_width(&self) -> f32 {
self.char_width
}
fn measure_text(&self, text: &str, _role: crate::FontRole) -> (f32, f32) {
(
text.chars().count() as f32 * self.char_width,
self.line_height,
)
}
fn default_fonts(&self) -> crate::backend::PlatformFontDefaults {
crate::backend::PlatformFontDefaults {
editor_family: String::new(),
editor_size_pt: 0.0,
ui_family: String::new(),
ui_size_pt: 0.0,
}
}
fn draw_tree(&mut self, _r: Rect, _t: &crate::TreeView) {
self.record("draw_tree");
}
fn draw_list(&mut self, _r: Rect, _l: &crate::ListView) {
self.record("draw_list");
}
fn draw_data_table(
&mut self,
_r: Rect,
_t: &crate::DataTable,
_h: Option<usize>,
) -> crate::DataTableLayout {
self.record("draw_data_table");
unimplemented!("RecordingBackend has no DataTable layout algorithm")
}
fn data_table_layout(&self, _r: Rect, _t: &crate::DataTable) -> crate::DataTableLayout {
unimplemented!("RecordingBackend has no DataTable layout algorithm")
}
fn list_hscrollbar(&self, _r: Rect, _l: &crate::ListView) -> Option<crate::Scrollbar> {
None
}
fn list_vscrollbar(&self, _r: Rect, _l: &crate::ListView) -> Option<crate::Scrollbar> {
None
}
fn list_layout(&self, r: Rect, l: &crate::ListView) -> crate::ListViewLayout {
l.layout(r.width, r.height, 0.0, |_| {
crate::primitives::list::ListItemMeasure::new(1.0)
})
}
fn draw_form(&mut self, _r: Rect, _f: &crate::Form) {
self.record("draw_form");
}
fn draw_palette(&mut self, _r: Rect, _p: &crate::Palette) {
self.record("draw_palette");
}
fn palette_layout(&self, r: Rect, p: &crate::Palette) -> crate::PaletteLayout {
let lh = self.line_height;
let title_h = lh;
let query_h = if p.show_query { lh } else { 0.0 };
p.layout(r.width, r.height, title_h, query_h, 0.0, 1.0, |_| {
crate::PaletteItemMeasure::new(lh)
})
}
fn draw_settings_chrome(
&mut self,
_r: Rect,
_header_text: &str,
_query: &str,
_placeholder: &str,
_active: bool,
) {
self.record("draw_settings_chrome");
}
fn draw_status_bar_interactive(
&mut self,
r: Rect,
b: &crate::primitives::status_bar::StatusBar,
_interaction: &crate::interaction::InteractionState,
) -> crate::StatusBarLayout {
self.record("draw_status_bar");
let cw = self.char_width.max(0.0);
b.layout(r.width, r.height, 2.0 * cw, |seg| {
crate::StatusSegmentMeasure::new(seg.text.chars().count() as f32 * cw)
})
}
#[allow(deprecated)]
fn draw_tab_bar(
&mut self,
_r: Rect,
_b: &crate::TabBar,
_h: Option<usize>,
) -> crate::TabBarHits {
self.record("draw_tab_bar");
crate::TabBarHits::default()
}
#[allow(deprecated)] fn draw_tab_bar_icons(
&mut self,
_r: Rect,
_b: &crate::TabBar,
_icons: &[Option<crate::TabIcon>],
_h: Option<usize>,
) -> crate::TabBarHits {
self.record("draw_tab_bar_icons");
crate::TabBarHits::default()
}
fn draw_tab_bar_layout(
&mut self,
_r: Rect,
_b: &crate::TabBar,
_h: Option<usize>,
) -> crate::TabBarLayout {
self.record("draw_tab_bar_layout");
empty_tab_bar_layout()
}
fn draw_tab_bar_icons_layout(
&mut self,
_r: Rect,
_b: &crate::TabBar,
_icons: &[Option<crate::TabIcon>],
_h: Option<usize>,
) -> crate::TabBarLayout {
self.record("draw_tab_bar_icons_layout");
empty_tab_bar_layout()
}
fn draw_activity_bar(
&mut self,
_r: Rect,
_b: &crate::primitives::activity_bar::ActivityBar,
_h: Option<usize>,
) -> Vec<crate::primitives::activity_bar::ActivityBarRowHit> {
self.record("draw_activity_bar");
Vec::new()
}
fn draw_terminal(&mut self, _r: Rect, _t: &crate::Terminal) {
self.record("draw_terminal");
}
fn draw_terminal_divider(&mut self, _r: Rect) {
self.record("draw_terminal_divider");
}
fn draw_solid_fill(&mut self, _r: Rect, _c: crate::Color) {
self.record("draw_solid_fill");
}
fn draw_text_display(&mut self, _r: Rect, _t: &crate::TextDisplay) {
self.record("draw_text_display");
}
fn draw_command_line(&mut self, _r: Rect, _c: &crate::CommandLine) {
self.record("draw_command_line");
}
fn draw_command_line_selection(
&mut self,
_r: Rect,
_c: &crate::CommandLine,
_s: Option<(usize, usize)>,
) {
self.record("draw_command_line_selection");
}
fn command_line_layout(
&self,
_r: Rect,
_c: &crate::CommandLine,
) -> crate::primitives::command_line::CommandLineLayout {
Default::default()
}
fn status_bar_layout(&self, _r: Rect, _b: &crate::StatusBar) -> crate::StatusBarLayout {
crate::StatusBarLayout {
bar_width: 0.0,
bar_height: 0.0,
visible_segments: Vec::new(),
hit_regions: Vec::new(),
resolved_right_start: 0,
}
}
#[allow(deprecated)] fn tab_bar_layout(&self, _r: Rect, _b: &crate::TabBar) -> crate::TabBarHits {
crate::TabBarHits::default()
}
#[allow(deprecated)] fn tab_bar_layout_icons(
&self,
_r: Rect,
_b: &crate::TabBar,
_icons: &[Option<crate::TabIcon>],
) -> crate::TabBarHits {
crate::TabBarHits::default()
}
fn resolve_tab_bar_layout(&self, _r: Rect, _b: &crate::TabBar) -> crate::TabBarLayout {
empty_tab_bar_layout()
}
fn resolve_tab_bar_layout_icons(
&self,
_r: Rect,
_b: &crate::TabBar,
_icons: &[Option<crate::TabIcon>],
) -> crate::TabBarLayout {
empty_tab_bar_layout()
}
fn activity_bar_layout(
&self,
_r: Rect,
_b: &crate::primitives::activity_bar::ActivityBar,
) -> Vec<crate::ActivityBarRowHit> {
Vec::new()
}
fn text_display_layout(&self, _r: Rect, _t: &crate::TextDisplay) -> crate::TextDisplayLayout {
unimplemented!("RecordingBackend has no TextDisplay layout algorithm")
}
fn draw_text_input(&mut self, r: Rect, ti: &crate::TextInput) -> crate::TextInputLayout {
self.record("draw_text_input");
ti.layout(r, crate::TextInputMeasure::from_metrics(&self.measure()))
}
fn text_input_layout(&self, r: Rect, ti: &crate::TextInput) -> crate::TextInputLayout {
ti.layout(r, crate::TextInputMeasure::from_metrics(&self.measure()))
}
fn draw_number_input(&mut self, _n: &crate::NumberInput, _l: &crate::NumberInputLayout) {
self.record("draw_number_input");
}
fn draw_combo_box(&mut self, _c: &crate::ComboBox, _l: &crate::ComboBoxLayout) {
self.record("draw_combo_box");
}
fn draw_date_picker(&mut self, _d: &crate::DatePicker, _l: &crate::DatePickerLayout) {
self.record("draw_date_picker");
}
fn draw_tooltip(&mut self, _t: &crate::Tooltip, _l: &crate::TooltipLayout) {
self.record("draw_tooltip");
}
fn draw_context_menu(
&mut self,
_m: &crate::ContextMenu,
_l: &crate::ContextMenuLayout,
) -> Vec<(Rect, WidgetId)> {
self.record("draw_context_menu");
Vec::new()
}
fn draw_dialog(&mut self, _d: &crate::Dialog, _l: &crate::DialogLayout) -> Vec<Rect> {
self.record("draw_dialog");
Vec::new()
}
fn draw_float(&mut self, _f: &crate::Float, _l: &crate::FloatLayout) {
self.record("draw_float");
}
fn draw_multi_section_view(&mut self, _r: Rect, _v: &crate::MultiSectionView) {
self.record("draw_multi_section_view");
}
fn msv_layout(&self, _r: Rect, _v: &crate::MultiSectionView) -> crate::MultiSectionViewLayout {
unimplemented!("RecordingBackend has no MultiSectionView layout algorithm")
}
fn msv_metrics(&self) -> crate::primitives::multi_section_view::MsvLayoutMetrics {
unimplemented!("RecordingBackend has no MultiSectionView metrics")
}
fn tree_layout(
&self,
rect: Rect,
tree: &crate::TreeView,
) -> crate::primitives::tree::TreeViewLayout {
let lh = self.line_height();
let indent_cells = tree.style.indent as f32;
let chevron_w = if tree.style.show_chevrons {
tree.style.chevron_expanded.chars().count() as f32 + 1.0
} else {
0.0
};
tree.layout(rect.width, rect.height, |i| {
let row = &tree.rows[i];
let chevron_end_x = if row.is_expanded.is_some() && chevron_w > 0.0 {
Some(row.indent as f32 * indent_cells + chevron_w)
} else {
None
};
crate::primitives::tree::TreeRowMeasure {
height: lh,
chevron_end_x,
}
})
}
fn tree_vscrollbar(&self, rect: Rect, tree: &crate::TreeView) -> Option<crate::Scrollbar> {
tree.vscrollbar_with_track_width(
rect,
self.line_height(),
crate::primitives::layout_metrics::pixel::TREE_VSCROLLBAR_WIDTH,
)
}
fn tree_paints_own_scrollbar(&self) -> bool {
#[cfg(test)]
{
TREE_PAINTS_OWN_SCROLLBAR.with(std::cell::Cell::get)
}
#[cfg(not(test))]
{
false
}
}
fn form_layout(&self, _r: Rect, _f: &crate::Form) -> crate::primitives::form::FormLayout {
unimplemented!("RecordingBackend has no Form layout algorithm")
}
fn draw_editor(
&mut self,
_r: Rect,
_e: &crate::primitives::editor::Editor,
) -> crate::backend::EditorPaintResult {
self.record("draw_editor");
Default::default()
}
fn draw_message_list(&mut self, _r: Rect, _l: &crate::primitives::message_list::MessageList) {
self.record("draw_message_list");
}
fn draw_rich_text_popup(
&mut self,
_p: &crate::RichTextPopup,
_l: &crate::primitives::rich_text_popup::RichTextPopupLayout,
) {
self.record("draw_rich_text_popup");
}
fn draw_find_replace(
&mut self,
_r: Rect,
_p: &crate::primitives::find_replace::FindReplacePanel,
) {
self.record("draw_find_replace");
}
fn draw_completions(
&mut self,
_c: &crate::Completions,
_l: &crate::primitives::completions::CompletionsLayout,
) {
self.record("draw_completions");
}
fn draw_scrollbar(&mut self, _r: Rect, _s: &crate::Scrollbar) {
self.record("draw_scrollbar");
}
fn draw_drop_overlay(&mut self, _o: &crate::primitives::drop_zone::DropOverlay) {
self.record("draw_drop_overlay");
}
fn draw_menu_bar(&mut self, _r: Rect, _b: &crate::MenuBar) -> crate::MenuBarLayout {
self.record("draw_menu_bar");
unimplemented!("RecordingBackend has no MenuBar layout algorithm")
}
fn menu_bar_layout(&self, _r: Rect, _b: &crate::MenuBar) -> crate::MenuBarLayout {
unimplemented!("RecordingBackend has no MenuBar layout algorithm")
}
fn draw_split(&mut self, _r: Rect, _s: &crate::Split) -> crate::SplitLayout {
self.record("draw_split");
unimplemented!("RecordingBackend has no Split layout algorithm")
}
fn split_layout(&self, _r: Rect, _s: &crate::Split) -> crate::SplitLayout {
unimplemented!("RecordingBackend has no Split layout algorithm")
}
fn draw_split_tree(&mut self, _r: Rect, _t: &crate::SplitTree) -> crate::SplitTreeLayout {
self.record("draw_split_tree");
unimplemented!("RecordingBackend has no SplitTree layout algorithm")
}
fn split_tree_layout(&self, _r: Rect, _t: &crate::SplitTree) -> crate::SplitTreeLayout {
unimplemented!("RecordingBackend has no SplitTree layout algorithm")
}
fn draw_panel(&mut self, _r: Rect, _p: &crate::Panel) -> crate::PanelLayout {
self.record("draw_panel");
unimplemented!("RecordingBackend has no Panel layout algorithm")
}
fn panel_layout(&self, _r: Rect, _p: &crate::Panel) -> crate::PanelLayout {
unimplemented!("RecordingBackend has no Panel layout algorithm")
}
fn draw_canvas(&mut self, _r: Rect, _c: &crate::Canvas) -> crate::CanvasLayout {
self.record("draw_canvas");
unimplemented!("RecordingBackend has no Canvas paint algorithm")
}
fn draw_toast_overlay(
&mut self,
_r: Rect,
_s: &crate::ToastOverlay,
) -> crate::ToastStackLayout {
self.record("draw_toast_stack");
unimplemented!("RecordingBackend has no ToastOverlay layout algorithm")
}
fn toast_stack_layout(&self, _r: Rect, _s: &crate::ToastOverlay) -> crate::ToastStackLayout {
unimplemented!("RecordingBackend has no ToastOverlay layout algorithm")
}
fn draw_pipeline_view(
&mut self,
_r: Rect,
_v: &crate::PipelineView,
) -> crate::PipelineViewLayout {
self.record("draw_pipeline_view");
unimplemented!("RecordingBackend has no PipelineView layout algorithm")
}
fn pipeline_view_layout(
&self,
_r: Rect,
_v: &crate::PipelineView,
) -> crate::PipelineViewLayout {
unimplemented!("RecordingBackend has no PipelineView layout algorithm")
}
fn draw_progress(&mut self, _r: Rect, _b: &crate::ProgressBar) -> crate::ProgressBarLayout {
self.record("draw_progress");
unimplemented!("RecordingBackend has no ProgressBar layout algorithm")
}
fn progress_layout(&self, _r: Rect, _b: &crate::ProgressBar) -> crate::ProgressBarLayout {
unimplemented!("RecordingBackend has no ProgressBar layout algorithm")
}
fn draw_spinner(&mut self, r: Rect, _s: &crate::Spinner) -> crate::SpinnerLayout {
self.record("draw_spinner");
crate::SpinnerLayout { bounds: r }
}
fn spinner_layout(&self, r: Rect, _s: &crate::Spinner) -> crate::SpinnerLayout {
crate::SpinnerLayout { bounds: r }
}
fn draw_button(&mut self, _r: Rect, _b: &crate::Button) -> crate::ButtonLayout {
self.record("draw_button");
unimplemented!("RecordingBackend has no Button layout algorithm")
}
fn button_layout(&self, _r: Rect, _b: &crate::Button) -> crate::ButtonLayout {
unimplemented!("RecordingBackend has no Button layout algorithm")
}
fn draw_radio_group(&mut self, _r: Rect, _g: &crate::RadioGroup) -> crate::RadioGroupLayout {
self.record("draw_radio_group");
unimplemented!("RecordingBackend has no RadioGroup layout algorithm")
}
fn radio_group_layout(&self, _r: Rect, _g: &crate::RadioGroup) -> crate::RadioGroupLayout {
unimplemented!("RecordingBackend has no RadioGroup layout algorithm")
}
fn draw_switch(&mut self, _r: Rect, _s: &crate::Switch) -> crate::SwitchLayout {
self.record("draw_switch");
unimplemented!("RecordingBackend has no Switch layout algorithm")
}
fn switch_layout(&self, _r: Rect, _s: &crate::Switch) -> crate::SwitchLayout {
unimplemented!("RecordingBackend has no Switch layout algorithm")
}
fn draw_command_center(
&mut self,
_r: Rect,
_c: &crate::CommandCenter,
) -> crate::CommandCenterLayout {
self.record("draw_command_center");
unimplemented!("RecordingBackend has no CommandCenter layout algorithm")
}
fn command_center_layout(
&self,
_r: Rect,
_c: &crate::CommandCenter,
) -> crate::CommandCenterLayout {
unimplemented!("RecordingBackend has no CommandCenter layout algorithm")
}
fn draw_chart(
&mut self,
_r: Rect,
_c: &crate::primitives::chart::Chart,
_h: Option<(usize, usize)>,
_x: Option<f64>,
) -> crate::primitives::chart::ChartLayout {
self.record("draw_chart");
unimplemented!("RecordingBackend has no Chart layout algorithm")
}
fn chart_layout(
&self,
_r: Rect,
_c: &crate::primitives::chart::Chart,
) -> crate::primitives::chart::ChartLayout {
unimplemented!("RecordingBackend has no Chart layout algorithm")
}
fn draw_toolbar_with_options(
&mut self,
_r: Rect,
_b: &crate::primitives::toolbar::Toolbar,
_interaction: &crate::interaction::InteractionState,
_options: crate::primitives::toolbar::ToolbarPaintOptions,
) -> crate::primitives::toolbar::ToolbarLayout {
self.record("draw_toolbar");
unimplemented!("RecordingBackend has no Toolbar layout algorithm")
}
fn toolbar_layout(
&self,
_r: Rect,
_b: &crate::primitives::toolbar::Toolbar,
) -> crate::primitives::toolbar::ToolbarLayout {
unimplemented!("RecordingBackend has no Toolbar layout algorithm")
}
fn draw_sidebar_panel_interactive(
&mut self,
_r: Rect,
_p: &crate::primitives::sidebar_panel::SidebarPanel,
_interaction: &crate::interaction::InteractionState,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
self.record("draw_sidebar_panel");
unimplemented!("RecordingBackend has no SidebarPanel layout algorithm")
}
fn sidebar_panel_layout(
&self,
_r: Rect,
_p: &crate::primitives::sidebar_panel::SidebarPanel,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
unimplemented!("RecordingBackend has no SidebarPanel layout algorithm")
}
fn draw_diff_view(
&mut self,
_r: Rect,
view: &crate::primitives::diff_view::DiffView,
) -> crate::primitives::diff_view::DiffViewLayout {
self.record("draw_diff_view");
crate::primitives::diff_view::DiffViewLayout {
visible_rows: 0,
total_rows: view.total_rows(),
}
}
fn draw_board(
&mut self,
_r: Rect,
_m: &crate::primitives::board::BoardModel,
) -> crate::primitives::board::BoardLayout {
self.record("draw_board");
crate::primitives::board::BoardLayout {
bounds: crate::event::Rect::new(_r.x, _r.y, _r.width, _r.height),
columns: vec![],
}
}
fn board_layout(
&self,
_r: Rect,
_m: &crate::primitives::board::BoardModel,
) -> crate::primitives::board::BoardLayout {
crate::primitives::board::BoardLayout {
bounds: crate::event::Rect::new(_r.x, _r.y, _r.width, _r.height),
columns: vec![],
}
}
fn draw_minimap(
&mut self,
_r: Rect,
_m: &crate::primitives::minimap::Minimap,
) -> crate::backend::MinimapPaintResult {
self.record("draw_minimap");
crate::backend::MinimapPaintResult::default()
}
fn minimap_layout(
&self,
_r: Rect,
_m: &crate::primitives::minimap::Minimap,
) -> crate::primitives::minimap::MinimapLayout {
crate::primitives::minimap::MinimapLayout::default()
}
fn draw_image(
&mut self,
_r: Rect,
_i: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
self.record("draw_image");
crate::backend::ImagePaintResult::Unsupported
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(
feature = "gtk",
all(feature = "macos", target_os = "macos"),
all(feature = "win", target_os = "windows")
))]
mod text_run_sink {
use super::*;
#[test]
fn record_is_a_no_op_with_no_sink_installed() {
record_text_run("stray", Rect::new(0.0, 0.0, 1.0, 1.0));
let prev = install_text_run_sink();
let runs = take_text_run_sink(prev);
assert!(
runs.is_empty(),
"a run recorded before any sink existed must not appear once one is installed: {runs:?}"
);
}
#[test]
fn install_then_record_then_take_round_trips_one_run() {
assert!(!text_run_sink_active());
let prev = install_text_run_sink();
assert!(text_run_sink_active());
record_text_run("hello", Rect::new(1.0, 2.0, 3.0, 4.0));
let runs = take_text_run_sink(prev);
assert_eq!(
runs,
vec![TextRun {
text: "hello".into(),
bounds: Rect::new(1.0, 2.0, 3.0, 4.0),
}]
);
assert!(
!text_run_sink_active(),
"take_text_run_sink(None) must leave recording off again"
);
}
#[test]
fn whitespace_only_runs_are_not_recorded() {
let prev = install_text_run_sink();
for text in ["", " ", "real"] {
record_text_run(text, Rect::new(0.0, 0.0, 1.0, 1.0));
}
let runs = take_text_run_sink(prev);
assert_eq!(
runs.len(),
1,
"only the non-blank run should record: {runs:?}"
);
assert_eq!(runs[0].text, "real");
}
#[test]
fn nested_install_restores_the_outer_sink_on_take() {
let outer_prev = install_text_run_sink();
record_text_run("outer-before", Rect::new(0.0, 0.0, 1.0, 1.0));
let inner_prev = install_text_run_sink();
record_text_run("inner", Rect::new(0.0, 0.0, 1.0, 1.0));
let inner_runs = take_text_run_sink(inner_prev);
assert_eq!(inner_runs.len(), 1);
assert_eq!(inner_runs[0].text, "inner");
assert!(text_run_sink_active());
record_text_run("outer-after", Rect::new(0.0, 0.0, 1.0, 1.0));
let outer_runs = take_text_run_sink(outer_prev);
assert_eq!(
outer_runs
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>(),
vec!["outer-before", "outer-after"]
);
}
}
fn fixture() -> FrameInventory {
FrameInventory {
text_runs: vec![
TextRun {
text: "E".into(),
bounds: Rect::new(0.0, 0.0, 1.0, 1.0),
},
TextRun {
text: "S".into(),
bounds: Rect::new(0.0, 1.0, 1.0, 1.0),
},
TextRun {
text: "G".into(),
bounds: Rect::new(0.0, 2.0, 1.0, 1.0),
},
TextRun {
text: "SOURCE CONTROL".into(),
bounds: Rect::new(10.0, 0.0, 14.0, 1.0),
},
TextRun {
text: "content".into(),
bounds: Rect::new(10.0, 1.0, 7.0, 1.0),
},
],
zones: vec![ZoneRec {
id: WidgetId::new("sidebar-content"),
bounds: Rect::new(10.0, 1.0, 30.0, 1.0),
}],
}
}
#[test]
fn screen_has_and_absent() {
let inv = fixture();
assert!(inv.screen_has("SOURCE"));
assert!(inv.screen_has("CONTROL"));
assert!(inv.absent("git"));
assert!(!inv.screen_has("git"));
}
#[test]
fn count_matches_every_run_containing_needle() {
let inv = fixture();
assert_eq!(inv.count("S"), 2);
assert_eq!(inv.count("G"), 1);
assert_eq!(inv.count("nope"), 0);
}
#[test]
fn left_of_is_true_for_the_icon_column_vs_the_sidebar() {
let inv = fixture();
assert!(inv.left_of("G", "SOURCE CONTROL"));
assert!(inv.left_of("G", "content"));
assert!(!inv.left_of("SOURCE CONTROL", "G"), "reverse must not hold");
}
#[test]
fn above_is_true_for_header_vs_content() {
let inv = fixture();
assert!(inv.above("SOURCE CONTROL", "content"));
assert!(
!inv.above("content", "SOURCE CONTROL"),
"reverse must not hold"
);
assert!(inv.above("E", "S"));
assert!(inv.above("S", "G"));
}
#[test]
fn same_row_true_for_overlapping_ranges_false_across_rows() {
let inv = fixture();
assert!(inv.same_row("E", "SOURCE CONTROL"));
assert!(inv.same_row("S", "content"));
assert!(!inv.same_row("E", "content"));
}
#[test]
fn inside_checks_containment_within_a_registered_zone() {
let inv = fixture();
let zone = WidgetId::new("sidebar-content");
assert!(inv.inside("content", &zone));
assert!(
!inv.inside("SOURCE CONTROL", &zone),
"the header run sits above the zone's y range, not inside it"
);
assert!(
!inv.inside("content", &WidgetId::new("no-such-zone")),
"an unregistered zone id must never report containment"
);
}
#[test]
fn relations_are_false_not_panicking_when_a_needle_never_painted() {
let inv = fixture();
assert!(!inv.left_of("nope", "G"));
assert!(!inv.above("G", "nope"));
assert!(!inv.same_row("nope", "G"));
assert!(!inv.inside("nope", &WidgetId::new("sidebar-content")));
}
use crate::MouseButton;
struct FakeDriver {
events: Vec<UiEvent>,
painted: Vec<(&'static str, Rect)>,
line_height: f32,
}
impl FakeDriver {
fn new() -> Self {
Self {
events: Vec::new(),
painted: vec![("Toggle", Rect::new(10.0, 20.0, 40.0, 8.0))],
line_height: 16.0,
}
}
}
impl DriverInput for FakeDriver {
fn dispatch(&mut self, event: UiEvent) -> Reaction {
self.events.push(event);
Reaction::Redraw
}
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(),
})
}
fn mouse_move(&mut self, x: f32, y: f32) -> Reaction {
self.dispatch(UiEvent::MouseMoved {
position: Point::new(x, y),
buttons: crate::ButtonMask {
left: true,
..crate::ButtonMask::default()
},
})
}
fn mouse_up(&mut self, x: f32, y: f32) -> Reaction {
self.dispatch(UiEvent::MouseUp {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
})
}
}
impl PixelClickConformance for FakeDriver {
const NAME: &'static str = "FakeDriver";
fn find_bounds(&self, needle: &str) -> Option<Rect> {
self.painted
.iter()
.find(|(t, _)| *t == needle)
.map(|(_, b)| *b)
}
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))
}
fn conformance_line_height(&self) -> f32 {
self.line_height
}
}
#[test]
fn driver_input_press_family_dispatches_expected_key_events() {
let mut d = FakeDriver::new();
d.press(Key::Char('a'));
d.type_char('b');
d.press_named(NamedKey::Enter);
d.ctrl_char('c');
assert_eq!(
d.events,
vec![
UiEvent::KeyPressed {
key: Key::Char('a'),
modifiers: Modifiers::default(),
repeat: false,
},
UiEvent::KeyPressed {
key: Key::Char('b'),
modifiers: Modifiers::default(),
repeat: false,
},
UiEvent::KeyPressed {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
repeat: false,
},
UiEvent::KeyPressed {
key: Key::Char('c'),
modifiers: Modifiers {
ctrl: true,
..Modifiers::default()
},
repeat: false,
},
]
);
}
#[test]
fn driver_input_click_is_a_bare_mouse_down_by_default() {
let mut d = FakeDriver::new();
d.click(3.0, 4.0);
assert_eq!(
d.events,
vec![UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: Point::new(3.0, 4.0),
modifiers: Modifiers::default(),
}],
"the default `click` is a bare press-down — WinDriver overrides \
this to add a release, everyone else keeps the default"
);
}
#[test]
fn driver_input_drag_is_down_then_move_then_up() {
let mut d = FakeDriver::new();
d.drag(1.0, 2.0, 5.0, 6.0);
assert_eq!(d.events.len(), 3);
assert!(matches!(d.events[0], UiEvent::MouseDown { .. }));
assert!(matches!(d.events[1], UiEvent::MouseMoved { .. }));
assert!(matches!(d.events[2], UiEvent::MouseUp { .. }));
}
#[test]
fn pixel_click_conformance_click_text_at_resolves_each_anchor() {
let mut d = FakeDriver::new();
PixelClickConformance::click_text_at(&mut d, "Toggle", Anchor::Center);
PixelClickConformance::click_text_at(&mut d, "Toggle", Anchor::LeftEdge);
PixelClickConformance::click_text_at(&mut d, "Toggle", Anchor::RightEdge);
let positions: Vec<(f32, f32)> = d
.events
.iter()
.map(|e| match e {
UiEvent::MouseDown { position, .. } => (position.x, position.y),
other => panic!("expected MouseDown, got {other:?}"),
})
.collect();
assert_eq!(positions, vec![(30.0, 24.0), (11.0, 24.0), (49.0, 24.0)]);
}
#[test]
#[should_panic(expected = "FakeDriver: \"nope\" not painted")]
fn pixel_click_conformance_click_text_at_panics_naming_the_driver_when_not_painted() {
let mut d = FakeDriver::new();
PixelClickConformance::click_text_at(&mut d, "nope", Anchor::Center);
}
#[test]
fn pixel_click_conformance_scroll_at_dispatches_scroll_scaled_by_line_height() {
let mut d = FakeDriver::new();
PixelClickConformance::scroll_at(&mut d, "Toggle", 2);
assert_eq!(d.events.len(), 1);
match &d.events[0] {
UiEvent::Scroll { delta, .. } => assert_eq!(delta.y, 2.0 * 16.0),
other => panic!("expected Scroll, got {other:?}"),
}
}
}