use gizmo_core::query::{Query, Mut};
use gizmo_core::system::{Res, ResMut};
use gizmo_core::window::WindowInfo;
use gizmo_core::component::{Children, Parent};
use crate::components::{Style, Node};
use crate::layout::UiContext;
pub fn ui_layout_system(
mut ctx: ResMut<UiContext>,
window: Res<WindowInfo>,
styles: Query<&Style>,
parents: Query<&Parent>,
children: Query<&Children>,
mut nodes: Query<Mut<Node>>,
) {
ctx.window_size = gizmo_math::Vec2::new(window.width, window.height);
let mut current_entities = std::collections::HashSet::new();
for (entity, style) in styles.iter() {
current_entities.insert(entity);
ctx.sync_style(entity, style);
}
ctx.retain_entities(¤t_entities);
for (entity, _) in styles.iter() {
match children.get(entity) {
Some(children_comp) => ctx.set_children(entity, &children_comp.0),
None => ctx.set_children(entity, &[]),
}
}
let roots: Vec<u32> = current_entities
.iter()
.copied()
.filter(|&entity| {
parents.get(entity).is_none()
})
.collect();
for &root in &roots {
ctx.compute_root_layout(root);
}
let mut stack: Vec<(u32, gizmo_math::Vec2)> =
roots.iter().map(|&e| (e, gizmo_math::Vec2::ZERO)).collect();
let mut visited = std::collections::HashSet::new();
while let Some((entity, parent_origin)) = stack.pop() {
if !visited.insert(entity) {
continue; }
let Some((size, local)) = ctx.relative_layout(entity) else {
continue;
};
let abs = parent_origin + local;
if let Some(mut node) = nodes.get_mut(entity) {
node.size = size;
node.position = abs;
}
if let Some(children_comp) = children.get(entity) {
for &child_id in &children_comp.0 {
stack.push((child_id, abs));
}
}
}
}
#[cfg(test)]
mod tests {
use crate::components::{Interaction, Node, Style, UiRect, Val};
use crate::layout::UiContext;
use gizmo_core::component::{Parent, Children};
use gizmo_core::entity::Entity;
use gizmo_core::input::Input;
use gizmo_core::system::Schedule;
use gizmo_core::window::WindowInfo;
use gizmo_core::world::World;
use gizmo_math::Vec2;
fn make_world() -> (World, Schedule) {
let mut world = World::new();
let mut schedule = Schedule::new();
crate::register(&mut world, &mut schedule);
world.register_component_type::<Parent>();
world.register_component_type::<Children>();
world.insert_resource(Input::new());
(world, schedule)
}
fn set_window(world: &mut World, w: f32, h: f32) {
let mut info = world.get_resource_mut::<WindowInfo>().unwrap();
*info = WindowInfo::new(w, h);
}
fn sized_style(w: f32, h: f32) -> Style {
Style { width: Val::Px(w), height: Val::Px(h), ..Default::default() }
}
fn padded_style(w: f32, h: f32, pad_left: f32, pad_top: f32) -> Style {
Style {
width: Val::Px(w),
height: Val::Px(h),
padding: UiRect::new(Val::Px(pad_left), Val::ZERO, Val::Px(pad_top), Val::ZERO),
..Default::default()
}
}
fn node_of(world: &World, e: Entity) -> Node {
*world.borrow::<Node>().get(e.id()).unwrap()
}
fn assert_vec2(got: Vec2, x: f32, y: f32) {
assert!(
(got.x - x).abs() < 1e-3 && (got.y - y).abs() < 1e-3,
"expected ({x}, {y}), got ({}, {})",
got.x,
got.y
);
}
#[test]
fn root_is_sized_and_placed_at_window_origin() {
let (mut world, mut schedule) = make_world();
let e = world.spawn();
world.add_component(e, sized_style(120.0, 80.0));
world.add_component(e, Node::default());
schedule.run(&mut world, 0.016);
let n = node_of(&world, e);
assert_vec2(n.size, 120.0, 80.0);
assert_vec2(n.position, 0.0, 0.0);
}
#[test]
fn window_size_drives_percentage_root_layout() {
let (mut world, mut schedule) = make_world();
set_window(&mut world, 800.0, 600.0);
let e = world.spawn();
world.add_component(
e,
Style {
width: Val::Percent(100.0),
height: Val::Percent(50.0),
..Default::default()
},
);
world.add_component(e, Node::default());
schedule.run(&mut world, 0.016);
let n = node_of(&world, e);
assert_vec2(n.size, 800.0, 300.0);
}
#[test]
fn px_percent_and_auto_widths_are_three_different_layouts() {
let (mut world, mut schedule) = make_world();
set_window(&mut world, 1000.0, 500.0);
let spawn = |world: &mut World, width: Val| {
let e = world.spawn();
world.add_component(e, Style { width, height: Val::Px(10.0), ..Default::default() });
world.add_component(e, Node::default());
e
};
let px = spawn(&mut world, Val::Px(40.0));
let percent = spawn(&mut world, Val::Percent(40.0));
let auto = spawn(&mut world, Val::Auto);
schedule.run(&mut world, 0.016);
assert_vec2(node_of(&world, px).size, 40.0, 10.0);
assert_vec2(node_of(&world, percent).size, 400.0, 10.0);
assert_vec2(node_of(&world, auto).size, 0.0, 10.0);
}
#[test]
fn child_positions_accumulate_ancestor_offsets_into_absolute_coords() {
let (mut world, mut schedule) = make_world();
let root = world.spawn();
world.add_component(root, padded_style(400.0, 400.0, 100.0, 70.0));
world.add_component(root, Node::default());
let mid = world.spawn();
world.add_component(mid, padded_style(200.0, 200.0, 20.0, 30.0));
world.add_component(mid, Node::default());
world.add_component(mid, Parent(root.id()));
let leaf = world.spawn();
world.add_component(leaf, sized_style(50.0, 50.0));
world.add_component(leaf, Node::default());
world.add_component(leaf, Parent(mid.id()));
world.add_component(root, Children(vec![mid.id()]));
world.add_component(mid, Children(vec![leaf.id()]));
schedule.run(&mut world, 0.016);
let (loc_root, loc_mid, loc_leaf) = {
let ctx = world.get_resource::<UiContext>().unwrap();
let rel = |e: Entity| ctx.relative_layout(e.id()).unwrap().1;
(rel(root), rel(mid), rel(leaf))
};
let pr = node_of(&world, root).position;
let pm = node_of(&world, mid).position;
let pl = node_of(&world, leaf).position;
assert_vec2(pr, loc_root.x, loc_root.y);
let expect_mid = loc_root + loc_mid;
assert_vec2(pm, expect_mid.x, expect_mid.y);
let expect_leaf = loc_root + loc_mid + loc_leaf;
assert_vec2(pl, expect_leaf.x, expect_leaf.y);
assert!(loc_mid.x > 0.0 && loc_mid.y > 0.0, "padding should offset mid within root");
assert!(
pl.x > loc_leaf.x + 1.0 && pl.y > loc_leaf.y + 1.0,
"grandchild absolute {pl:?} must exceed its parent-relative {loc_leaf:?}"
);
}
#[test]
fn layout_node_is_created_then_reclaimed_when_style_removed() {
let (mut world, mut schedule) = make_world();
let e = world.spawn();
world.add_component(e, sized_style(10.0, 10.0));
world.add_component(e, Node::default());
schedule.run(&mut world, 0.016);
{
let ctx = world.get_resource::<UiContext>().unwrap();
assert!(ctx.is_tracked(e.id()), "node mapped after first frame");
assert_eq!(ctx.tracked_count(), 1);
assert_eq!(ctx.node_count(), 1);
}
world.remove_component::<Style>(e);
schedule.run(&mut world, 0.016);
{
let ctx = world.get_resource::<UiContext>().unwrap();
assert!(!ctx.is_tracked(e.id()), "mapping evicted");
assert_eq!(ctx.tracked_count(), 0);
assert_eq!(ctx.node_count(), 0, "layout node freed");
}
}
#[test]
fn interaction_state_machine_tracks_pointer_and_button() {
let (mut world, mut schedule) = make_world();
let btn = world.spawn();
world.add_component(
btn,
Node { position: Vec2::new(100.0, 100.0), size: Vec2::new(50.0, 30.0) },
);
world.add_component(btn, Interaction::None);
let interaction = |world: &World| *world.borrow::<Interaction>().get(btn.id()).unwrap();
world.get_resource_mut::<Input>().unwrap().set_mouse_position(10.0, 10.0);
schedule.run(&mut world, 0.016);
assert_eq!(interaction(&world), Interaction::None);
world.get_resource_mut::<Input>().unwrap().set_mouse_position(120.0, 110.0);
schedule.run(&mut world, 0.016);
assert_eq!(interaction(&world), Interaction::Hovered);
{
let mut input = world.get_resource_mut::<Input>().unwrap();
input.set_mouse_position(120.0, 110.0);
input.on_mouse_button_pressed(0);
}
schedule.run(&mut world, 0.016);
assert_eq!(interaction(&world), Interaction::Pressed);
{
let mut input = world.get_resource_mut::<Input>().unwrap();
*input = Input::new();
input.set_mouse_position(0.0, 0.0);
}
schedule.run(&mut world, 0.016);
assert_eq!(interaction(&world), Interaction::None);
}
#[test]
fn hovering_one_element_does_not_affect_a_disjoint_element() {
let (mut world, mut schedule) = make_world();
let a = world.spawn();
world.add_component(
a,
Node { position: Vec2::new(0.0, 0.0), size: Vec2::new(50.0, 50.0) },
);
world.add_component(a, Interaction::None);
let b = world.spawn();
world.add_component(
b,
Node { position: Vec2::new(100.0, 0.0), size: Vec2::new(50.0, 50.0) },
);
world.add_component(b, Interaction::None);
world.get_resource_mut::<Input>().unwrap().set_mouse_position(25.0, 25.0);
schedule.run(&mut world, 0.016);
assert_eq!(*world.borrow::<Interaction>().get(a.id()).unwrap(), Interaction::Hovered);
assert_eq!(*world.borrow::<Interaction>().get(b.id()).unwrap(), Interaction::None);
}
}