use super::*;
#[test]
fn test_window_layout_never_mutates_bounds() {
use crate::text::Font;
use crate::widgets::window::Window;
use crate::Label;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let content: Box<dyn crate::widget::Widget> =
Box::new(Label::new("content", Arc::clone(&font)));
let saved = crate::geometry::Rect::new(50.0, 800.0, 400.0, 200.0);
let mut win = Window::new("Test", Arc::clone(&font), content).with_bounds(saved);
let sizes = [
(800.0, 600.0), (1920.0, 1017.0), (800.0, 600.0), (1920.0, 1017.0), ];
for (w, h) in sizes {
let _ =
<Window as crate::widget::Widget>::layout(&mut win, crate::geometry::Size::new(w, h));
assert_eq!(
win.bounds().y,
800.0,
"layout({w}, {h}) mutated bounds.y to {} — auto-save would \
now persist the mutated position, corrupting saved state",
win.bounds().y,
);
assert_eq!(win.bounds().x, 50.0);
}
}
#[test]
fn test_window_middle_drag_title_moves_for_touch_scroll_bridge() {
use crate::text::Font;
use crate::widgets::{primitives::Stack, window::Window};
use crate::{App, Label, Modifiers, MouseButton};
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let win = Window::new(
"Touch Movable",
Arc::clone(&font),
Box::new(Label::new("content", Arc::clone(&font))),
)
.with_bounds(crate::geometry::Rect::new(100.0, 100.0, 240.0, 140.0));
let mut app = App::new(Box::new(Stack::new().add(Box::new(win))));
let viewport = crate::geometry::Size::new(640.0, 480.0);
app.layout(viewport);
let start_x = 140.0;
let start_y_up = 100.0 + 140.0 - 12.0;
let start_y_down = viewport.height - start_y_up;
app.on_mouse_down(
start_x,
start_y_down,
MouseButton::Middle,
Modifiers::default(),
);
app.on_mouse_move(start_x + 30.0, start_y_down - 20.0);
app.on_mouse_up(
start_x + 30.0,
start_y_down - 20.0,
MouseButton::Middle,
Modifiers::default(),
);
app.layout(viewport);
let moved = crate::find_widget_by_id(app.root(), "Touch Movable")
.expect("window remains in tree")
.bounds();
assert_eq!(moved.x, 130.0);
assert_eq!(moved.y, 120.0);
}
#[test]
fn test_window_move_drag_requests_draw_without_invalidation() {
use crate::text::Font;
use crate::widgets::{primitives::Stack, window::Window};
use crate::{App, Label, Modifiers, MouseButton};
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let win = Window::new(
"Retained Move",
Arc::clone(&font),
Box::new(Label::new("content", Arc::clone(&font))),
)
.with_bounds(crate::geometry::Rect::new(100.0, 100.0, 240.0, 140.0));
let mut app = App::new(Box::new(Stack::new().add(Box::new(win))));
let viewport = crate::geometry::Size::new(640.0, 480.0);
app.layout(viewport);
let start_x = 140.0;
let start_y_up = 100.0 + 140.0 - 12.0;
let start_y_down = viewport.height - start_y_up;
app.on_mouse_down(
start_x,
start_y_down,
MouseButton::Left,
Modifiers::default(),
);
crate::animation::clear_draw_request();
let epoch = crate::animation::invalidation_epoch();
app.on_mouse_move(start_x + 30.0, start_y_down - 20.0);
assert_eq!(
crate::animation::invalidation_epoch(),
epoch,
"moving a retained window should translate its cached layer, not invalidate it"
);
assert!(
crate::animation::wants_draw(),
"moving a retained window still needs a frame at the new position"
);
let moved = crate::find_widget_by_id(app.root(), "Retained Move")
.expect("window remains in tree")
.bounds();
assert_eq!(moved.x, 130.0);
assert_eq!(moved.y, 120.0);
}
#[test]
fn test_sidebar_toggle_reorders_stack_to_end() {
use crate::widgets::{primitives::Stack, window::Window};
use crate::{
geometry::{Rect, Size},
text::Font,
Label, Widget,
};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let a_visible = Rc::new(Cell::new(true));
let b_visible = Rc::new(Cell::new(true));
let c_visible = Rc::new(Cell::new(true));
let make = |x: f64, vis: Rc<Cell<bool>>| -> Box<dyn Widget> {
Box::new(
Window::new(
"W",
Arc::clone(&font),
Box::new(Label::new("x", Arc::clone(&font))),
)
.with_bounds(Rect::new(x, 0.0, 200.0, 120.0))
.with_visible_cell(vis),
)
};
let mut stack: Box<dyn Widget> = Box::new(
Stack::new()
.add(make(100.0, Rc::clone(&a_visible)))
.add(make(200.0, Rc::clone(&b_visible)))
.add(make(300.0, Rc::clone(&c_visible))),
);
let _ = stack.layout(Size::new(1024.0, 768.0));
b_visible.set(false);
let _ = stack.layout(Size::new(1024.0, 768.0));
b_visible.set(true);
let _ = stack.layout(Size::new(1024.0, 768.0));
let last_x = stack.children()[2].bounds().x;
assert_eq!(
last_x, 200.0,
"after sidebar-toggle-on of B (x=200), B must be at the END of \
Stack.children (got child with bounds.x={last_x} at index 2)"
);
let first_x = stack.children()[0].bounds().x;
assert_eq!(first_x, 100.0, "A preserved at index 0");
let mid_x = stack.children()[1].bounds().x;
assert_eq!(mid_x, 300.0, "C preserved at index 1");
}
#[test]
fn test_raise_takes_effect_same_frame_as_visibility_toggle() {
use crate::widgets::{primitives::Stack, window::Window};
use crate::{
geometry::{Rect, Size},
text::Font,
Label, Widget,
};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let a_visible = Rc::new(Cell::new(false)); let b_visible = Rc::new(Cell::new(true));
let make = |vis: Rc<Cell<bool>>| -> Box<dyn Widget> {
Box::new(
Window::new(
"W",
Arc::clone(&font),
Box::new(Label::new("x", Arc::clone(&font))),
)
.with_bounds(Rect::new(0.0, 0.0, 200.0, 120.0))
.with_visible_cell(vis),
)
};
let mut stack: Box<dyn Widget> = Box::new(
Stack::new()
.add(make(Rc::clone(&a_visible))) .add(make(Rc::clone(&b_visible))), );
let _ = stack.layout(Size::new(1024.0, 768.0));
a_visible.set(true);
let _ = stack.layout(Size::new(1024.0, 768.0));
assert!(
!stack.children_mut()[0].take_raise_request(),
"child 0 still has a pending raise — Stack drain ran before \
Window.layout set the flag; sidebar-opened windows will paint \
in the back for one frame"
);
assert!(
!stack.children_mut()[1].take_raise_request(),
"child 1 still has a pending raise — same bug"
);
}
#[test]
fn test_window_raises_on_visibility_rising_edge() {
use crate::widgets::{primitives::Stack, window::Window};
use crate::{
geometry::{Rect, Size},
text::Font,
Label, Widget,
};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let a_visible = Rc::new(Cell::new(true));
let b_visible = Rc::new(Cell::new(true));
let make = |title: &str, vis: Rc<Cell<bool>>| -> Box<dyn Widget> {
Box::new(
Window::new(
title,
Arc::clone(&font),
Box::new(Label::new("x", Arc::clone(&font))),
)
.with_bounds(Rect::new(0.0, 0.0, 200.0, 120.0))
.with_visible_cell(vis),
)
};
let mut stack: Box<dyn Widget> = Box::new(
Stack::new()
.add(make("A", Rc::clone(&a_visible)))
.add(make("B", Rc::clone(&b_visible))),
);
let _ = stack.layout(Size::new(1024.0, 768.0));
assert_eq!(stack.children()[0].type_name(), "Window");
assert_eq!(stack.children()[1].type_name(), "Window");
a_visible.set(false);
let _ = stack.layout(Size::new(1024.0, 768.0)); a_visible.set(true);
let _ = stack.layout(Size::new(1024.0, 768.0)); b_visible.set(false);
let _ = stack.layout(Size::new(1024.0, 768.0)); b_visible.set(true);
let _ = stack.layout(Size::new(1024.0, 768.0));
assert!(
!stack.children_mut()[0].take_raise_request(),
"first child still has a pending raise — Stack didn't consume it"
);
assert!(
!stack.children_mut()[1].take_raise_request(),
"second child still has a pending raise — Stack didn't consume it"
);
a_visible.set(false);
let _ = stack.layout(Size::new(1024.0, 768.0));
a_visible.set(true);
let raise_flags_before: Vec<bool> = (0..stack.children().len())
.map(|i| {
let _ = i;
false
})
.collect();
let _ = raise_flags_before;
let _ = stack.layout(Size::new(1024.0, 768.0));
assert!(!stack.children_mut()[0].take_raise_request());
assert!(!stack.children_mut()[1].take_raise_request());
}
#[test]
fn test_paint_subtree_snaps_ctm_for_manual_translate_entry() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::Rect;
use crate::widget::{paint_subtree, Widget};
use agg_rust::trans_affine::TransAffine;
use std::cell::Cell;
use std::rc::Rc;
struct CtmProbe {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
captured: Rc<Cell<Option<TransAffine>>>,
}
impl Widget for CtmProbe {
fn type_name(&self) -> &'static str {
"CtmProbe"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
Size::new(
self.bounds.width.min(available.width),
self.bounds.height.min(available.height),
)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
self.captured.set(Some(ctx.transform()));
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
}
let captured = Rc::new(Cell::new(None));
let mut probe = CtmProbe {
bounds: Rect::new(0.0, 0.0, 10.0, 10.0),
children: Vec::new(),
captured: Rc::clone(&captured),
};
let mut fb = Framebuffer::new(100, 100);
let mut ctx = GfxCtx::new(&mut fb);
ctx.translate(100.3, 50.7);
paint_subtree(&mut probe, &mut ctx);
let ctm = captured.get().expect("probe must have been painted");
assert_eq!(
ctm.tx.fract(),
0.0,
"tx still fractional at paint() entry: {} — paint_subtree snap regressed",
ctm.tx,
);
assert_eq!(
ctm.ty.fract(),
0.0,
"ty still fractional at paint() entry: {} — paint_subtree snap regressed",
ctm.ty,
);
assert_eq!(ctm.tx, 100.0);
assert_eq!(ctm.ty, 50.0);
}
#[test]
fn test_paint_subtree_preserves_fractional_ctm_when_opted_out() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::Rect;
use crate::widget::{paint_subtree, Widget};
use agg_rust::trans_affine::TransAffine;
use std::cell::Cell;
use std::rc::Rc;
struct SubpixelProbe {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
captured: Rc<Cell<Option<TransAffine>>>,
}
impl Widget for SubpixelProbe {
fn type_name(&self) -> &'static str {
"SubpixelProbe"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
Size::new(
self.bounds.width.min(available.width),
self.bounds.height.min(available.height),
)
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
self.captured.set(Some(ctx.transform()));
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
fn enforce_integer_bounds(&self) -> bool {
false
} }
let captured = Rc::new(Cell::new(None));
let mut probe = SubpixelProbe {
bounds: Rect::new(0.0, 0.0, 10.0, 10.0),
children: Vec::new(),
captured: Rc::clone(&captured),
};
let mut fb = Framebuffer::new(100, 100);
let mut ctx = GfxCtx::new(&mut fb);
ctx.translate(100.3, 50.7);
paint_subtree(&mut probe, &mut ctx);
let ctm = captured.get().expect("probe must have been painted");
assert!(
(ctm.tx - 100.3).abs() < 1e-9,
"opt-out widget had tx snapped: {}",
ctm.tx
);
assert!(
(ctm.ty - 50.7).abs() < 1e-9,
"opt-out widget had ty snapped: {}",
ctm.ty
);
}
#[test]
fn test_agg_rasters_1px_stripes_with_zero_gray() {
use crate::framebuffer::unpremultiply_rgba_inplace;
let w = 96_u32;
let h = 96_u32;
let mut fb = Framebuffer::new(w, h);
{
let mut gfx = GfxCtx::new(&mut fb);
for i in 0..(w as usize / 2) {
let x = (2 * i) as f64;
gfx.set_fill_color(Color::white());
gfx.begin_path();
gfx.rect(x, 0.0, 1.0, h as f64);
gfx.fill();
gfx.set_fill_color(Color::black());
gfx.begin_path();
gfx.rect(x + 1.0, 0.0, 1.0, h as f64);
gfx.fill();
}
}
let mut pixels = fb.pixels_flipped();
unpremultiply_rgba_inplace(&mut pixels);
let row_bytes = (w * 4) as usize;
for y in 0..h as usize {
for x in 0..w as usize {
let off = y * row_bytes + x * 4;
let px = &pixels[off..off + 4];
let expected_white = x % 2 == 0;
let (er, eg, eb) = if expected_white {
(255, 255, 255)
} else {
(0, 0, 0)
};
assert_eq!(
(px[0], px[1], px[2], px[3]),
(er, eg, eb, 255),
"pixel ({x}, {y}) should be {} but is {:?}",
if expected_white { "white" } else { "black" },
px,
);
}
}
}
#[test]
fn test_snap_to_pixel_zeros_fractional_translation() {
use crate::draw_ctx::DrawCtx;
let mut fb = Framebuffer::new(10, 10);
let mut ctx = GfxCtx::new(&mut fb);
ctx.translate(100.3, 50.7);
let before = ctx.transform();
assert!((before.tx - 100.3).abs() < 1e-9);
assert!((before.ty - 50.7).abs() < 1e-9);
ctx.snap_to_pixel();
let after = ctx.transform();
assert_eq!(after.tx.fract(), 0.0, "tx still fractional: {}", after.tx);
assert_eq!(after.ty.fract(), 0.0, "ty still fractional: {}", after.ty);
assert_eq!(after.tx, 100.0);
assert_eq!(after.ty, 50.0);
let mut fb2 = Framebuffer::new(10, 10);
let mut ctx2 = GfxCtx::new(&mut fb2);
ctx2.translate(-3.3, -4.7);
ctx2.snap_to_pixel();
let after2 = ctx2.transform();
assert_eq!(after2.tx, -4.0);
assert_eq!(after2.ty, -5.0);
let mut fb3 = Framebuffer::new(10, 10);
let mut ctx3 = GfxCtx::new(&mut fb3);
ctx3.translate(7.0, 13.0);
ctx3.snap_to_pixel();
let after3 = ctx3.transform();
assert_eq!(after3.tx, 7.0);
assert_eq!(after3.ty, 13.0);
}
#[test]
fn test_slider_drag_outside_bounds_tracks_cursor() {
use crate::text::Font;
use crate::widgets::slider::Slider;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../demo/assets/CascadiaCode.ttf");
let font = Arc::new(Font::from_slice(FONT_BYTES).expect("font"));
let last_val = Rc::new(Cell::new(0.5_f64));
let lv = Rc::clone(&last_val);
let slider = Slider::new(0.5, 0.0, 1.0, Arc::clone(&font)).on_change(move |v| lv.set(v));
let mut app = App::new(Box::new(
SizedBox::new()
.with_width(200.0)
.with_height(36.0)
.with_child(Box::new(slider)),
));
app.layout(Size::new(200.0, 36.0));
app.on_mouse_down(100.0, 18.0, MouseButton::Left, Modifiers::default());
app.on_mouse_move(9999.0, 18.0);
assert_eq!(
last_val.get(),
1.0,
"dragging outside right must clamp to max (1.0), not snap to 0.0"
);
app.on_mouse_move(-9999.0, 18.0);
assert_eq!(
last_val.get(),
0.0,
"dragging outside left must clamp to min (0.0)"
);
app.on_mouse_up(0.0, 18.0, MouseButton::Left, Modifiers::default());
last_val.set(999.0); app.on_mouse_move(100.0, 18.0);
assert_eq!(
last_val.get(),
999.0,
"after mouse-up the slider must stop tracking cursor movement"
);
}