use crate::background::{BackgroundPattern, BackgroundState, PATTERN_COLORS};
use crate::chrome::{self, WindowChrome};
use crate::event::{Event, EventCtx};
use crate::font::{Font, FontSet};
use crate::geometry::{Color, Rect, Size};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::Widget;
pub struct MockBackend {
logical_size: Size,
scale: f32,
theme: Theme,
font: Option<Font>,
serif_font: Option<Font>,
mono_font: Option<Font>,
bg: BackgroundState,
}
#[derive(Clone, Copy)]
enum Backdrop {
Plain,
Pattern,
}
impl MockBackend {
pub fn new(width: i32, height: i32) -> Self {
Self {
logical_size: Size::new(width.max(1), height.max(1)),
scale: 1.0,
theme: Theme::default(),
font: None,
serif_font: None,
mono_font: None,
bg: BackgroundState {
pattern: BackgroundPattern::DiagonalForward,
color: PATTERN_COLORS[0].1,
},
}
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = scale.max(0.01);
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn with_sans_font(mut self, font: Font) -> Self {
self.font = Some(font);
self
}
pub fn with_serif_font(mut self, font: Font) -> Self {
self.serif_font = Some(font);
self
}
pub fn with_mono_font(mut self, font: Font) -> Self {
self.mono_font = Some(font);
self
}
pub fn with_background_pattern(mut self, pattern: BackgroundPattern, color: Color) -> Self {
self.bg = BackgroundState { pattern, color };
self
}
pub fn physical_size(&self) -> Size {
let w = (self.logical_size.w as f32 * self.scale).round().max(1.0) as i32;
let h = (self.logical_size.h as f32 * self.scale).round().max(1.0) as i32;
Size::new(w, h)
}
pub fn dispatch(&self, root: &mut dyn Widget, event: &Event) -> DispatchOutcome {
let mut ctx = EventCtx::new();
root.event(event, &mut ctx);
DispatchOutcome {
paint_requested: ctx.paint_requested,
close_requested: ctx.close_requested,
}
}
pub fn render(&self, root: &mut dyn Widget) -> Snapshot {
self.render_backdrop(root, Backdrop::Plain)
}
fn render_backdrop(&self, root: &mut dyn Widget, backdrop: Backdrop) -> Snapshot {
let physical = self.physical_size();
let logical_w = (physical.w as f32 / self.scale).round().max(1.0) as i32;
let logical_h = (physical.h as f32 / self.scale).round().max(1.0) as i32;
root.layout(Rect::new(0, 0, logical_w, logical_h));
let mut pixels = vec![0u32; (physical.w * physical.h) as usize];
let (origin_x, origin_y) = origin_centered(self.logical_size, self.scale, physical);
{
let mut painter = Painter::with_popup_anchor(
&mut pixels,
physical.w,
physical.h,
self.scale,
origin_x,
origin_y,
FontSet {
sans: self.font.as_ref(),
serif: self.serif_font.as_ref(),
mono: self.mono_font.as_ref(),
},
None,
);
match backdrop {
Backdrop::Plain => painter.fill(self.theme.background),
Backdrop::Pattern => {
painter.fill_pattern(self.theme.background, self.bg.pattern, self.bg.color)
}
}
root.paint(&mut painter, &self.theme);
}
let mut popups = Vec::new();
root.collect_popups(&mut popups);
for req in &popups {
let popup_phys_x = origin_x + (req.rect.x as f32 * self.scale).round() as i32;
let popup_phys_y = origin_y + (req.rect.y as f32 * self.scale).round() as i32;
let popup_phys_w = (req.rect.w as f32 * self.scale).round() as i32;
let popup_phys_h = (req.rect.h as f32 * self.scale).round() as i32;
let mut painter = Painter::with_popup_anchor(
&mut pixels,
physical.w,
physical.h,
self.scale,
origin_x,
origin_y,
FontSet {
sans: self.font.as_ref(),
serif: self.serif_font.as_ref(),
mono: self.mono_font.as_ref(),
},
Some(req.rect),
);
painter.set_clip_phys(popup_phys_x, popup_phys_y, popup_phys_w, popup_phys_h);
root.paint(&mut painter, &self.theme);
painter.clear_clip();
}
Snapshot {
width: physical.w,
height: physical.h,
pixels,
}
}
pub fn render_framed(&self, root: &mut dyn Widget, chrome: &WindowChrome) -> Snapshot {
let backdrop = if chrome.paints_background_pattern() {
Backdrop::Pattern
} else {
Backdrop::Plain
};
let content = self.render_backdrop(root, backdrop);
let content_size = Size::new(content.width, content.height);
let m = chrome::metrics(content_size, self.scale, chrome);
let mut pixels = vec![0u32; (m.buffer.w * m.buffer.h) as usize];
{
let mut painter = Painter::new(
&mut pixels,
m.buffer.w,
m.buffer.h,
1.0,
0,
0,
FontSet {
sans: self.font.as_ref(),
serif: self.serif_font.as_ref(),
mono: self.mono_font.as_ref(),
},
);
chrome::paint(&mut painter, &m, chrome);
}
let cw = content.width as usize;
for row in 0..content.height {
let src = (row * content.width) as usize;
let dst = ((m.content.y + row) * m.buffer.w + m.content.x) as usize;
pixels[dst..dst + cw].copy_from_slice(&content.pixels[src..src + cw]);
}
Snapshot {
width: m.buffer.w,
height: m.buffer.h,
pixels,
}
}
}
fn origin_centered(logical: Size, scale: f32, physical: Size) -> (i32, i32) {
let content_w = (logical.w as f32 * scale).round() as i32;
let content_h = (logical.h as f32 * scale).round() as i32;
let ox = ((physical.w - content_w) / 2).max(0);
let oy = ((physical.h - content_h) / 2).max(0);
(ox, oy)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DispatchOutcome {
pub paint_requested: bool,
pub close_requested: bool,
}
pub struct Snapshot {
width: i32,
height: i32,
pixels: Vec<u32>,
}
impl Snapshot {
pub fn width(&self) -> i32 {
self.width
}
pub fn height(&self) -> i32 {
self.height
}
pub fn pixels(&self) -> &[u32] {
&self.pixels
}
pub fn to_png(&self) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut encoder = png::Encoder::new(&mut buf, self.width as u32, self.height as u32);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().expect("saudade::mock: png header");
let mut rgba = Vec::with_capacity(self.pixels.len() * 4);
for &px in &self.pixels {
let a = ((px >> 24) & 0xFF) as u8;
let r = ((px >> 16) & 0xFF) as u8;
let g = ((px >> 8) & 0xFF) as u8;
let b = (px & 0xFF) as u8;
rgba.extend_from_slice(&[r, g, b, a]);
}
writer
.write_image_data(&rgba)
.expect("saudade::mock: png data");
}
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Container;
#[test]
fn framed_regular_window_paints_pattern_but_dialog_stays_plain() {
let distinct = Color::rgb(0xAB, 0xCD, 0xEF);
let build = || Container::new(40, 30);
let backend =
MockBackend::new(40, 30).with_background_pattern(BackgroundPattern::Solid, distinct);
let census = |snap: &Snapshot| snap.pixels().iter().filter(|&&px| px == distinct.0).count();
let content_px = (backend.physical_size().w * backend.physical_size().h) as usize;
let resizable = backend.render_framed(&mut build(), &WindowChrome::resizable("App"));
let dialog = backend.render_framed(&mut build(), &WindowChrome::dialog("App"));
assert_eq!(census(&resizable), content_px);
assert_eq!(census(&dialog), 0);
}
#[test]
fn framed_uses_live_default_pattern() {
let hatch = PATTERN_COLORS[0].1.0; let build = || Container::new(40, 30);
let backend = MockBackend::new(40, 30);
let has_hatch = |snap: &Snapshot| snap.pixels().contains(&hatch);
let resizable = backend.render_framed(&mut build(), &WindowChrome::resizable("App"));
let dialog = backend.render_framed(&mut build(), &WindowChrome::dialog("App"));
assert!(
has_hatch(&resizable),
"regular window should show the default hatch"
);
assert!(!has_hatch(&dialog), "dialog must stay plain");
}
#[test]
fn bare_render_stays_plain() {
let distinct = Color::rgb(0xAB, 0xCD, 0xEF);
let mut root = Container::new(40, 30);
let snap = MockBackend::new(40, 30)
.with_background_pattern(BackgroundPattern::Solid, distinct)
.render(&mut root);
assert!(snap.pixels().iter().all(|&px| px != distinct.0));
}
}