#[cfg(target_os = "windows")]
use windows::core::Result as WinResult;
#[cfg(target_os = "windows")]
use windows::Win32::Foundation::RECT;
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct2D::Common::{
D2D1_ALPHA_MODE_IGNORE, D2D1_COLOR_F, D2D1_PIXEL_FORMAT, D2D_RECT_F,
};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct2D::{
D2D1CreateFactory, ID2D1DCRenderTarget, ID2D1Factory, D2D1_FACTORY_TYPE_SINGLE_THREADED,
D2D1_RENDER_TARGET_PROPERTIES, D2D1_RENDER_TARGET_TYPE_SOFTWARE,
};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_B8G8R8A8_UNORM;
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Gdi::{
CreateCompatibleDC, CreateDIBSection, DeleteDC, DeleteObject, SelectObject, BITMAPINFO,
BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP, HDC,
};
#[cfg(not(target_os = "windows"))]
pub type WinResult<T> = Result<T, HeadlessSurfaceUnavailable>;
#[cfg(not(target_os = "windows"))]
#[derive(Debug)]
pub struct HeadlessSurfaceUnavailable;
#[cfg(not(target_os = "windows"))]
impl std::fmt::Display for HeadlessSurfaceUnavailable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"HeadlessSurface: Direct2D is only available on target_os = \"windows\""
)
}
}
#[cfg(not(target_os = "windows"))]
impl std::error::Error for HeadlessSurfaceUnavailable {}
use crate::event::{Rect, Viewport};
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, Color, Key, Modifiers, MouseButton, NamedKey, Point, UiEvent};
use super::backend::WinBackend;
use super::run::{
dispatch_event, render_frame, route_mouse_down, route_mouse_move, route_mouse_up, EventOutcome,
};
pub struct HeadlessSurface {
#[cfg(target_os = "windows")]
#[allow(dead_code)]
factory: ID2D1Factory,
#[cfg(target_os = "windows")]
target: ID2D1DCRenderTarget,
#[cfg(target_os = "windows")]
hdc: HDC,
#[cfg(target_os = "windows")]
bitmap: HBITMAP,
#[cfg(target_os = "windows")]
bits: *mut u8,
width: u32,
height: u32,
}
impl HeadlessSurface {
pub fn new(width: u32, height: u32) -> WinResult<Self> {
#[cfg(target_os = "windows")]
{
let width = width.max(1);
let height = height.max(1);
let factory: ID2D1Factory =
unsafe { D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, None)? };
let render_props = D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_SOFTWARE,
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_IGNORE,
},
..Default::default()
};
let target = unsafe { factory.CreateDCRenderTarget(&render_props)? };
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width as i32,
biHeight: -(height as i32),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let hdc = unsafe { CreateCompatibleDC(None) };
let mut bits: *mut core::ffi::c_void = std::ptr::null_mut();
let bitmap = match unsafe {
CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0)
} {
Ok(bitmap) => bitmap,
Err(err) => {
unsafe {
let _ = DeleteDC(hdc);
}
return Err(err);
}
};
unsafe { SelectObject(hdc, bitmap.into()) };
let rect = RECT {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
};
if let Err(err) = unsafe { target.BindDC(hdc, &rect) } {
unsafe {
let _ = DeleteObject(bitmap.into());
let _ = DeleteDC(hdc);
}
return Err(err);
}
Ok(Self {
factory,
target,
hdc,
bitmap,
bits: bits as *mut u8,
width,
height,
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = (width, height);
Err(HeadlessSurfaceUnavailable)
}
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
#[cfg(target_os = "windows")]
pub fn target(&self) -> &ID2D1DCRenderTarget {
&self.target
}
#[cfg(target_os = "windows")]
pub fn paint(&self, paint: impl FnOnce(&ID2D1DCRenderTarget)) -> WinResult<()> {
unsafe { self.target.BeginDraw() };
paint(&self.target);
unsafe { self.target.EndDraw(None, None) }
}
pub fn fill_rect(&self, rect: Rect, color: Color) -> WinResult<()> {
#[cfg(target_os = "windows")]
{
self.paint(|target| {
let brush =
match unsafe { target.CreateSolidColorBrush(&color_to_d2d(color), None) } {
Ok(brush) => brush,
Err(_) => return,
};
let rect_f = D2D_RECT_F {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height,
};
unsafe { target.FillRectangle(&rect_f, &brush) };
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = (rect, color);
Err(HeadlessSurfaceUnavailable)
}
}
pub fn pixel_at(&self, x: u32, y: u32) -> Color {
#[cfg(target_os = "windows")]
{
assert!(
x < self.width && y < self.height,
"pixel_at({x}, {y}) out of bounds for a {}x{} surface",
self.width,
self.height
);
let offset = (y as isize * self.width as isize + x as isize) * 4;
unsafe {
let px = self.bits.offset(offset);
let b = *px;
let g = *px.add(1);
let r = *px.add(2);
Color::rgb(r, g, b)
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = (x, y);
Color::rgb(0, 0, 0)
}
}
pub fn stride(&self) -> u32 {
self.width * 4
}
pub fn buffer(&self) -> Vec<u8> {
let len = self.stride() as usize * self.height as usize;
#[cfg(target_os = "windows")]
{
unsafe { std::slice::from_raw_parts(self.bits, len).to_vec() }
}
#[cfg(not(target_os = "windows"))]
{
let mut buf = vec![0u8; len];
for px in buf.chunks_exact_mut(4) {
px[3] = 255;
}
buf
}
}
}
impl Drop for HeadlessSurface {
fn drop(&mut self) {
#[cfg(target_os = "windows")]
unsafe {
let _ = DeleteObject(self.bitmap.into());
let _ = DeleteDC(self.hdc);
}
}
}
#[cfg(target_os = "windows")]
fn color_to_d2d(color: Color) -> D2D1_COLOR_F {
D2D1_COLOR_F {
r: color.r as f32 / 255.0,
g: color.g as f32 / 255.0,
b: color.b as f32 / 255.0,
a: color.a as f32 / 255.0,
}
}
pub fn driver_with_shell<A: ShellApp + 'static>(
app: A,
config: ShellConfig,
width: u32,
height: u32,
) -> WinDriver<impl AppLogic> {
let adapter = crate::shell_adapter::build_shell_adapter(app, config);
WinDriver::new(adapter, width, height)
}
pub struct WinDriver<A: AppLogic> {
core: DriverCore<WinBackend, A>,
surface: HeadlessSurface,
width: u32,
height: u32,
}
impl<A: AppLogic> WinDriver<A> {
pub fn new(app: A, width: u32, height: u32) -> Self {
let surface = HeadlessSurface::new(width, height)
.expect("WinDriver::new: create offscreen Direct2D DC render target");
let mut backend = WinBackend::new();
backend.set_painted_text_recording(true);
#[cfg(target_os = "windows")]
backend
.attach_headless(surface.target().clone(), width, height)
.expect("WinDriver::new: attach headless surface to WinBackend");
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) {
#[cfg(target_os = "windows")]
let _ = self.core.backend_mut().ensure_surface();
let viewport = Viewport::new(self.width as f32, self.height as f32, 1.0);
let (backend, app) = self.core.parts_mut();
render_frame(backend, app, viewport);
}
pub fn dispatch(&mut self, event: UiEvent) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let outcome = {
let (backend, app) = self.core.parts_mut();
dispatch_event(event, backend, app)
};
self.apply_outcome(outcome)
}
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)
};
self.apply_outcome(reaction.into())
}
fn apply_outcome(&mut self, outcome: EventOutcome) -> Reaction {
let viewport = Viewport::new(self.width as f32, self.height as f32, 1.0);
self.core.apply_outcome(outcome, |backend, app| {
#[cfg(target_os = "windows")]
let _ = backend.ensure_surface();
render_frame(backend, app, viewport);
})
}
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 drag(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) -> Reaction {
DriverInput::drag(self, x0, y0, x1, y1)
}
pub fn mouse_down(&mut self, x: f32, y: f32) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let outcome = {
let (backend, app) = self.core.parts_mut();
route_mouse_down(
backend,
app,
Point::new(x, y),
MouseButton::Left,
Modifiers::default(),
)
};
self.apply_outcome(outcome)
}
pub fn mouse_move(&mut self, x: f32, y: f32) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let outcome = {
let (backend, app) = self.core.parts_mut();
route_mouse_move(
backend,
app,
Point::new(x, y),
ButtonMask {
left: true,
..ButtonMask::default()
},
)
};
self.apply_outcome(outcome)
}
pub fn mouse_up(&mut self, x: f32, y: f32) -> Reaction {
if self.core.exited() {
return Reaction::Exit;
}
let outcome = {
let (backend, app) = self.core.parts_mut();
route_mouse_up(backend, app, Point::new(x, y), MouseButton::Left)
};
self.apply_outcome(outcome)
}
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) -> &WinBackend {
self.core.backend()
}
pub fn backend_mut(&mut self) -> &mut WinBackend {
self.core.backend_mut()
}
pub fn drop_surface(&mut self) {
#[cfg(target_os = "windows")]
self.core.backend_mut().drop_surface_for_testing();
}
pub fn surface(&self) -> &HeadlessSurface {
&self.surface
}
pub fn pixel(&self, x: u32, y: u32) -> Color {
self.surface.pixel_at(x, y)
}
pub fn screen(&self) -> Vec<u8> {
self.surface.buffer()
}
pub fn stride(&self) -> u32 {
self.surface.stride()
}
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<Rect> {
self.core.find_bounds(needle)
}
pub fn find(&self, needle: &str) -> Option<(f32, f32)> {
self.core.find(needle)
}
}
impl<A: AppLogic> DriverInput for WinDriver<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)
}
fn click(&mut self, x: f32, y: f32) -> Reaction {
self.mouse_down(x, y);
self.mouse_up(x, y)
}
}
impl<A: AppLogic> PixelClickConformance for WinDriver<A> {
const NAME: &'static str = "WinDriver";
fn find_bounds(&self, needle: &str) -> Option<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 WinDriver<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;
WinDriver::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) {
WinDriver::press_named(self, key);
}
fn type_char(&mut self, c: char) {
WinDriver::type_char(self, c);
}
fn ctrl_char(&mut self, c: char) {
WinDriver::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.clone(),
}
}
fn screen_has(&self, needle: &str) -> bool {
self.screen_contains(needle)
}
fn exited(&self) -> bool {
WinDriver::exited(self)
}
}
#[cfg(test)]
mod tests {
#[cfg_attr(not(target_os = "windows"), allow(unused_imports))]
use super::*;
#[cfg(target_os = "windows")]
use std::time::Duration;
#[cfg(target_os = "windows")]
#[test]
fn fills_a_solid_rect_and_reads_the_pixel_back() {
let surface = HeadlessSurface::new(64, 64).expect("create headless surface");
surface
.fill_rect(Rect::new(0.0, 0.0, 64.0, 64.0), Color::rgb(200, 40, 40))
.expect("fill rect");
let center = surface.pixel_at(32, 32);
assert_eq!((center.r, center.g, center.b), (200, 40, 40));
}
#[cfg(target_os = "windows")]
#[test]
fn buffer_matches_pixel_at_and_stride_is_width_times_four() {
let surface = HeadlessSurface::new(16, 8).expect("create headless surface");
surface
.fill_rect(Rect::new(0.0, 0.0, 16.0, 8.0), Color::rgb(10, 20, 30))
.expect("fill rect");
assert_eq!(surface.stride(), 16 * 4);
let buf = surface.buffer();
assert_eq!(buf.len(), surface.stride() as usize * 8);
let off = 2 * surface.stride() as usize + 3 * 4;
let (b, g, r, a) = (buf[off], buf[off + 1], buf[off + 2], buf[off + 3]);
let expected = surface.pixel_at(3, 2);
assert_eq!((r, g, b), (expected.r, expected.g, expected.b));
assert_eq!(a, 255);
}
#[cfg(target_os = "windows")]
#[test]
fn a_rect_that_does_not_cover_the_whole_surface_leaves_the_rest_cleared() {
let surface = HeadlessSurface::new(32, 32).expect("create headless surface");
surface
.paint(|target| unsafe {
target.Clear(Some(&D2D1_COLOR_F {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}));
})
.expect("clear");
surface
.fill_rect(Rect::new(0.0, 0.0, 8.0, 8.0), Color::rgb(0, 255, 0))
.expect("fill rect");
let inside = surface.pixel_at(2, 2);
let outside = surface.pixel_at(20, 20);
assert_eq!((inside.r, inside.g, inside.b), (0, 255, 0));
assert_eq!((outside.r, outside.g, outside.b), (0, 0, 0));
}
#[cfg(target_os = "windows")]
struct StatusBarApp;
#[cfg(target_os = "windows")]
impl AppLogic for StatusBarApp {
type AreaId = ();
fn render(&self, backend: &mut dyn crate::Backend, _area: ()) {
backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, 200.0, 20.0),
&crate::StatusBar {
id: crate::WidgetId::new("status"),
left_segments: vec![crate::StatusBarSegment {
text: "NORMAL".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 20, 30),
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn crate::Backend) -> Reaction {
Reaction::Continue
}
}
#[cfg(target_os = "windows")]
#[test]
fn find_locates_a_status_bar_segment_after_paint() {
let driver = WinDriver::new(StatusBarApp, 200, 20);
assert!(
driver.find("NORMAL").is_some(),
"WinDriver::find should locate the painted status-bar segment: {:?}",
driver.painted_texts()
);
assert!(driver.screen_contains("NORMAL"));
let inventory = ConformanceDriver::inventory(&driver);
assert!(
!inventory.text_runs().is_empty(),
"inventory().text_runs() should be non-empty after a status-bar paint"
);
assert!(inventory.screen_has("NORMAL"));
}
#[cfg(target_os = "windows")]
#[test]
fn end_frame_recovers_the_next_frame_after_a_forced_end_draw_failure() {
use windows::Win32::Graphics::Direct2D::Common::D2D_SIZE_U;
use windows::Win32::Graphics::Direct2D::{
ID2D1Bitmap, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, D2D1_BITMAP_PROPERTIES,
};
const W: u32 = 32;
const H: u32 = 32;
let divider_color = Color::rgb(0x22, 0x66, 0xaa);
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let paint_divider = |backend: &mut WinBackend| {
use crate::Backend;
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.set_theme(crate::theme::Theme {
separator: divider_color,
..crate::theme::Theme::default()
});
backend.draw_terminal_divider(Rect::new(0.0, 0.0, W as f32, H as f32));
backend.end_frame();
};
paint_divider(&mut backend);
assert_eq!(
(
surface.pixel_at(0, H / 2).r,
surface.pixel_at(0, H / 2).g,
surface.pixel_at(0, H / 2).b,
),
(divider_color.r, divider_color.g, divider_color.b),
"sanity: frame 1 must paint the divider before we force anything to fail"
);
let foreign_surface = HeadlessSurface::new(4, 4).expect("create foreign headless surface");
let bitmap_props = D2D1_BITMAP_PROPERTIES {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_IGNORE,
},
dpiX: 96.0,
dpiY: 96.0,
};
let foreign_bitmap: ID2D1Bitmap = unsafe {
foreign_surface
.target()
.CreateBitmap(
D2D_SIZE_U {
width: 1,
height: 1,
},
None,
0,
&bitmap_props,
)
.expect("create foreign-domain bitmap")
};
{
use crate::Backend;
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
unsafe {
surface.target().DrawBitmap(
&foreign_bitmap,
None,
1.0,
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
None,
);
}
backend.end_frame();
assert_eq!(
backend.last_error(),
Some(crate::backend::BackendError::SurfaceLost),
"end_frame must report SurfaceLost once EndDraw genuinely fails"
);
assert_eq!(
backend.last_error(),
None,
"last_error() must clear on read — a second call must not repeat the same error"
);
}
{
let _ = backend.ensure_surface();
paint_divider(&mut backend);
}
assert_eq!(
(
surface.pixel_at(0, H / 2).r,
surface.pixel_at(0, H / 2).g,
surface.pixel_at(0, H / 2).b,
),
(divider_color.r, divider_color.g, divider_color.b),
"frame 3 (after recovery) must still paint the divider — the render target must have \
been rebuilt by ensure_surface, not left permanently dropped"
);
}
#[cfg(target_os = "windows")]
#[test]
fn render_frame_survives_a_panicking_app_render_and_the_next_frame_still_paints() {
const W: u32 = 32;
const H: u32 = 32;
let divider_color = Color::rgb(0x22, 0x66, 0xaa);
struct PanicsOnceThenPaints {
panicked_yet: std::cell::Cell<bool>,
divider_color: Color,
}
impl AppLogic for PanicsOnceThenPaints {
type AreaId = ();
fn render(&self, backend: &mut dyn crate::Backend, _area: ()) {
if !self.panicked_yet.replace(true) {
panic!("PanicsOnceThenPaints: deliberate panic for #922 coverage");
}
backend.set_theme(crate::theme::Theme {
separator: self.divider_color,
..crate::theme::Theme::default()
});
backend.draw_terminal_divider(Rect::new(0.0, 0.0, W as f32, H as f32));
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn crate::Backend) -> Reaction {
Reaction::Continue
}
}
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let app = PanicsOnceThenPaints {
panicked_yet: std::cell::Cell::new(false),
divider_color,
};
let viewport = Viewport::new(W as f32, H as f32, 1.0);
render_frame(&mut backend, &app, viewport);
let _ = backend.ensure_surface();
render_frame(&mut backend, &app, viewport);
assert_eq!(
(
surface.pixel_at(0, H / 2).r,
surface.pixel_at(0, H / 2).g,
surface.pixel_at(0, H / 2).b,
),
(divider_color.r, divider_color.g, divider_color.b),
"frame 2 (after a caught panic in frame 1) must still paint — the \
BeginDraw/EndDraw bracket must have stayed balanced despite the panic"
);
}
#[cfg(target_os = "windows")]
#[test]
fn drop_surface_reaches_the_same_unpainted_fallback_a_real_end_draw_failure_does() {
use crate::Backend;
struct NoopApp;
impl AppLogic for NoopApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn crate::Backend, _area: ()) {}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn crate::Backend) -> Reaction {
Reaction::Continue
}
}
let minimap = crate::primitives::minimap::Minimap {
id: crate::types::WidgetId::new("minimap"),
lines: (0..10)
.map(|i| crate::primitives::minimap::MinimapLine {
text: format!("line {i}"),
line_idx: i,
})
.collect(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 5,
total_buffer_lines: 10,
};
let rect = Rect::new(0.0, 0.0, 20.0, 64.0);
let mut driver = WinDriver::new(NoopApp, 64, 64);
let viewport = Viewport::new(64.0, 64.0, 1.0);
driver.backend_mut().begin_frame(viewport);
let before = driver.backend_mut().draw_minimap(rect, &minimap);
driver.backend_mut().end_frame();
assert!(
before.painted,
"sanity: a freshly-built WinDriver has a real surface attached"
);
driver.drop_surface();
driver.backend_mut().begin_frame(viewport);
let after = driver.backend_mut().draw_minimap(rect, &minimap);
driver.backend_mut().end_frame();
assert!(
!after.painted,
"drop_surface must reach the same `painted: false` fallback a \
real EndDraw failure leaves behind"
);
assert_eq!(
after.layout, before.layout,
"the fallback layout must still agree with the real paint's \
layout — same contract \
draw_minimap_with_no_surface_reports_unpainted_and_agrees_with_minimap_layout \
proves in-crate"
);
driver.render();
driver.backend_mut().begin_frame(viewport);
let recovered = driver.backend_mut().draw_minimap(rect, &minimap);
driver.backend_mut().end_frame();
assert!(
recovered.painted,
"WinDriver::render must recover the dropped surface via \
ensure_surface, same as a real EndDraw-failure recovery"
);
}
#[cfg(target_os = "windows")]
struct TickCountingApp {
ticks: std::cell::Cell<u32>,
}
#[cfg(target_os = "windows")]
impl AppLogic for TickCountingApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn crate::Backend, _area: ()) {}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn crate::Backend) -> Reaction {
Reaction::Continue
}
fn tick(&mut self, _backend: &mut dyn crate::Backend) -> Reaction {
let n = self.ticks.get() + 1;
self.ticks.set(n);
if n == 1 {
Reaction::RedrawAfter(Duration::from_millis(100))
} else {
Reaction::Continue
}
}
}
#[cfg(target_os = "windows")]
#[test]
fn tick_calls_applogic_tick_and_applies_its_reaction() {
let mut driver = WinDriver::new(
TickCountingApp {
ticks: std::cell::Cell::new(0),
},
64,
64,
);
let reaction = driver.tick();
assert_eq!(
driver.app().ticks.get(),
1,
"tick() must call AppLogic::tick"
);
assert_eq!(reaction, Reaction::RedrawAfter(Duration::from_millis(100)));
let reaction = driver.tick();
assert_eq!(driver.app().ticks.get(), 2);
assert_eq!(reaction, Reaction::Continue);
}
}