Skip to main content

zenthra_widgets/controls/
checkbox.rs

1// crates/zenthra-widgets/src/controls/checkbox.rs
2
3use 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    // Layout
14    size: f32,
15    radius: [f32; 4],
16    gap: f32,
17
18    // Visuals (Unchecked)
19    bg: Color,
20    border_color: Color,
21    border_width: f32,
22
23    // Visuals (Checked)
24    check_bg: Color,
25    check_color: Color,
26
27    // Label
28    label_size: f32,
29    label_color: Color,
30    active_label_color: Option<Color>,
31
32    // Premium Effects
33    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    // Interaction
44    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            // Default Layout
59            size: 18.0,
60            radius: [4.0; 4],
61            gap: 8.0,
62
63            // Default Visuals
64            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        // 1. Measure Text
272        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        // 2. Hit-testing (Use recorded layout for accurate positioning in containers)
287        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        // 3. Animation (Generic & Selection)
313        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        // Color Logic
342        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        // Visual Midpoints
352        let mid_x = x + self.size / 2.0;
353        let mid_y = y + (total_h / 2.0);
354
355        // Scaled dimensions
356        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        // 4. Draw Box
361        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        // 5. Draw Check Indicator (Unicode Checkmark)
391        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                // Scale the checkmark slightly smaller than the box
399                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        // 6. Draw Label (Color Transition)
421        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        // 7. Register Semantic & Advance
441        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}