1use crate::ui::{DrawCommand, RectDraw, TextDraw, Ui};
4use zenthra_core::{Color, Id, Rect, Response, Role, SemanticNode};
5use zenthra_render::RectInstance;
6
7pub struct CheckboxBuilder<'u, 'a, 'b> {
8 ui: &'u mut Ui<'a>,
9 value: &'b mut bool,
10 label: String,
11 id: Id,
12
13 size: f32,
15 radius: [f32; 4],
16 gap: f32,
17
18 bg: Color,
20 border_color: Color,
21 border_width: f32,
22
23 check_bg: Color,
25 check_color: Color,
26
27 label_size: f32,
29 label_color: Color,
30 active_label_color: Option<Color>,
31
32 glow: bool,
34 shadow_color: Color,
35 shadow_offset: [f32; 2],
36 shadow_blur: f32,
37 shadow_opacity: f32,
38 shadow_enabled: bool,
39 hover_scale: f32,
40 pressed_scale: f32,
41 hover_brightness: f32,
42
43 hover_bg: Option<Color>,
45 active_bg: Option<Color>,
46}
47
48impl<'u, 'a, 'b> CheckboxBuilder<'u, 'a, 'b> {
49 pub fn new(ui: &'u mut Ui<'a>, value: &'b mut bool, label: &str) -> Self {
50 let id = ui.id();
51
52 Self {
53 ui,
54 value,
55 label: label.to_string(),
56 id,
57
58 size: 18.0,
60 radius: [4.0; 4],
61 gap: 8.0,
62
63 bg: Color::rgb(0.15, 0.15, 0.15),
65 border_color: Color::rgb(0.3, 0.3, 0.3),
66 border_width: 1.0,
67
68 check_bg: Color::rgb(0.2, 0.5, 1.0),
69 check_color: Color::WHITE,
70
71 label_size: 14.0,
72 label_color: Color::WHITE,
73 active_label_color: None,
74
75 glow: false,
76 shadow_enabled: false,
77 shadow_color: Color::rgb(0.0, 0.0, 0.0),
78 shadow_offset: [0.0, 2.0],
79 shadow_blur: 8.0,
80 shadow_opacity: 0.4,
81 hover_scale: 1.0,
82 pressed_scale: 1.0,
83
84 hover_bg: None,
85 active_bg: None,
86 hover_brightness: 1.0,
87 }
88 }
89
90 pub fn size(mut self, size: f32) -> Self {
91 self.size = size;
92 self
93 }
94
95 pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
96 self.radius = [tl, tr, br, bl];
97 self
98 }
99
100 pub fn radius_all(mut self, r: f32) -> Self {
101 self.radius = [r; 4];
102 self
103 }
104
105 pub fn radius_top(mut self, r: f32) -> Self {
106 self.radius[0] = r;
107 self.radius[1] = r;
108 self
109 }
110
111 pub fn radius_bottom(mut self, r: f32) -> Self {
112 self.radius[2] = r;
113 self.radius[3] = r;
114 self
115 }
116
117 pub fn radius_top_left(mut self, r: f32) -> Self {
118 self.radius[0] = r;
119 self
120 }
121
122 pub fn radius_top_right(mut self, r: f32) -> Self {
123 self.radius[1] = r;
124 self
125 }
126
127 pub fn radius_bottom_right(mut self, r: f32) -> Self {
128 self.radius[2] = r;
129 self
130 }
131
132 pub fn radius_bottom_left(mut self, r: f32) -> Self {
133 self.radius[3] = r;
134 self
135 }
136
137 pub fn radius_left(mut self, r: f32) -> Self {
138 self.radius[0] = r;
139 self.radius[3] = r;
140 self
141 }
142
143 pub fn radius_right(mut self, r: f32) -> Self {
144 self.radius[1] = r;
145 self.radius[2] = r;
146 self
147 }
148
149 pub fn gap(mut self, gap: f32) -> Self {
150 self.gap = gap;
151 self
152 }
153
154 pub fn bg(mut self, color: Color) -> Self {
155 self.bg = color;
156 self
157 }
158
159 pub fn check_bg(mut self, color: Color) -> Self {
160 self.check_bg = color;
161 self
162 }
163
164 pub fn check_color(mut self, color: Color) -> Self {
165 self.check_color = color;
166 self
167 }
168
169 pub fn label_size(mut self, size: f32) -> Self {
170 self.label_size = size;
171 self
172 }
173
174 pub fn label_color(mut self, color: Color) -> Self {
175 self.label_color = color;
176 self
177 }
178
179 pub fn border(mut self, color: Color, weight: f32) -> Self {
180 self.border_color = color;
181 self.border_width = weight;
182 self
183 }
184
185 pub fn active_label(mut self, color: Color) -> Self {
186 self.active_label_color = Some(color);
187 self
188 }
189
190 pub fn glow(mut self, enabled: bool) -> Self {
191 self.glow = enabled;
192 self
193 }
194
195 pub fn scaling(mut self, hover: f32, pressed: f32) -> Self {
196 self.hover_scale = hover;
197 self.pressed_scale = pressed;
198 self
199 }
200
201 pub fn hover_bg(mut self, color: Color) -> Self {
202 self.hover_bg = Some(color);
203 self
204 }
205
206 pub fn active_bg(mut self, color: Color) -> Self {
207 self.active_bg = Some(color);
208 self
209 }
210
211 pub fn hover_brightness(mut self, b: f32) -> Self {
212 self.hover_brightness = b;
213 self
214 }
215
216 pub fn id(mut self, id: impl std::hash::Hash) -> Self {
217 let mut hasher = std::collections::hash_map::DefaultHasher::new();
218 use std::hash::Hasher;
219 id.hash(&mut hasher);
220 self.id = Id::from_u64(hasher.finish());
221 self
222 }
223
224 pub fn on_click<F>(self, mut f: F) -> Self
225 where
226 F: FnMut() + 'a,
227 {
228 self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
229 if let crate::ui::WidgetEvent::Click = event {
230 f();
231 }
232 });
233 self
234 }
235
236 pub fn on_change<F>(self, mut f: F) -> Self
237 where
238 F: FnMut(bool) + 'a,
239 {
240 self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
241 if let crate::ui::WidgetEvent::Change(crate::ui::EventValue::Bool(val)) = event {
242 f(*val);
243 }
244 });
245 self
246 }
247
248 pub fn on_hover<F>(self, mut f: F) -> Self
249 where
250 F: FnMut(bool) + 'a,
251 {
252 self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
253 if let crate::ui::WidgetEvent::Hover(hovered) = event {
254 f(*hovered);
255 }
256 });
257 self
258 }
259
260 pub fn on_event<F>(self, phase: crate::ui::EventPhase, f: F) -> Self
261 where
262 F: FnMut(&mut crate::ui::EventContext, &crate::ui::WidgetEvent) + 'a,
263 {
264 self.ui.add_listener(self.id, phase, f);
265 self
266 }
267
268 pub fn show(self) -> Response {
269 let (x, y) = (self.ui.cursor_x, self.ui.cursor_y);
270
271 let mut text_w = 0.0;
273 let mut text_h = 0.0;
274 if let Some(fs) = self.ui.font_system.as_ref() {
275 let mut adapter = zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
276 let options = zenthra_text::prelude::TextOptions::new().font_size(self.label_size);
277 let buffer = adapter.shape(&self.label, &options);
278 let (cw, ch) = buffer.content_size();
279 text_w = cw;
280 text_h = ch;
281 }
282
283 let total_w = self.size + self.gap + text_w;
284 let total_h = self.size.max(text_h);
285
286 let (actual_ox, actual_oy, actual_w, actual_h) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
288 (
289 rect.origin.x + self.ui.offset_x,
290 rect.origin.y + self.ui.offset_y,
291 if rect.size.width > 0.0 { rect.size.width } else { total_w },
292 if rect.size.height > 0.0 { rect.size.height } else { total_h }
293 )
294 } else {
295 (x + self.ui.offset_x, y + self.ui.offset_y, total_w, total_h)
296 };
297
298 let is_hovered = self.ui.is_hovered(self.id, actual_ox, actual_oy, actual_w, actual_h);
299 let is_pressed = is_hovered && self.ui.mouse_down;
300 let mut clicked = false;
301
302 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Hover(is_hovered));
303
304 if self.ui.clicked && is_hovered {
305 *self.value = !*self.value;
306 self.ui.needs_redraw = true;
307 clicked = true;
308 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Click);
309 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Change(crate::ui::EventValue::Bool(*self.value)));
310 }
311
312 let sel_target = if *self.value { 1.0 } else { 0.0 };
314 let current_sel = *self.ui.interaction_state.entry(self.id).or_insert(sel_target);
315
316 let hover_id = Id::from_u64(self.id.raw().wrapping_add(2000000));
317 let hover_target = if is_pressed { self.pressed_scale } else if is_hovered { self.hover_scale } else { 1.0 };
318 let current_scale = *self.ui.interaction_state.entry(hover_id).or_insert(1.0);
319
320 let mut final_sel = current_sel;
321 let mut final_scale = current_scale;
322
323 let s_delta = sel_target - current_sel;
324 if s_delta.abs() > 0.001 {
325 final_sel += s_delta * 0.5;
326 self.ui.interaction_state.insert(self.id, final_sel);
327 self.ui.needs_redraw = true;
328 } else {
329 final_sel = sel_target;
330 }
331
332 let h_delta = hover_target - current_scale;
333 if h_delta.abs() > 0.001 {
334 final_scale += h_delta * 0.3;
335 self.ui.interaction_state.insert(hover_id, final_scale);
336 self.ui.needs_redraw = true;
337 } else {
338 final_scale = hover_target;
339 }
340
341 let mut current_bg = if *self.value { self.check_bg } else { self.bg };
343 if is_pressed {
344 if let Some(c) = self.active_bg { current_bg = c; }
345 } else if is_hovered {
346 if let Some(c) = self.hover_bg { current_bg = c; }
347 }
348
349 let start_draw = self.ui.draws.len();
350
351 let mid_x = x + self.size / 2.0;
353 let mid_y = y + (total_h / 2.0);
354
355 let base_size = self.size * final_scale;
357 let bx = mid_x - base_size / 2.0;
358 let by = mid_y - base_size / 2.0;
359
360 self.ui.draws.push(DrawCommand::Rect(RectDraw {
362 instance: RectInstance {
363 pos: [bx, by],
364 size: [base_size, base_size],
365 color: current_bg.to_array(),
366 radius: [
367 self.radius[0] * final_scale,
368 self.radius[1] * final_scale,
369 self.radius[2] * final_scale,
370 self.radius[3] * final_scale,
371 ],
372 border_width: self.border_width,
373 border_color: self.border_color.to_array(),
374 brightness: if is_hovered { self.hover_brightness } else { 1.0 },
375 shadow_color: if self.shadow_enabled {
376 let mut a = self.shadow_color.to_array();
377 a[3] *= self.shadow_opacity;
378 a
379 } else if self.glow && *self.value {
380 let mut sc = self.check_bg.to_array();
381 sc[3] *= 0.4 * final_sel;
382 sc
383 } else { [0.0, 0.0, 0.0, 0.0] },
384 shadow_offset: if self.shadow_enabled { self.shadow_offset } else { [0.0, 0.0] },
385 shadow_blur: if self.shadow_enabled { self.shadow_blur } else if self.glow && *self.value { 10.0 * final_sel } else { 0.0 },
386 ..Default::default()
387 },
388 }));
389
390 if *self.value {
392 let check_str = "✓";
393 let mut check_w = 0.0;
394 let mut check_h = 0.0;
395
396 if let Some(fs) = self.ui.font_system.as_ref() {
397 let mut adapter = zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
398 let check_font_size = self.size * 0.8;
400 let options = zenthra_text::prelude::TextOptions::new().font_size(check_font_size);
401 let buffer = adapter.shape(check_str, &options);
402 let (cw, ch) = buffer.content_size();
403 check_w = cw;
404 check_h = ch;
405 }
406
407 let cx = x + (self.size - check_w) / 2.0;
408 let cy = y + (total_h - check_h) / 2.0;
409
410 self.ui.draws.push(DrawCommand::Text(TextDraw {
411 text: check_str.to_string(),
412 pos: [cx, cy],
413 options: zenthra_text::prelude::TextOptions::new()
414 .font_size(self.size * 0.8)
415 .color(self.check_color),
416 clip: [0.0, 0.0, 9999.0, 9999.0],
417 }));
418 }
419
420 let lx = x + self.size + self.gap;
422 let ly = y + (total_h - text_h) / 2.0;
423
424 let mut l_color = self.label_color;
425 if let Some(active_c) = self.active_label_color {
426 if final_sel > 0.5 {
427 l_color = active_c;
428 }
429 }
430
431 self.ui.draws.push(DrawCommand::Text(TextDraw {
432 text: self.label.clone(),
433 pos: [lx, ly],
434 options: zenthra_text::prelude::TextOptions::new()
435 .font_size(self.label_size)
436 .color(l_color),
437 clip: [0.0, 0.0, 9999.0, 9999.0],
438 }));
439
440 self.ui.register_semantic(
442 SemanticNode::new(self.id, Role::CheckBox, Rect::new(x, y, total_w, total_h))
443 .with_label(self.label.clone())
444 .with_value(if *self.value { 1.0 } else { 0.0 }),
445 );
446
447 self.ui.record_layout(self.id, Rect::new(x, y, total_w, total_h));
448 self.ui.advance(total_w, total_h, start_draw);
449
450 Response {
451 clicked,
452 hovered: is_hovered,
453 pressed: is_pressed,
454 }
455 }
456}