use super::*;
#[test]
fn test_color_picker_opens_and_updates_on_drag() {
use crate::text::Font;
use crate::ColorPicker;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let start = Color::rgba(1.0, 0.0, 0.0, 1.0);
let cell = Rc::new(Cell::new(start));
let picker = ColorPicker::new(Rc::clone(&cell), Arc::clone(&font));
let mut app = App::new(Box::new(picker));
const VP_H: f64 = 400.0;
app.layout(Size::new(300.0, VP_H));
let swatch_screen_y = VP_H - 10.0;
app.on_mouse_down(
50.0,
swatch_screen_y,
MouseButton::Left,
Modifiers::default(),
);
app.on_mouse_up(
50.0,
swatch_screen_y,
MouseButton::Left,
Modifiers::default(),
);
app.layout(Size::new(300.0, VP_H));
let hue_screen_y = VP_H - 242.0;
app.on_mouse_down(220.0, hue_screen_y, MouseButton::Left, Modifiers::default());
app.on_mouse_move(210.0, hue_screen_y);
app.on_mouse_up(210.0, hue_screen_y, MouseButton::Left, Modifiers::default());
let final_color = cell.get();
assert_ne!(
(start.r, start.g, start.b),
(final_color.r, final_color.g, final_color.b),
"hue drag must have mutated the bound colour cell (got {:?})",
final_color,
);
}
#[test]
fn test_click_outside_bounds_ignored() {
use crate::text::Font;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let clicked = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let clicked2 = std::sync::Arc::clone(&clicked);
let button = Button::new("X", font)
.with_font_size(14.0)
.on_click(move || {
clicked2.store(true, std::sync::atomic::Ordering::Relaxed);
});
let mut app = App::new(Box::new(button));
app.layout(Size::new(200.0, 100.0));
app.on_mouse_down(100.0, 200.0, MouseButton::Left, Modifiers::default());
app.on_mouse_up(100.0, 200.0, MouseButton::Left, Modifiers::default());
assert!(
!clicked.load(std::sync::atomic::Ordering::Relaxed),
"click outside button bounds must not fire callback"
);
}
#[test]
fn test_text_field_tracks_external_text_cell() {
use crate::text::Font;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let text = Rc::new(RefCell::new("initial".to_string()));
let mut field = TextField::new(font).with_text_cell(Rc::clone(&text));
field.layout(Size::new(160.0, 32.0));
assert_eq!(field.text(), "initial");
*text.borrow_mut() = "cleared externally".to_string();
field.layout(Size::new(160.0, 32.0));
assert_eq!(field.text(), "cleared externally");
field.set_text("typed locally");
assert_eq!(text.borrow().as_str(), "typed locally");
}
#[test]
fn test_read_only_text_field_rejects_paste() {
use crate::text::Font;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut field = TextField::new(font)
.with_text("locked")
.with_read_only(true);
field.layout(Size::new(180.0, 32.0));
field.on_event(&crate::Event::FocusGained);
crate::clipboard::set_text(" pasted");
let result = field.on_event(&crate::Event::KeyDown {
key: Key::Char('v'),
modifiers: Modifiers {
ctrl: true,
..Modifiers::default()
},
});
assert_eq!(result, crate::EventResult::Consumed);
assert_eq!(field.text(), "locked");
}
#[test]
fn test_tab_focus_advance() {
use crate::text::Font;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut root = Container::new().with_padding(4.0);
root.children_mut().push(Box::new(
TextField::new(Arc::clone(&font)).with_font_size(14.0),
));
root.children_mut().push(Box::new(
TextField::new(Arc::clone(&font)).with_font_size(14.0),
));
let mut app = App::new(Box::new(root));
app.layout(Size::new(200.0, 200.0));
app.on_key_down(Key::Tab, Modifiers::default());
app.on_key_down(Key::Tab, Modifiers::default());
app.on_key_down(Key::Tab, Modifiers::default());
}
#[test]
fn test_flex_column_first_child_highest_y() {
let mut col = FlexColumn::new()
.with_gap(0.0)
.with_padding(0.0)
.add(Box::new(SizedBox::new().with_height(40.0))) .add(Box::new(SizedBox::new().with_height(60.0)));
col.layout(Size::new(200.0, 200.0));
let y0 = col.children()[0].bounds().y;
let y1 = col.children()[1].bounds().y;
assert!(
y0 > y1,
"first child (top) should have higher Y in Y-up; got y0={y0}, y1={y1}",
);
assert_eq!(col.children()[0].bounds().height, 40.0);
assert_eq!(col.children()[1].bounds().height, 60.0);
}
#[test]
fn test_flex_row_distributes_space() {
let mut row = FlexRow::new()
.with_gap(0.0)
.with_padding(0.0)
.add_flex(Box::new(SizedBox::new()), 1.0) .add_flex(Box::new(SizedBox::new()), 1.0);
row.layout(Size::new(200.0, 40.0));
let x0 = row.children()[0].bounds().x;
let x1 = row.children()[1].bounds().x;
assert_eq!(x0, 0.0, "first flex child should start at x=0");
assert!(x1 > x0, "second flex child should be to the right of first");
assert!(
(x1 - 100.0).abs() < 1.0,
"second child should start at x≈100; got {x1}"
);
}
#[test]
fn test_scroll_view_tall_content_child_y() {
let content = SizedBox::new().with_height(500.0);
let mut scroll = ScrollView::new(Box::new(content));
let result = scroll.layout(Size::new(200.0, 200.0));
assert_eq!(result.width, 200.0);
assert_eq!(result.height, 200.0);
let child_y = scroll.children()[0].bounds().y;
assert!(
child_y < 0.0,
"tall content with offset=0 should have negative child_y; got {child_y}",
);
}
#[test]
fn test_scroll_view_middle_drag_pans_both_axes() {
use std::cell::Cell;
use std::rc::Rc;
let v_offset = Rc::new(Cell::new(80.0));
let h_offset = Rc::new(Cell::new(80.0));
let content = SizedBox::new().with_width(500.0).with_height(500.0);
let mut scroll = ScrollView::new(Box::new(content))
.horizontal(true)
.with_offset_cell(Rc::clone(&v_offset))
.with_h_offset_cell(Rc::clone(&h_offset));
scroll.layout(Size::new(200.0, 200.0));
let mods = Modifiers::default();
scroll.on_event(&crate::Event::MouseDown {
pos: crate::Point::new(100.0, 100.0),
button: MouseButton::Middle,
modifiers: mods,
});
scroll.on_event(&crate::Event::MouseMove {
pos: crate::Point::new(80.0, 120.0),
});
scroll.on_event(&crate::Event::MouseUp {
pos: crate::Point::new(80.0, 120.0),
button: MouseButton::Middle,
modifiers: mods,
});
assert_eq!(h_offset.get(), 100.0);
assert_eq!(v_offset.get(), 100.0);
}
#[test]
fn test_scroll_bar_style_defaults_match_egui() {
let style = ScrollBarStyle::default();
assert_eq!(style.kind, ScrollBarKind::Floating);
assert_eq!(style.color, ScrollBarColor::Foreground);
assert_eq!(style.bar_width, 10.0);
assert_eq!(style.floating_width, 2.0);
assert_eq!(style.handle_min_length, 12.0);
assert_eq!(style.outer_margin, 0.0);
assert_eq!(style.inner_margin, 4.0);
assert_eq!(style.content_margin, 0.0);
assert_eq!(style.fade_strength, 0.5);
assert_eq!(style.fade_size, 20.0);
}
#[test]
fn test_scroll_fade_does_not_overpaint_front_window() {
use crate::widget::paint_subtree;
use crate::widgets::{primitives::Stack, window::Window};
use crate::{DrawCtx, Event, EventResult, Rect};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
struct SolidBox {
bounds: Rect,
color: Color,
}
impl SolidBox {
fn new(color: Color) -> Self {
Self {
bounds: Rect::default(),
color,
}
}
}
impl Widget for SolidBox {
fn type_name(&self) -> &'static str {
"SolidBox"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
panic!("SolidBox has no children")
}
fn layout(&mut self, available: Size) -> Size {
self.bounds = Rect::new(0.0, 0.0, available.width, available.height);
available
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
ctx.set_fill_color(self.color);
ctx.begin_path();
ctx.rect(0.0, 0.0, self.bounds.width, self.bounds.height);
ctx.fill();
}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
}
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let offset = Rc::new(Cell::new(120.0));
let mut scroll_style = ScrollBarStyle::default();
scroll_style.fade_strength = 1.0;
scroll_style.fade_size = 80.0;
let back_content = Box::new(SizedBox::new().with_height(600.0));
let back_scroll = ScrollView::new(back_content)
.with_offset_cell(Rc::clone(&offset))
.with_style(scroll_style);
let back = Window::new("Back", Arc::clone(&font), Box::new(back_scroll))
.with_bounds(Rect::new(20.0, 20.0, 260.0, 220.0));
let front_color = Color::rgba(1.0, 0.0, 0.0, 1.0);
let front = Window::new(
"Front",
Arc::clone(&font),
Box::new(SolidBox::new(front_color)),
)
.with_bounds(Rect::new(70.0, 70.0, 180.0, 140.0));
let mut stack = Stack::new().add(Box::new(back)).add(Box::new(front));
stack.set_bounds(Rect::new(0.0, 0.0, 320.0, 260.0));
stack.layout(Size::new(320.0, 260.0));
let mut fb = Framebuffer::new(320, 260);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::black());
paint_subtree(&mut stack, &mut ctx);
}
let p = sample(&fb, 120, 150);
assert!(
p[0] > 230 && p[1] < 40 && p[2] < 40,
"back window scroll fade overpainted the front window; sampled {p:?}"
);
}
#[test]
fn test_scroll_fade_uses_window_background() {
use crate::theme::{set_visuals, Visuals};
use crate::widget::paint_subtree;
use crate::Rect;
use std::cell::Cell;
use std::rc::Rc;
struct VisualsGuard;
impl Drop for VisualsGuard {
fn drop(&mut self) {
set_visuals(Visuals::dark());
}
}
let _guard = VisualsGuard;
let visuals = Visuals::light();
let expected = visuals.window_fill;
set_visuals(visuals);
let offset = Rc::new(Cell::new(40.0));
let mut style = ScrollBarStyle::default();
style.fade_strength = 1.0;
style.fade_size = 40.0;
let content = Box::new(SizedBox::new().with_height(300.0));
let mut scroll = ScrollView::new(content)
.with_offset_cell(Rc::clone(&offset))
.with_bar_visibility(crate::ScrollBarVisibility::AlwaysHidden)
.with_style(style);
scroll.layout(Size::new(200.0, 100.0));
scroll.set_bounds(Rect::new(0.0, 0.0, 200.0, 100.0));
let mut fb = Framebuffer::new(200, 100);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(expected);
paint_subtree(&mut scroll, &mut ctx);
}
let p = sample(&fb, 100, 98);
assert!(
p[0] > 244 && p[1] > 244 && p[2] > 244,
"scroll fade should blend toward the window background, got {p:?}"
);
}
mod combo_popup;
#[test]
fn test_splitter_drag_updates_ratio() {
let mut splitter = Splitter::new(Box::new(SizedBox::new()), Box::new(SizedBox::new()));
splitter.layout(Size::new(400.0, 200.0));
splitter.set_bounds(crate::Rect::new(0.0, 0.0, 400.0, 200.0));
let div_x = (400.0_f64 - 6.0) * 0.5;
splitter.on_event(&crate::Event::MouseDown {
pos: crate::Point::new(div_x + 1.0, 100.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
splitter.on_event(&crate::Event::MouseMove {
pos: crate::Point::new(100.0, 100.0),
});
assert!(
(splitter.ratio - 0.25).abs() < 0.01,
"ratio should be ≈0.25 after drag; got {}",
splitter.ratio,
);
}
#[test]
fn test_tab_view_always_has_one_child() {
use crate::text::Font;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut tv = TabView::new(Arc::clone(&font))
.add_tab("A", Box::new(SizedBox::new().with_height(100.0)))
.add_tab("B", Box::new(SizedBox::new().with_height(200.0)));
tv.layout(Size::new(400.0, 300.0));
tv.set_bounds(crate::Rect::new(0.0, 0.0, 400.0, 300.0));
assert_eq!(
tv.children().len(),
1,
"TabView should always have exactly 1 active child"
);
tv.on_event(&crate::Event::MouseDown {
pos: crate::Point::new(300.0, 270.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
assert_eq!(
tv.children().len(),
1,
"TabView should still have exactly 1 active child after switch"
);
}
#[test]
fn test_window_close_hides_content() {
use crate::text::Font;
use crate::widget::paint_subtree;
use crate::widgets::window::Window;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let content = Button::new("Content", Arc::clone(&font)).with_font_size(14.0);
let mut win = Window::new("Test", Arc::clone(&font), Box::new(content))
.with_bounds(crate::Rect::new(0.0, 0.0, 200.0, 200.0));
win.layout(Size::new(200.0, 200.0));
let mut fb_visible = Framebuffer::new(200, 200);
{
let mut ctx = GfxCtx::new(&mut fb_visible);
ctx.clear(Color::black());
paint_subtree(&mut win, &mut ctx);
}
win.hide();
let mut fb_hidden = Framebuffer::new(200, 200);
{
let mut ctx = GfxCtx::new(&mut fb_hidden);
ctx.clear(Color::black());
paint_subtree(&mut win, &mut ctx);
}
let visible_has_pixels = fb_visible
.pixels()
.chunks(4)
.any(|p| p[0] > 50 || p[1] > 50 || p[2] > 50);
assert!(visible_has_pixels, "visible window must paint something");
let hidden_all_black = fb_hidden
.pixels()
.chunks(4)
.all(|p| p[0] < 10 && p[1] < 10 && p[2] < 10);
assert!(
hidden_all_black,
"hidden window must not paint anything; content child leaked"
);
}
#[test]
fn test_collapsed_window_title_bar_rounds_bottom_corners() {
use crate::text::Font;
use crate::widget::{paint_subtree, Widget};
use crate::widgets::window::Window;
use std::sync::Arc;
fn sample(fb: &Framebuffer, x: u32, y: u32) -> [u8; 4] {
let i = ((y * fb.width() + x) * 4) as usize;
let p = &fb.pixels()[i..i + 4];
[p[0], p[1], p[2], p[3]]
}
fn brightness(px: [u8; 4]) -> u16 {
px[0] as u16 + px[1] as u16 + px[2] as u16
}
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let content = Button::new("Content", Arc::clone(&font)).with_font_size(14.0);
let mut win = Window::new("Test", Arc::clone(&font), Box::new(content))
.with_bounds(crate::Rect::new(0.0, 0.0, 200.0, 80.0));
win.layout(Size::new(240.0, 120.0));
win.on_event(&crate::Event::MouseDown {
pos: crate::Point::new(12.0, 66.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
win.layout(Size::new(240.0, 120.0));
let mut fb = Framebuffer::new(220, 60);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::black());
paint_subtree(&mut win, &mut ctx);
}
let bottom_left_corner = sample(&fb, 1, 1);
let title_bar_interior = sample(&fb, 100, 14);
assert!(
brightness(bottom_left_corner) + 40 < brightness(title_bar_interior),
"collapsed title bar should leave the bottom-left corner rounded; corner={bottom_left_corner:?}, interior={title_bar_interior:?}"
);
}
#[test]
fn test_window_backbuffer_spec_covers_shadow_and_fade_out() {
use crate::text::Font;
use crate::widget::{BackbufferKind, Widget};
use crate::widgets::window::Window;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let content = Button::new("Content", Arc::clone(&font)).with_font_size(14.0);
let mut win = Window::new("Layered", Arc::clone(&font), Box::new(content))
.with_bounds(crate::Rect::new(0.0, 0.0, 200.0, 120.0));
let visible_spec = win.backbuffer_spec();
assert_eq!(visible_spec.kind, BackbufferKind::GlFbo);
assert!(visible_spec.cached);
assert!(visible_spec.alpha > 0.99);
assert!(visible_spec.outsets.left > 0.0);
assert!(visible_spec.outsets.bottom > 0.0);
assert!(visible_spec.outsets.right > 0.0);
assert!(visible_spec.outsets.top > 0.0);
win.hide();
assert!(
!win.is_visible(),
"non-layer renderers should still see hide() as immediate"
);
let fading_layer = win.backbuffer_spec();
assert!(
fading_layer.alpha > 0.001 && fading_layer.alpha <= 1.0,
"fade-out layer alpha should be visible and bounded, got {}",
fading_layer.alpha
);
}
#[test]
fn test_window_can_opt_out_of_gl_backbuffer() {
use crate::text::Font;
use crate::widget::{BackbufferKind, Widget};
use crate::widgets::window::Window;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let content = Button::new("Content", Arc::clone(&font)).with_font_size(14.0);
let mut win =
Window::new("Direct", Arc::clone(&font), Box::new(content)).with_gl_backbuffer(false);
assert_eq!(win.backbuffer_spec().kind, BackbufferKind::None);
}
#[test]
fn test_scroll_view_reports_overlay_animation_draw_need() {
use crate::widget::paint_subtree;
let mut scroll = ScrollView::new(Box::new(SizedBox::fixed(50.0, 300.0)))
.with_style(ScrollBarStyle::thin())
.with_bar_visibility(crate::ScrollBarVisibility::AlwaysVisible);
scroll.layout(Size::new(100.0, 100.0));
let mut fb = Framebuffer::new(100, 100);
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut scroll, &mut ctx);
assert!(
scroll.needs_draw(),
"scrollbar fade/width tweens must keep retained parents repainting"
);
}
#[test]
fn test_toggle_switch_reports_animation_draw_need() {
use crate::widget::paint_subtree;
use std::time::Duration;
let mut toggle = ToggleSwitch::new(false);
toggle.layout(Size::new(100.0, 40.0));
toggle.set_bounds(crate::Rect::new(0.0, 0.0, 34.0, 20.0));
assert!(
!toggle.needs_draw(),
"idle toggle switch should not keep the host repainting"
);
let event = crate::Event::MouseDown {
pos: crate::Point::new(10.0, 10.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
};
assert_eq!(toggle.on_event(&event), crate::EventResult::Consumed);
assert!(
toggle.needs_draw(),
"press-ring tween must make retained parents repaint"
);
let mut fb = Framebuffer::new(40, 24);
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut toggle, &mut ctx);
assert!(
crate::animation::wants_draw(),
"in-flight toggle tweens must request the next frame"
);
std::thread::sleep(Duration::from_millis(260));
crate::animation::clear_draw_request();
paint_subtree(&mut toggle, &mut ctx);
assert!(
!crate::animation::wants_draw() && !toggle.needs_draw(),
"settled toggle tweens must let reactive mode go idle"
);
}
#[test]
fn test_scroll_view_reports_global_style_epoch_change() {
use crate::widget::paint_subtree;
let mut scroll = ScrollView::new(Box::new(SizedBox::fixed(20.0, 20.0)));
scroll.layout(Size::new(100.0, 100.0));
let mut fb = Framebuffer::new(100, 100);
let mut ctx = GfxCtx::new(&mut fb);
paint_subtree(&mut scroll, &mut ctx);
assert!(
!scroll.needs_draw(),
"clean scroll view without active scrollbar animation should be idle"
);
crate::set_scroll_style(ScrollBarStyle::solid());
assert!(
scroll.needs_draw(),
"global scrollbar style changes must invalidate clean retained parents"
);
}
#[test]
fn test_consumed_event_marks_widget_backbuffer_dirty() {
use crate::widget::{dispatch_event, BackbufferState, Widget};
use crate::{DrawCtx, Event, EventResult, Modifiers, MouseButton, Point, Rect, Size};
struct DirtyProbe {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
backbuffer: BackbufferState,
}
impl Widget for DirtyProbe {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
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 {
available
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Consumed
}
fn backbuffer_state_mut(&mut self) -> Option<&mut BackbufferState> {
Some(&mut self.backbuffer)
}
}
let mut root: Box<dyn Widget> = Box::new(DirtyProbe {
bounds: Rect::new(0.0, 0.0, 10.0, 10.0),
children: Vec::new(),
backbuffer: BackbufferState::new(),
});
root.backbuffer_state_mut().unwrap().dirty = false;
let event = Event::MouseDown {
pos: Point::new(1.0, 1.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
};
assert_eq!(
dispatch_event(&mut root, &[], &event, Point::new(1.0, 1.0)),
EventResult::Consumed
);
assert!(root.backbuffer_state_mut().unwrap().dirty);
}