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 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 !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 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 let win_x = self.pos[0];
159 let win_y = self.pos[1];
160
161 let is_hovered = self.ui.is_hovered(id, win_x, win_y, self.width, self.height);
163
164 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 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 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 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 let prev_win_id = ui.current_window_id;
232 ui.current_window_id = Some(id);
233
234 ui.overlay(|ui| {
235 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 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 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 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 ui.container()
340 .full_width()
341 .fill_y()
342 .padding_all(15.0)
343 .show(content);
344 });
345 });
346
347 ui.current_window_id = prev_win_id;
349
350 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}