use super::*;
#[test]
fn test_inspector_row0_at_top() {
use crate::geometry::Rect;
use crate::text::Font;
use crate::widget::{InspectorNode, Widget};
use crate::widgets::inspector::InspectorPanel;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let hovered_bounds = Rc::new(RefCell::new(None));
let nodes: Rc<RefCell<Vec<InspectorNode>>> = Rc::new(RefCell::new(vec![
InspectorNode {
type_name: "Root",
screen_bounds: Rect::new(0.0, 0.0, 100.0, 50.0),
depth: 0,
properties: vec![],
},
InspectorNode {
type_name: "Child",
screen_bounds: Rect::new(0.0, 0.0, 50.0, 20.0),
depth: 1,
properties: vec![],
},
]));
let mut panel = InspectorPanel::new(
Arc::clone(&font),
Rc::clone(&nodes),
Rc::clone(&hovered_bounds),
);
panel.layout(crate::Size::new(200.0, 300.0));
panel.set_bounds(Rect::new(0.0, 0.0, 200.0, 300.0));
assert_eq!(
panel.children().len(),
1,
"InspectorPanel must have one presence child"
);
assert_eq!(
panel.children()[0].type_name(),
"TreeView",
"The presence child must report type_name 'TreeView'"
);
assert_eq!(
panel.tree_view.nodes.len(),
2,
"tree_view must have 2 nodes"
);
assert!(
panel.tree_view.nodes[0].parent.is_none(),
"Root must have no parent"
);
assert_eq!(
panel.tree_view.nodes[1].parent,
Some(0),
"Child must have Root (0) as parent"
);
let tv_bounds = panel.tree_view.bounds();
assert!(tv_bounds.height > 0.0, "TreeView must have positive height");
assert!(
tv_bounds.y >= 60.0,
"TreeView bottom must be above split handle"
);
assert!(
tv_bounds.y + tv_bounds.height <= 270.0 + 1.0,
"TreeView top must not exceed list_area_h (270); got {}",
tv_bounds.y + tv_bounds.height
);
}
#[test]
fn test_inspector_tree_populates_from_nodes() {
use crate::geometry::Rect;
use crate::text::Font;
use crate::widget::{InspectorNode, Widget};
use crate::widgets::inspector::InspectorPanel;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let hovered_bounds = Rc::new(RefCell::new(None));
let nodes: Rc<RefCell<Vec<InspectorNode>>> = Rc::new(RefCell::new(vec![
InspectorNode {
type_name: "Root",
screen_bounds: Rect::new(0.0, 0.0, 100.0, 50.0),
depth: 0,
properties: vec![],
},
InspectorNode {
type_name: "Child",
screen_bounds: Rect::new(0.0, 0.0, 50.0, 20.0),
depth: 1,
properties: vec![],
},
InspectorNode {
type_name: "Sibling",
screen_bounds: Rect::new(0.0, 0.0, 50.0, 20.0),
depth: 0,
properties: vec![],
},
]));
let mut panel = InspectorPanel::new(Arc::clone(&font), Rc::clone(&nodes), hovered_bounds);
panel.layout(crate::Size::new(200.0, 400.0));
assert_eq!(panel.tree_view.nodes.len(), 3, "must have 3 tree nodes");
assert!(
panel.tree_view.nodes[0].parent.is_none(),
"node 0 must be root-level"
);
assert_eq!(
panel.tree_view.nodes[1].parent,
Some(0),
"node 1 must be child of node 0"
);
assert!(
panel.tree_view.nodes[2].parent.is_none(),
"node 2 must be root-level"
);
assert_eq!(
panel.children().len(),
1,
"InspectorPanel must have one presence child"
);
assert_eq!(
panel.children()[0].type_name(),
"TreeView",
"Presence child must report type_name 'TreeView'"
);
}
#[test]
fn test_inspector_tree_default_expanded() {
use crate::geometry::Rect;
use crate::text::Font;
use crate::widget::{InspectorNode, Widget};
use crate::widgets::inspector::InspectorPanel;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let hovered_bounds = Rc::new(RefCell::new(None));
let nodes: Rc<RefCell<Vec<InspectorNode>>> = Rc::new(RefCell::new(vec![
InspectorNode {
type_name: "Root",
screen_bounds: Rect::new(0.0, 0.0, 100.0, 50.0),
depth: 0,
properties: vec![],
},
InspectorNode {
type_name: "Child",
screen_bounds: Rect::new(0.0, 0.0, 50.0, 20.0),
depth: 1,
properties: vec![],
},
]));
let mut panel = InspectorPanel::new(Arc::clone(&font), Rc::clone(&nodes), hovered_bounds);
panel.layout(crate::Size::new(200.0, 400.0));
for (i, node) in panel.tree_view.nodes.iter().enumerate() {
assert!(node.is_expanded, "node {} must be expanded by default", i);
}
}
#[test]
fn test_inspector_tree_drag_disabled() {
use crate::geometry::Rect;
use crate::text::Font;
use crate::widget::InspectorNode;
use crate::widgets::inspector::InspectorPanel;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let hovered_bounds = Rc::new(RefCell::new(None));
let nodes: Rc<RefCell<Vec<InspectorNode>>> = Rc::new(RefCell::new(vec![InspectorNode {
type_name: "Root",
screen_bounds: Rect::new(0.0, 0.0, 100.0, 50.0),
depth: 0,
properties: vec![],
}]));
let panel = InspectorPanel::new(Arc::clone(&font), Rc::clone(&nodes), hovered_bounds);
assert!(
!panel.tree_view.drag_enabled,
"inspector TreeView must have drag disabled"
);
}
#[test]
fn test_expand_toggle_paints_arrow_only_when_has_children() {
use crate::widget::paint_subtree;
use crate::widgets::tree_view::row::ExpandToggle;
let mut fb_with = Framebuffer::new(20, 20);
let mut fb_without = Framebuffer::new(20, 20);
{
let mut ctx = GfxCtx::new(&mut fb_with);
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
let mut toggle = ExpandToggle::new(true, false);
toggle.layout(Size::new(20.0, 20.0));
toggle.set_bounds(crate::Rect::new(0.0, 0.0, 20.0, 20.0));
paint_subtree(&mut toggle, &mut ctx);
}
{
let mut ctx = GfxCtx::new(&mut fb_without);
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
let mut toggle = ExpandToggle::new(false, false);
toggle.layout(Size::new(20.0, 20.0));
toggle.set_bounds(crate::Rect::new(0.0, 0.0, 20.0, 20.0));
paint_subtree(&mut toggle, &mut ctx);
}
assert_ne!(fb_with.pixels(), fb_without.pixels());
}
#[test]
fn test_text_field_typing() {
use crate::text::Font;
use crate::widgets::text_field::TextField as TF;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut field = TF::new(font).with_font_size(14.0);
field.layout(Size::new(200.0, 36.0));
field.set_bounds(crate::Rect::new(0.0, 0.0, 200.0, 36.0));
field.on_event(&crate::Event::FocusGained);
field.on_event(&crate::Event::KeyDown {
key: Key::Char('H'),
modifiers: Modifiers::default(),
});
field.on_event(&crate::Event::KeyDown {
key: Key::Char('i'),
modifiers: Modifiers::default(),
});
assert_eq!(field.text(), "Hi", "typed characters should appear in text");
field.on_event(&crate::Event::KeyDown {
key: Key::Backspace,
modifiers: Modifiers::default(),
});
assert_eq!(field.text(), "H", "backspace should remove last character");
}
#[test]
fn test_treeview_children_count_equals_visible_rows() {
use crate::geometry::Size;
use crate::text::Font;
use crate::widgets::tree_view::{NodeIcon, TreeView};
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut tv = TreeView::new(Arc::clone(&font));
let root = tv.add_root("Root", NodeIcon::Folder);
tv.add_child(root, "Child A", NodeIcon::File);
tv.add_child(root, "Child B", NodeIcon::File);
tv.nodes[root].is_expanded = true;
tv.layout(Size::new(300.0, 200.0));
assert_eq!(
tv.children().len(),
3,
"expected 3 children after expanding root with 2 children"
);
}
#[test]
fn test_treeview_row_node_idx() {
use crate::geometry::Size;
use crate::text::Font;
use crate::widgets::tree_view::{NodeIcon, TreeView};
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let mut tv = TreeView::new(Arc::clone(&font));
tv.add_root("Only Root", NodeIcon::Package);
tv.layout(Size::new(200.0, 100.0));
assert_eq!(tv.children().len(), 1);
assert_eq!(tv.children()[0].type_name(), "TreeRow");
}
#[test]
fn test_inspector_top_row_appears_at_top_of_tree_area() {
use crate::geometry::{Rect, Size};
use crate::text::Font;
use crate::widget::{paint_subtree, InspectorNode, Widget};
use crate::widgets::inspector::InspectorPanel;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
let font = Arc::new(Font::from_slice(TEST_FONT).unwrap());
let nodes: Rc<RefCell<Vec<InspectorNode>>> = Rc::new(RefCell::new(vec![InspectorNode {
type_name: "Window",
screen_bounds: Rect::new(0.0, 0.0, 100.0, 100.0),
depth: 0,
properties: vec![],
}]));
let hovered = Rc::new(RefCell::new(None));
let mut panel = InspectorPanel::new(Arc::clone(&font), Rc::clone(&nodes), Rc::clone(&hovered));
let pw = 240u32;
let ph = 400u32;
let mut fb = Framebuffer::new(pw, ph);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
panel.layout(Size::new(pw as f64, ph as f64));
panel.set_bounds(Rect::new(0.0, 0.0, pw as f64, ph as f64));
paint_subtree(&mut panel, &mut ctx);
}
let row_y_down: usize = 35;
let row_y_up = (ph as usize).saturating_sub(1).saturating_sub(row_y_down);
let pixels = fb.pixels();
let mut found_non_white = false;
for px in 5..(pw as usize - 5) {
let idx = (row_y_up * pw as usize + px) * 4;
if idx + 3 < pixels.len() {
let r = pixels[idx] as u32;
let g = pixels[idx + 1] as u32;
let b = pixels[idx + 2] as u32;
if r < 240 || g < 240 || b < 240 {
found_non_white = true;
break;
}
}
}
assert!(
found_non_white,
"expected non-white content just below the header at row_y_down={}, but got all-white — check clip_rect+translate ordering in InspectorPanel::paint()",
row_y_down
);
}
#[test]
fn test_treeview_drag_node_excluded_from_row_widgets() {
use crate::event::{Event, Modifiers, MouseButton};
use crate::geometry::{Point, Size};
use crate::widgets::tree_view::{NodeIcon, TreeView};
use std::sync::Arc;
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
use crate::geometry::Rect;
let mut tv = TreeView::new(Arc::clone(&font)).with_drag_enabled();
tv.add_root("Node A", NodeIcon::File);
tv.add_root("Node B", NodeIcon::File);
tv.layout(Size::new(200.0, 100.0));
tv.set_bounds(Rect::new(0.0, 0.0, 200.0, 100.0));
assert_eq!(tv.children().len(), 2);
tv.on_event(&Event::MouseDown {
pos: Point::new(50.0, 88.0),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
tv.on_event(&Event::MouseMove {
pos: Point::new(50.0, 78.0),
});
tv.layout(Size::new(200.0, 100.0));
assert_eq!(
tv.children().len(),
1,
"dragged node must be excluded from row_widgets during live drag"
);
}
#[test]
fn test_button_has_label_child() {
use crate::text::Font;
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 mut btn = Button::new("Click me", font);
btn.layout(Size::new(200.0, 40.0));
assert_eq!(
btn.children().len(),
1,
"Button must expose exactly one Label child"
);
assert_eq!(
btn.children()[0].type_name(),
"Label",
"Button's child must be a Label widget"
);
}
#[test]
fn test_button_label_child_fills_button() {
use crate::text::Font;
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 mut btn = Button::new("Click me", font);
let size = btn.layout(Size::new(300.0, 50.0));
let label_bounds = btn.children()[0].bounds();
assert!(
label_bounds.width < size.width,
"Label width must be tight (less than button width); got label_w={} btn_w={}",
label_bounds.width,
size.width
);
assert!(label_bounds.width > 0.0, "Label width must be positive");
assert!(label_bounds.height > 0.0, "Label height must be positive");
let expected_x = (size.width - label_bounds.width) * 0.5;
assert!(
(label_bounds.x - expected_x).abs() < 1.0,
"Label must be horizontally centred; expected x≈{:.1}, got x={:.1}",
expected_x,
label_bounds.x
);
let expected_y = (size.height - label_bounds.height) * 0.5;
assert!(
(label_bounds.y - expected_y).abs() < 1.0,
"Label must be vertically centred; expected y≈{:.1}, got y={:.1}",
expected_y,
label_bounds.y
);
}
#[test]
fn test_label_properties() {
use crate::{text::Font, 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 label = Label::new("Hello", font).with_font_size(13.0);
let props: std::collections::HashMap<_, _> = label.properties().into_iter().collect();
assert!(
props.contains_key("text"),
"Label must expose 'text' property"
);
assert_eq!(props["text"], "Hello");
assert!(
props.contains_key("has_backbuffer"),
"Label must expose 'has_backbuffer'"
);
assert_eq!(props["has_backbuffer"], "true");
}
#[test]
fn test_button_properties() {
use crate::text::Font;
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 btn = Button::new("Primary Action", font);
let props: std::collections::HashMap<_, _> = btn.properties().into_iter().collect();
assert!(
props.contains_key("label"),
"Button must expose 'label' property"
);
assert_eq!(props["label"], "Primary Action");
}
#[test]
fn test_button_inspector_hierarchy() {
use crate::{
geometry::{Point, Rect},
text::Font,
widget::collect_inspector_nodes,
};
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 mut btn = Button::new("OK", font);
btn.layout(Size::new(200.0, 40.0));
btn.set_bounds(Rect::new(0.0, 0.0, 200.0, 40.0));
let mut nodes = Vec::new();
let boxed: Box<dyn Widget> = Box::new(btn);
collect_inspector_nodes(boxed.as_ref(), 0, Point::new(0.0, 0.0), &mut nodes);
assert!(nodes.len() >= 2, "Must have at least Button + Label nodes");
assert_eq!(nodes[0].type_name, "Button");
assert_eq!(nodes[0].depth, 0);
assert_eq!(nodes[1].type_name, "Label");
assert_eq!(nodes[1].depth, 1);
}
#[test]
fn test_invisible_widget_excluded_from_inspector() {
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Point, Rect, Size};
use crate::widget::{collect_inspector_nodes, Widget};
struct ToggleWidget {
bounds: Rect,
visible: bool,
children: Vec<Box<dyn Widget>>,
}
impl Widget for ToggleWidget {
fn type_name(&self) -> &'static str {
"ToggleWidget"
}
fn is_visible(&self) -> bool {
self.visible
}
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 {
available
}
fn paint(&mut self, _: &mut dyn DrawCtx) {}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
}
let visible = ToggleWidget {
bounds: Rect::new(0.0, 0.0, 100.0, 40.0),
visible: true,
children: Vec::new(),
};
let hidden = ToggleWidget {
bounds: Rect::new(0.0, 50.0, 100.0, 40.0),
visible: false,
children: Vec::new(),
};
let mut nodes = Vec::new();
collect_inspector_nodes(&visible, 0, Point::ORIGIN, &mut nodes);
assert_eq!(nodes.len(), 1, "visible widget appears once");
assert_eq!(nodes[0].type_name, "ToggleWidget");
nodes.clear();
collect_inspector_nodes(&hidden, 0, Point::ORIGIN, &mut nodes);
assert!(
nodes.is_empty(),
"invisible widget produces no inspector nodes"
);
}
#[test]
fn test_treeview_click_selects_without_collapsing_when_flag_off() {
use crate::event::Modifiers;
use crate::geometry::{Point, Size};
use crate::text::Font;
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 mut tv = crate::widgets::tree_view::TreeView::new(Arc::clone(&font)).with_row_height(20.0);
let root = tv.add_root("Root", crate::widgets::tree_view::NodeIcon::Package);
tv.expand(root);
tv.add_child(root, "Child A", crate::widgets::tree_view::NodeIcon::File);
tv.add_child(root, "Child B", crate::widgets::tree_view::NodeIcon::File);
use crate::widget::Widget;
tv.layout(Size::new(300.0, 200.0));
tv.set_bounds(crate::geometry::Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(
tv.children().len(),
3,
"should have Root + 2 children visible"
);
let root_row_y = 200.0 - 20.0 * 0.5; tv.on_event(&crate::event::Event::MouseDown {
pos: Point::new(80.0, root_row_y),
button: crate::event::MouseButton::Left,
modifiers: Modifiers::default(),
});
tv.layout(Size::new(300.0, 200.0));
assert_eq!(
tv.children().len(),
3,
"clicking root row must NOT collapse it when toggle_on_row_click = false"
);
}
#[test]
fn test_treeview_click_collapses_when_flag_on() {
use crate::event::Modifiers;
use crate::geometry::{Point, Size};
use crate::text::Font;
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 mut tv = crate::widgets::tree_view::TreeView::new(Arc::clone(&font))
.with_row_height(20.0)
.with_toggle_on_row_click();
let root = tv.add_root("Root", crate::widgets::tree_view::NodeIcon::Package);
tv.expand(root);
tv.add_child(root, "Child A", crate::widgets::tree_view::NodeIcon::File);
use crate::widget::Widget;
tv.layout(Size::new(300.0, 200.0));
tv.set_bounds(crate::geometry::Rect::new(0.0, 0.0, 300.0, 200.0));
assert_eq!(tv.children().len(), 2, "Root + 1 child visible initially");
let root_row_y = 200.0 - 20.0 * 0.5;
tv.on_event(&crate::event::Event::MouseDown {
pos: Point::new(80.0, root_row_y), button: crate::event::MouseButton::Left,
modifiers: Modifiers::default(),
});
tv.layout(Size::new(300.0, 200.0));
assert_eq!(
tv.children().len(),
1,
"clicking root row body must collapse it when toggle_on_row_click = true"
);
}
#[test]
fn test_push_pop_layer_solid_composites_correctly() {
let mut fb = Framebuffer::new(20, 20);
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::white());
ctx.push_layer(20.0, 20.0);
ctx.set_fill_color(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 20.0, 20.0);
ctx.fill();
ctx.pop_layer();
drop(ctx);
let center = sample(&fb, 10, 10);
assert!(
is_red(center),
"After layer composite, centre must be red; got {center:?}"
);
}
#[test]
fn test_push_pop_layer_alpha_blends_into_parent() {
let mut fb = Framebuffer::new(20, 20);
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::white());
ctx.push_layer(20.0, 20.0);
ctx.set_fill_color(Color::rgba(1.0, 0.0, 0.0, 0.5));
ctx.begin_path();
ctx.rect(0.0, 0.0, 20.0, 20.0);
ctx.fill();
ctx.pop_layer();
drop(ctx);
let [r, g, b, _] = sample(&fb, 10, 10);
assert!(r > 200, "Red channel must be high; got {r}");
assert!(
g > 80 && g < 200,
"Green channel must be mid-tone (pink); got {g}"
);
assert!(
b > 80 && b < 200,
"Blue channel must be mid-tone (pink); got {b}"
);
}
#[cfg(any())]
fn _deleted_backbuffer_tests_marker() {}