1use 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 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_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 !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 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 let win_x = self.pos[0];
152 let win_y = self.pos[1];
153
154 let is_hovered = self.ui.is_hovered(id, win_x, win_y, self.width, self.height);
156
157 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 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 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 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 let prev_win_id = ui.current_window_id;
225 ui.current_window_id = Some(id);
226
227 ui.overlay(|ui| {
228 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 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 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 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 ui.container()
330 .full_width()
331 .fill_y()
332 .padding_all(15.0)
333 .show(content);
334 });
335 });
336
337 ui.current_window_id = prev_win_id;
339
340 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}