Skip to main content

gizmo_ui/
bundles.rs

1use crate::components::{Style, Node, BackgroundColor, Interaction};
2
3/// Bundle for a plain, non-interactive UI element.
4#[derive(Default)]
5pub struct NodeBundle {
6    /// Layout style of the element.
7    pub style: Style,
8    /// Computed layout geometry of the element.
9    pub node: Node,
10    /// Fill color of the element.
11    pub background_color: BackgroundColor,
12}
13
14impl gizmo_core::component::Bundle for NodeBundle {
15    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> {
16        vec![
17            gizmo_core::archetype::ComponentInfo::of::<Style>(),
18            gizmo_core::archetype::ComponentInfo::of::<Node>(),
19            gizmo_core::archetype::ComponentInfo::of::<BackgroundColor>(),
20        ]
21    }
22
23    fn apply(self, world: &mut gizmo_core::world::World, entity: gizmo_core::entity::Entity) {
24        world.add_component(entity, self.style);
25        world.add_component(entity, self.node);
26        world.add_component(entity, self.background_color);
27    }
28
29    unsafe fn write_to_archetype(
30        self,
31        arch: &mut gizmo_core::archetype::Archetype,
32        row: usize,
33        tick: u32,
34    ) {
35        self.style.write_to_archetype(arch, row, tick);
36        self.node.write_to_archetype(arch, row, tick);
37        self.background_color.write_to_archetype(arch, row, tick);
38    }
39}
40
41/// Bundle for an interactive button UI element, adding an [`Interaction`] state.
42#[derive(Default)]
43pub struct ButtonBundle {
44    /// Layout style of the element.
45    pub style: Style,
46    /// Computed layout geometry of the element.
47    pub node: Node,
48    /// Fill color of the element.
49    pub background_color: BackgroundColor,
50    /// Current pointer interaction state of the element.
51    pub interaction: Interaction,
52}
53
54impl gizmo_core::component::Bundle for ButtonBundle {
55    fn get_infos() -> Vec<gizmo_core::archetype::ComponentInfo> {
56        vec![
57            gizmo_core::archetype::ComponentInfo::of::<Style>(),
58            gizmo_core::archetype::ComponentInfo::of::<Node>(),
59            gizmo_core::archetype::ComponentInfo::of::<BackgroundColor>(),
60            gizmo_core::archetype::ComponentInfo::of::<Interaction>(),
61        ]
62    }
63
64    fn apply(self, world: &mut gizmo_core::world::World, entity: gizmo_core::entity::Entity) {
65        world.add_component(entity, self.style);
66        world.add_component(entity, self.node);
67        world.add_component(entity, self.background_color);
68        world.add_component(entity, self.interaction);
69    }
70
71    unsafe fn write_to_archetype(
72        self,
73        arch: &mut gizmo_core::archetype::Archetype,
74        row: usize,
75        tick: u32,
76    ) {
77        self.style.write_to_archetype(arch, row, tick);
78        self.node.write_to_archetype(arch, row, tick);
79        self.background_color.write_to_archetype(arch, row, tick);
80        self.interaction.write_to_archetype(arch, row, tick);
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use gizmo_core::component::Bundle;
88    use gizmo_core::world::World;
89    use gizmo_math::Vec4;
90
91    fn register_ui(world: &mut World) {
92        world.register_component_type::<Style>();
93        world.register_component_type::<Node>();
94        world.register_component_type::<BackgroundColor>();
95        world.register_component_type::<Interaction>();
96    }
97
98    #[test]
99    fn node_bundle_attaches_only_the_visual_components() {
100        let mut world = World::new();
101        register_ui(&mut world);
102        let e = world.spawn();
103        NodeBundle::default().apply(&mut world, e);
104
105        assert!(world.borrow::<Style>().get(e.id()).is_some());
106        assert!(world.borrow::<Node>().get(e.id()).is_some());
107        assert!(world.borrow::<BackgroundColor>().get(e.id()).is_some());
108        // A plain node is non-interactive: it must NOT carry an Interaction, or it
109        // would be picked up by the interaction system's hit-test query.
110        assert!(world.borrow::<Interaction>().get(e.id()).is_none());
111        // The advertised archetype shape must match what `apply` actually writes.
112        assert_eq!(NodeBundle::get_infos().len(), 3);
113    }
114
115    #[test]
116    fn button_bundle_adds_interaction_with_sane_defaults() {
117        let mut world = World::new();
118        register_ui(&mut world);
119        let e = world.spawn();
120        ButtonBundle::default().apply(&mut world, e);
121
122        assert!(world.borrow::<Style>().get(e.id()).is_some());
123        assert!(world.borrow::<Node>().get(e.id()).is_some());
124        // A fresh button starts un-interacted.
125        assert_eq!(*world.borrow::<Interaction>().get(e.id()).unwrap(), Interaction::None);
126        // Default fill is OPAQUE white: a transparent (alpha 0) or black default
127        // would make every button invisible, so this default is a real contract.
128        let bg = *world.borrow::<BackgroundColor>().get(e.id()).unwrap();
129        assert_eq!(bg.0, Vec4::new(1.0, 1.0, 1.0, 1.0));
130        // Button advertises one more component (Interaction) than a plain node.
131        assert_eq!(ButtonBundle::get_infos().len(), 4);
132        assert_eq!(ButtonBundle::get_infos().len(), NodeBundle::get_infos().len() + 1);
133    }
134}