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