Skip to main content

fantasy_craft/gui/
gui_button.rs

1use macroquad::prelude::*;
2use serde::Deserialize;
3
4use crate::{core::event::EventBus, gui::{alignment::{HorizontalAlignment, HorizontalAlignmentType, VerticalAlignment, VerticalAlignmentType}, event::UiClickEvent, gui_action::GuiAction, gui_box::GuiBox, resources::UiResolvedRects}, prelude::{ColorData, ComponentLoader, Context, Visible}};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ButtonState {
8    Idle,
9    Hovered,
10    Pressed
11}
12
13impl ButtonState {
14    pub fn to_str(&self) -> &'static str {
15        match self {
16            ButtonState::Idle => "idle",
17            ButtonState::Hovered => "hovered",
18            ButtonState::Pressed => "pressed"
19        }
20    }
21
22    pub fn from_str(value: &str) -> ButtonState {
23        match value {
24            "idle" => ButtonState::Idle,
25            "hovered" => ButtonState::Hovered,
26            "pressed" => ButtonState::Pressed,
27            _ => ButtonState::Idle
28        }
29    }
30}
31
32#[derive(Debug, Clone, Copy)]
33pub struct GuiButton {
34    pub state: ButtonState,
35    pub just_clicked: bool,
36    pub hovered_color: Color,
37    pub pressed_color: Color,
38    pub normal_color: Color
39}
40
41impl Default for GuiButton {
42    fn default() -> Self {
43        Self {
44            state: ButtonState::Idle,
45            just_clicked: false,
46            hovered_color: Color::new(0.0, 0.0, 0.0, 1.0),
47            pressed_color: Color::new(0.0, 0.0, 0.0, 1.0),
48            normal_color: Color::new(0.0, 0.0, 0.0, 1.0)
49        }
50    }
51}
52
53#[derive(Deserialize, Debug, Default)]
54pub struct GuiButtonLoaderData {
55    #[serde(default)]
56    pub state: String,
57    #[serde(default)]
58    pub just_clicked: bool,
59    
60    pub hovered_color: ColorData,
61    pub pressed_color: ColorData,
62    pub normal_color: ColorData
63}
64
65pub struct GuiButtonLoader;
66
67impl ComponentLoader for GuiButtonLoader {
68    fn load(&self, ctx: &mut crate::prelude::Context, entity: hecs::Entity, data: &serde_json::Value) {
69        let loader_data: GuiButtonLoaderData = serde_json::from_value(data.clone())
70            .unwrap_or_default();
71
72        let component = GuiButton {
73            state: ButtonState::from_str(loader_data.state.as_str()),
74            just_clicked: loader_data.just_clicked,
75            hovered_color: Color::new(
76                loader_data.hovered_color.r,
77                loader_data.hovered_color.g,
78                loader_data.hovered_color.b,
79                loader_data.hovered_color.a
80            ),
81            pressed_color: Color::new(
82                loader_data.pressed_color.r,
83                loader_data.pressed_color.g,
84                loader_data.pressed_color.b,
85                loader_data.pressed_color.a
86            ),
87            normal_color: Color::new(
88                loader_data.normal_color.r,
89                loader_data.normal_color.g,
90                loader_data.normal_color.b,
91                loader_data.normal_color.a
92            )
93        };
94
95        ctx.world.insert_one(entity, component).expect("Failed to insert GuiButton");
96    }
97}
98
99pub fn button_interaction_system(ctx: &mut Context) {
100    let (mouse_x, mouse_y) = mouse_position();
101    let is_pressed = is_mouse_button_down(MouseButton::Left);
102    let just_clicked = is_mouse_button_pressed(MouseButton::Left);
103
104    let (world, resources) = (&mut ctx.world, &mut ctx.resources);
105
106    // --- READ PHASE ---
107    // We get the read-only resource first.
108    let resolved_rects_map = &resources.get::<UiResolvedRects>()
109        .expect("UiResolvedRects resource is missing")
110        .0;
111
112    // We create a local buffer to store events because we can't 
113    // borrow EventBus mutably while holding resolved_rects_map.
114    let mut events_to_send: Vec<UiClickEvent> = Vec::new();
115
116    let mut query = world.query::<(
117        &mut GuiButton, 
118        &GuiBox, 
119        Option<&GuiAction>, 
120        Option<&Visible>, 
121        Option<&HorizontalAlignment>, 
122        Option<&VerticalAlignment>
123    )>();
124
125    for (entity, (button, gui_box, action_opt, visibility, h_align, v_align)) in query.iter() {
126        let is_visible = visibility.map_or(true, |v| v.0);
127
128        if !is_visible {
129            continue;
130        }
131
132        button.just_clicked = false;
133
134        // We use the read-only map here
135        let (resolved_pos, resolved_size) = 
136            if let Some(rect) = resolved_rects_map.get(&entity) {
137                *rect
138            } else {
139                continue; 
140            };
141
142        if !gui_box.screen_space { continue; }
143
144        let mut x = resolved_pos.x;
145        let mut y = resolved_pos.y;
146        let w = resolved_size.x;
147        let h = resolved_size.y;
148
149        // Apply alignment
150        if let Some(h_align) = h_align {
151            match h_align.0 {
152                HorizontalAlignmentType::Left => {},
153                HorizontalAlignmentType::Center => x -= w / 2.0,
154                HorizontalAlignmentType::Right => x -= w,
155            }
156        }
157        
158        if let Some(v_align) = v_align {
159            match v_align.0 {
160                VerticalAlignmentType::Top => {},
161                VerticalAlignmentType::Center => y -= h / 2.0,
162                VerticalAlignmentType::Bottom => y -= h,
163            }
164        }
165
166        let is_hovered = mouse_x >= x && mouse_x <= (x + w) && mouse_y >= y && mouse_y <= (y + h);
167
168        match button.state {
169            ButtonState::Idle => {
170                if is_hovered { button.state = ButtonState::Hovered; }
171            }
172            ButtonState::Hovered => {
173                if !is_hovered { button.state = ButtonState::Idle; }
174                else if just_clicked { button.state = ButtonState::Pressed; }
175            }
176            ButtonState::Pressed => {
177                if !is_pressed {
178                    if is_hovered {
179                        button.just_clicked = true;
180                        button.state = ButtonState::Hovered;
181
182                        // --- COLLECT PHASE ---
183                        // Instead of sending immediately, we push to the buffer.
184                        if let Some(action) = action_opt {
185                            events_to_send.push(UiClickEvent {
186                                action_id: action.action_id.clone(),
187                                entity,
188                            });
189                        }
190                    } else {
191                        button.state = ButtonState::Idle;
192                    }
193                }
194            }
195        }
196    }
197
198    // --- SEND PHASE ---
199    // The loop is done, so `resolved_rects_map` borrow is dropped (or can be inferred dropped).
200    // We are now free to borrow `resources` mutably to get the EventBus.
201    
202    if !events_to_send.is_empty() {
203        let event_bus = resources.get_mut::<EventBus>()
204            .expect("EventBus resource is missing");
205
206        for event in events_to_send {
207            event_bus.send(event);
208        }
209    }
210}