Skip to main content

zenthra_widgets/
window.rs

1// crates/zenthra-widgets/src/window.rs
2
3use crate::ui::{Ui, DrawCommand};
4use zenthra_core::{Color, Id, Response, Align};
5
6pub struct FloatingWindowBuilder<'u, 'a, 'b> {
7    ui: &'u mut Ui<'a>,
8    id: Id,
9    title: String,
10    is_open: &'b mut bool,
11    pos: &'b mut [f32; 2],
12    
13    // Styling
14    width: f32,
15    height: f32,
16    bg: Color,
17    border_color: Color,
18    border_width: f32,
19    radius: f32,
20    shadow_color: Color,
21    shadow_blur: f32,
22    shadow_offset: [f32; 2],
23    shadow_opacity: f32,
24
25    // Header Styling
26    header_bg: Color,
27    header_text_color: Color,
28    header_height: f32,
29    
30    closable: bool,
31    modal: bool,
32    light_dismiss: bool,
33}
34
35impl<'u, 'a, 'b> FloatingWindowBuilder<'u, 'a, 'b> {
36    pub fn new(ui: &'u mut Ui<'a>, title: &str, is_open: &'b mut bool, pos: &'b mut [f32; 2]) -> Self {
37        let id = ui.id();
38        Self {
39            ui,
40            id,
41            title: title.to_string(),
42            is_open,
43            pos,
44            
45            width: 320.0,
46            height: 400.0,
47            bg: Color::rgb(0.1, 0.1, 0.12),
48            border_color: Color::rgb(0.3, 0.3, 0.35),
49            border_width: 1.0,
50            radius: 8.0,
51            shadow_color: Color::BLACK,
52            shadow_blur: 20.0,
53            shadow_offset: [0.0, 10.0],
54            shadow_opacity: 0.5,
55
56            header_bg: Color::rgb(0.15, 0.15, 0.18),
57            header_text_color: Color::WHITE,
58            header_height: 40.0,
59            
60            closable: true,
61            modal: false,
62            light_dismiss: false,
63        }
64    }
65
66    pub fn modal(mut self, modal: bool) -> Self {
67        self.modal = modal;
68        self
69    }
70
71    pub fn light_dismiss(mut self, dismiss: bool) -> Self {
72        self.light_dismiss = dismiss;
73        self
74    }
75
76    pub fn size(mut self, w: f32, h: f32) -> Self {
77        self.width = w;
78        self.height = h;
79        self
80    }
81
82    pub fn closable(mut self, closable: bool) -> Self {
83        self.closable = closable;
84        self
85    }
86
87    pub fn bg(mut self, bg: Color) -> Self {
88        self.bg = bg;
89        self
90    }
91
92    pub fn border(mut self, border_color: Color, border_width: f32) -> Self {
93        self.border_color = border_color;
94        self.border_width = border_width;
95        self
96    }
97
98    pub fn radius_all(mut self, radius: f32) -> Self {
99        self.radius = radius;
100        self
101    }
102
103    pub fn header_bg(mut self, bg: Color) -> Self {
104        self.header_bg = bg;
105        self
106    }
107
108    pub fn header_text_color(mut self, color: Color) -> Self {
109        self.header_text_color = color;
110        self
111    }
112
113    pub fn header_height(mut self, height: f32) -> Self {
114        self.header_height = height;
115        self
116    }
117
118    pub fn show<F>(self, content: F) -> Response 
119    where F: FnOnce(&mut Ui)
120    {
121        let id = self.id;
122        let z_key = Id::from_u64((id.raw() << 8) | 4);
123        let modal_key = Id::from_u64((id.raw() << 8) | 5);
124        let opened_key = Id::from_u64((id.raw() << 8) | 6);
125
126        if !*self.is_open {
127            self.ui.interaction_state.remove(&z_key);
128            self.ui.interaction_state.remove(&modal_key);
129            self.ui.interaction_state.remove(&opened_key);
130            return Response { clicked: false, hovered: false, pressed: false };
131        }
132
133        let drag_id = Id::from_u64((id.raw() << 8) | 1);
134
135        // If the window was just opened, promote it to the top z-index
136        if !self.ui.interaction_state.contains_key(&z_key) {
137            let max_z_key = Id::from_u64(999999999);
138            let max_z = self.ui.interaction_state.get(&max_z_key).copied().unwrap_or(0.0);
139            let new_z = max_z + 1.0;
140            self.ui.interaction_state.insert(max_z_key, new_z);
141            self.ui.interaction_state.insert(z_key, new_z);
142            self.ui.needs_redraw = true;
143        }
144
145        // Store modal state
146        self.ui.interaction_state.insert(modal_key, if self.modal { 1.0 } else { 0.0 });
147
148        let mut is_dragging = self.ui.interaction_state.get(&drag_id).map(|&v| v > 0.5).unwrap_or(false);
149
150        // Window position
151        let win_x = self.pos[0];
152        let win_y = self.pos[1];
153
154        // Is hovered check (uses occlusion detection internally)
155        let is_hovered = self.ui.is_hovered(id, win_x, win_y, self.width, self.height);
156
157        // Check if the window was already open in the previous frame
158        let was_already_open = self.ui.interaction_state.insert(opened_key, 1.0).is_some();
159
160        {
161            use std::fs::OpenOptions;
162            use std::io::Write;
163            if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("window_debug.log") {
164                let _ = writeln!(
165                    file,
166                    "WINDOW ID: {:?} | is_open: {} | clicked: {} | is_hovered: {} | was_already_open: {}",
167                    id, *self.is_open, self.ui.clicked, is_hovered, was_already_open
168                );
169            }
170        }
171
172        // Light dismiss logic
173        if self.light_dismiss && self.ui.clicked && !is_hovered && was_already_open {
174            *self.is_open = false;
175            {
176                use std::fs::OpenOptions;
177                use std::io::Write;
178                if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("window_debug.log") {
179                    let _ = writeln!(file, "   --> LIGHT DISMISS TRIGGERED! Setting is_open to false.");
180                }
181            }
182            self.ui.interaction_state.remove(&z_key);
183            self.ui.interaction_state.remove(&modal_key);
184            self.ui.interaction_state.remove(&opened_key);
185            self.ui.needs_redraw = true;
186            return Response { clicked: true, hovered: false, pressed: false };
187        }
188
189        // Active focus z-order promotion logic
190        if self.ui.clicked && is_hovered {
191            let max_z_key = Id::from_u64(999999999);
192            let max_z = self.ui.interaction_state.get(&max_z_key).copied().unwrap_or(0.0);
193            let new_z = max_z + 1.0;
194            self.ui.interaction_state.insert(max_z_key, new_z);
195            self.ui.interaction_state.insert(z_key, new_z);
196            self.ui.needs_redraw = true;
197        }
198
199        // Capture start length of overlays
200        let start_len = self.ui.overlays.len();
201
202        let is_open = self.is_open;
203        let pos = self.pos;
204        let ui = self.ui;
205        
206        let title = self.title;
207        let modal = self.modal;
208        let closable = self.closable;
209        let width = self.width;
210        let height = self.height;
211        let bg = self.bg;
212        let border_color = self.border_color;
213        let border_width = self.border_width;
214        let radius = self.radius;
215        let shadow_color = self.shadow_color;
216        let shadow_offset = self.shadow_offset;
217        let shadow_blur = self.shadow_blur;
218        let shadow_opacity = self.shadow_opacity;
219        let header_height = self.header_height;
220        let header_bg = self.header_bg;
221        let header_text_color = self.header_text_color;
222
223        // Set current window ID context
224        let prev_win_id = ui.current_window_id;
225        ui.current_window_id = Some(id);
226
227        ui.overlay(|ui| {
228            // Draw modal backdrop if modal
229            if modal {
230                ui.draws.push(crate::ui::DrawCommand::Rect(crate::ui::RectDraw {
231                    instance: zenthra_render::RectInstance {
232                        pos: [0.0, 0.0],
233                        size: [ui.width, ui.height],
234                        color: Color::rgba(0.0, 0.0, 0.0, 0.4).to_array(),
235                        radius: [0.0; 4],
236                        border_width: 0.0,
237                        border_color: Color::TRANSPARENT.to_array(),
238                        shadow_color: Color::TRANSPARENT.to_array(),
239                        shadow_offset: [0.0, 0.0],
240                        shadow_blur: 0.0,
241                        clip_rect: [0.0, 0.0, 9999.0, 9999.0],
242                        ..Default::default()
243                    }
244                }));
245            }
246
247            ui.container()
248                .absolute(win_x, win_y)
249                .width(width)
250                .height(height)
251                .bg(bg)
252                .border(border_color, border_width)
253                .radius_all(radius)
254                .shadow(shadow_color, shadow_offset[0], shadow_offset[1], shadow_blur)
255                .shadow_opacity(shadow_opacity)
256                .clip(true)
257                .show(|ui| {
258                    let mut close_clicked = false;
259                    let header_res = ui.container()
260                        .full_width()
261                        .height(header_height)
262                        .bg(header_bg)
263                        .padding_x(15.0)
264                        .row()
265                        .halign(Align::SpaceBetween)
266                        .valign(Align::Center)
267                        .show(|ui| {
268                            // Left spacer to balance close button width for centering
269                            ui.spacing(20.0);
270
271                            ui.text(&title)
272                                .color(header_text_color)
273                                .bold()
274                                .show();
275                            
276                            if closable {
277                                if ui.button("×")
278                                    .bg(Color::TRANSPARENT)
279                                    .hover_bg(Color::rgba(1.0, 1.0, 1.0, 0.1))
280                                    .text_color(header_text_color)
281                                    .padding(4.0, 8.0, 4.0, 8.0)
282                                    .radius_all(4.0)
283                                    .size(16.0)
284                                    .show()
285                                    .clicked 
286                                {
287                                    close_clicked = true;
288                                }
289                            } else {
290                                ui.spacing(20.0);
291                            }
292                        });
293
294                    if close_clicked {
295                        *is_open = false;
296                        ui.request_redraw();
297                    }
298
299                    // --- Dragging Logic ---
300                    if header_res.pressed && ui.clicked && !close_clicked {
301                        if !is_dragging {
302                            is_dragging = true;
303                            ui.interaction_state.insert(drag_id, 1.0);
304                            // Store drag offset in interaction state
305                            let ox_id = Id::from_u64((id.raw() << 8) | 2);
306                            let oy_id = Id::from_u64((id.raw() << 8) | 3);
307                            ui.interaction_state.insert(ox_id, ui.mouse_x - win_x);
308                            ui.interaction_state.insert(oy_id, ui.mouse_y - win_y);
309                        }
310                    }
311
312                    if is_dragging {
313                        if ui.mouse_down {
314                            let ox_id = Id::from_u64((id.raw() << 8) | 2);
315                            let oy_id = Id::from_u64((id.raw() << 8) | 3);
316                            let ox = ui.interaction_state.get(&ox_id).cloned().unwrap_or(0.0);
317                            let oy = ui.interaction_state.get(&oy_id).cloned().unwrap_or(0.0);
318                            
319                            pos[0] = ui.mouse_x - ox;
320                            pos[1] = ui.mouse_y - oy;
321                            ui.request_redraw();
322                        } else {
323                            is_dragging = false;
324                            ui.interaction_state.insert(drag_id, 0.0);
325                        }
326                    }
327
328                    // --- Content ---
329                    ui.container()
330                        .full_width()
331                        .fill_y()
332                        .padding_all(15.0)
333                        .show(content);
334                });
335        });
336
337        // Restore window context
338        ui.current_window_id = prev_win_id;
339
340        // Drain window's draw commands from ui.overlays and save in window_overlays
341        let window_cmds = ui.overlays.drain(start_len..).collect::<Vec<_>>();
342        {
343            use std::fs::OpenOptions;
344            use std::io::Write;
345            if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("window_debug.log") {
346                let _ = writeln!(
347                    file,
348                    "WINDOW {} DRAWS: count={}",
349                    title, window_cmds.len()
350                );
351                for (i, cmd) in window_cmds.iter().enumerate() {
352                    match cmd {
353                        DrawCommand::Rect(r) => {
354                            let _ = writeln!(file, "  [{}] Rect: pos={:?}, size={:?}, color={:?}, clip_rect={:?}", i, r.instance.pos, r.instance.size, r.instance.color, r.instance.clip_rect);
355                        }
356                        DrawCommand::Text(t) => {
357                            let _ = writeln!(file, "  [{}] Text: text='{}', pos={:?}, clip={:?}", i, t.text, t.pos, t.clip);
358                        }
359                        DrawCommand::OverlayRect(o) => {
360                            let _ = writeln!(file, "  [{}] OverlayRect: pos={:?}, size={:?}, clip={:?}", i, [o.x, o.y], [o.width, o.height], o.clip);
361                        }
362                        DrawCommand::Image(img) => {
363                            let _ = writeln!(file, "  [{}] Image: pos={:?}, size={:?}, clip_rect={:?}", i, img.instance.pos, img.instance.size, img.instance.clip_rect);
364                        }
365                    }
366                }
367            }
368        }
369        ui.window_overlays.push((id, window_cmds));
370
371        Response {
372            clicked: false,
373            hovered: is_hovered,
374            pressed: is_dragging,
375        }
376    }
377}