Skip to main content

zenthra_widgets/controls/
toggle.rs

1// crates/zenthra-widgets/src/controls/toggle.rs
2
3use crate::ui::{DrawCommand, RectDraw, TextDraw, Ui};
4use zenthra_core::{Color, Id, Rect, Response, Role, SemanticNode};
5use zenthra_render::RectInstance;
6
7pub enum LabelSide {
8    Left,
9    Right,
10}
11
12pub struct ToggleBuilder<'u, 'a, 'b> {
13    ui: &'u mut Ui<'a>,
14    state: &'b mut bool,
15    id: Id,
16    label: Option<String>,
17    label_side: LabelSide,
18
19    // Track Style
20    width: f32,
21    height: f32,
22    radius: [f32; 4],
23    padding: f32,
24    
25    // Colors
26    on_color: Color,
27    off_color: Color,
28    
29    // Thumb
30    thumb_color: Color,
31    thumb_radius: Option<[f32; 4]>,
32    thumb_width: Option<f32>,
33
34    // Inner Labels
35    inner_on: Option<String>,
36    inner_off: Option<String>,
37    inner_text_color: Color,
38    inner_font_size: f32,
39
40    // External Label Style
41    label_size: f32,
42    label_color: Color,
43    active_label_color: Option<Color>,
44    label_gap: f32,
45
46    // Premium Effects
47    glow: bool,
48    shadow_color: Color,
49    shadow_offset: [f32; 2],
50    shadow_blur: f32,
51    shadow_opacity: f32,
52    shadow_enabled: bool,
53    hover_scale: f32,
54    pressed_scale: f32,
55
56    // State
57    disabled: bool,
58    animation_speed: f32,
59    is_pill: bool,
60    hover_brightness: f32,
61    opacity: f32,
62}
63
64impl<'u, 'a, 'b> ToggleBuilder<'u, 'a, 'b> {
65    pub fn new(ui: &'u mut Ui<'a>, state: &'b mut bool, label: Option<&str>) -> Self {
66        let id = ui.id();
67        Self {
68            ui,
69            state,
70            id,
71            label: label.map(|s| s.to_string()),
72            label_side: LabelSide::Right,
73
74            width: 48.0,
75            height: 24.0,
76            radius: [12.0; 4],
77            padding: 3.0,
78
79            on_color: Color::rgb(0.0, 0.5, 1.0),
80            off_color: Color::rgb(0.2, 0.2, 0.2),
81            thumb_color: Color::WHITE,
82            thumb_radius: None,
83            thumb_width: None,
84
85            inner_on: None,
86            inner_off: None,
87            inner_text_color: Color::WHITE,
88            inner_font_size: 10.0,
89
90            label_size: 14.0,
91            label_color: Color::WHITE,
92            active_label_color: None,
93            label_gap: 10.0,
94
95            glow: false,
96            shadow_enabled: false,
97            shadow_color: Color::rgb(0.0, 0.0, 0.0),
98            shadow_offset: [0.0, 2.0],
99            shadow_blur: 8.0,
100            shadow_opacity: 0.4,
101            hover_scale: 1.0,
102            pressed_scale: 1.0,
103
104            disabled: false,
105            animation_speed: 15.0,
106            is_pill: true,
107            hover_brightness: 1.05,
108            opacity: 1.0,
109        }
110    }
111
112    pub fn id(mut self, id: impl std::hash::Hash) -> Self {
113        let mut hasher = std::collections::hash_map::DefaultHasher::new();
114        use std::hash::Hasher;
115        id.hash(&mut hasher);
116        self.id = Id::from_u64(hasher.finish());
117        self
118    }
119
120    pub fn size(mut self, w: f32, h: f32) -> Self {
121        self.width = w;
122        self.height = h;
123        self
124    }
125    pub fn width(mut self, w: f32) -> Self {
126        self.width = w;
127        self
128    }
129    pub fn height(mut self, h: f32) -> Self {
130        self.height = h;
131        self
132    }
133    pub fn padding(mut self, p: f32) -> Self {
134        self.padding = p;
135        self
136    }
137    pub fn opacity(mut self, a: f32) -> Self {
138        self.opacity = a;
139        self
140    }
141
142    pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
143        self.radius = [tl, tr, br, bl];
144        self.is_pill = false;
145        self
146    }
147
148    pub fn radius_all(mut self, r: f32) -> Self {
149        self.radius = [r; 4];
150        self.is_pill = false;
151        self
152    }
153
154    pub fn radius_top(mut self, r: f32) -> Self {
155        self.radius[0] = r;
156        self.radius[1] = r;
157        self.is_pill = false;
158        self
159    }
160
161    pub fn radius_bottom(mut self, r: f32) -> Self {
162        self.radius[2] = r;
163        self.radius[3] = r;
164        self.is_pill = false;
165        self
166    }
167
168    pub fn radius_top_left(mut self, r: f32) -> Self {
169        self.radius[0] = r;
170        self.is_pill = false;
171        self
172    }
173
174    pub fn radius_top_right(mut self, r: f32) -> Self {
175        self.radius[1] = r;
176        self.is_pill = false;
177        self
178    }
179
180    pub fn radius_bottom_right(mut self, r: f32) -> Self {
181        self.radius[2] = r;
182        self.is_pill = false;
183        self
184    }
185
186    pub fn radius_bottom_left(mut self, r: f32) -> Self {
187        self.radius[3] = r;
188        self.is_pill = false;
189        self
190    }
191
192    pub fn radius_left(mut self, r: f32) -> Self {
193        self.radius[0] = r;
194        self.radius[3] = r;
195        self.is_pill = false;
196        self
197    }
198
199    pub fn radius_right(mut self, r: f32) -> Self {
200        self.radius[1] = r;
201        self.radius[2] = r;
202        self.is_pill = false;
203        self
204    }
205
206    pub fn square(mut self) -> Self {
207        self.radius = [0.0; 4];
208        self.is_pill = false;
209        self
210    }
211
212    pub fn pill(mut self) -> Self {
213        self.is_pill = true;
214        self
215    }
216
217    pub fn inner_labels(mut self, on: &str, off: &str) -> Self {
218        self.inner_on = Some(on.to_string());
219        self.inner_off = Some(off.to_string());
220        self
221    }
222
223    pub fn label_left(mut self) -> Self {
224        self.label_side = LabelSide::Left;
225        self
226    }
227
228    pub fn disabled(mut self, disabled: bool) -> Self {
229        self.disabled = disabled;
230        self
231    }
232
233    pub fn colors(mut self, on: Color, off: Color, thumb: Color) -> Self {
234        self.on_color = on;
235        self.off_color = off;
236        self.thumb_color = thumb;
237        self
238    }
239
240    pub fn thumb_radius(mut self, r: f32) -> Self {
241        self.thumb_radius = Some([r; 4]);
242        self
243    }
244
245    pub fn inner_color(mut self, color: Color) -> Self {
246        self.inner_text_color = color;
247        self
248    }
249
250    pub fn inner_size(mut self, size: f32) -> Self {
251        self.inner_font_size = size;
252        self
253    }
254
255    pub fn label_size(mut self, size: f32) -> Self {
256        self.label_size = size;
257        self
258    }
259
260    pub fn label_color(mut self, color: Color) -> Self {
261        self.label_color = color;
262        self
263    }
264
265    pub fn active_label(mut self, color: Color) -> Self {
266        self.active_label_color = Some(color);
267        self
268    }
269
270    pub fn glow(mut self, enabled: bool) -> Self {
271        self.glow = enabled;
272        self
273    }
274
275    pub fn scaling(mut self, hover: f32, pressed: f32) -> Self {
276        self.hover_scale = hover;
277        self.pressed_scale = pressed;
278        self
279    }
280
281    pub fn label_gap(mut self, gap: f32) -> Self {
282        self.label_gap = gap;
283        self
284    }
285
286    pub fn label_right(mut self) -> Self {
287        self.label_side = LabelSide::Right;
288        self
289    }
290
291    pub fn animation_speed(mut self, speed: f32) -> Self {
292        self.animation_speed = speed;
293        self
294    }
295
296    pub fn hover_brightness(mut self, b: f32) -> Self {
297        self.hover_brightness = b;
298        self
299    }
300    pub fn shadow(mut self, color: Color, x: f32, y: f32, blur: f32) -> Self {
301        self.shadow_color = color;
302        self.shadow_offset = [x, y];
303        self.shadow_blur = blur;
304        self.shadow_enabled = true;
305        self
306    }
307    pub fn shadow_opacity(mut self, opacity: f32) -> Self {
308        self.shadow_opacity = opacity;
309        self
310    }
311
312    pub fn show(self) -> Response {
313        let (x, y) = (self.ui.cursor_x, self.ui.cursor_y);
314        
315        // 1. Measure External Label
316        let mut label_w = 0.0;
317        let mut label_h = 0.0;
318        if let Some(label_text) = &self.label {
319            // Use the internal text measurement helper
320            if let Some(fs) = self.ui.font_system.as_ref() {
321                let mut adapter = zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
322                let options = zenthra_text::prelude::TextOptions::new().font_size(self.label_size);
323                let buffer = adapter.shape(label_text, &options);
324                let (cw, ch) = buffer.content_size();
325                label_w = cw;
326                label_h = ch;
327            }
328        }
329
330        let total_w = if label_w > 0.0 { self.width + self.label_gap + label_w } else { self.width };
331        let total_h = self.height.max(label_h);
332
333        // 2. Hit-testing
334        let (actual_ox, actual_oy, actual_w, actual_h) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
335            (
336                rect.origin.x + self.ui.offset_x,
337                rect.origin.y + self.ui.offset_y,
338                rect.size.width.max(total_w),
339                rect.size.height.max(total_h)
340            )
341        } else {
342            (x + self.ui.offset_x, y + self.ui.offset_y, total_w, total_h)
343        };
344
345        let is_hovered = self.ui.is_hovered(self.id, actual_ox, actual_oy, actual_w, actual_h) && !self.disabled;
346        let is_pressed = is_hovered && self.ui.mouse_down;
347        let mut clicked = false;
348
349        if self.ui.clicked && is_hovered {
350            *self.state = !*self.state;
351            self.ui.needs_redraw = true;
352            clicked = true;
353        }
354
355        // 3. Animation (Generic & Selection)
356        let sel_target = if *self.state { 1.0 } else { 0.0 };
357        let current_sel = *self.ui.interaction_state.entry(self.id).or_insert(sel_target);
358
359        let hover_id = Id::from_u64(self.id.raw().wrapping_add(3000000));
360        let hover_target = if is_pressed { self.pressed_scale } else if is_hovered { self.hover_scale } else { 1.0 };
361        let current_scale = *self.ui.interaction_state.entry(hover_id).or_insert(1.0);
362
363        let mut final_pos = current_sel;
364        let mut final_scale = current_scale;
365        
366        let s_delta = sel_target - current_sel;
367        if s_delta.abs() > 0.001 {
368            final_pos += s_delta * (0.15 * self.animation_speed).min(1.0);
369            self.ui.interaction_state.insert(self.id, final_pos);
370            self.ui.needs_redraw = true;
371        } else {
372            final_pos = sel_target;
373        }
374
375        let h_delta = hover_target - current_scale;
376        if h_delta.abs() > 0.001 {
377            final_scale += h_delta * 0.3;
378            self.ui.interaction_state.insert(hover_id, final_scale);
379            self.ui.needs_redraw = true;
380        } else {
381            final_scale = hover_target;
382        }
383
384        let start_draw = self.ui.draws.len();
385
386        // 4. Calculate Layout & Radius
387        let base_w = self.width * final_scale;
388        let base_h = self.height * final_scale;
389        
390        let current_radius = if self.is_pill { [base_h / 2.0; 4] } else { 
391            let mut r = self.radius;
392            for i in 0..4 { r[i] *= final_scale; }
393            r
394        };
395        
396        let toggle_x = if let LabelSide::Left = self.label_side { x + label_w + self.label_gap } else { x };
397        let toggle_y_orig = y + (total_h - self.height) / 2.0;
398        
399        // Midpoint of the original toggle track
400        let mid_x = toggle_x + self.width / 2.0;
401        let mid_y = toggle_y_orig + self.height / 2.0;
402        
403        // Final position for scaled track
404        let tx = mid_x - base_w / 2.0;
405        let ty = mid_y - base_h / 2.0;
406
407        // 5. Draw Track
408        let mut track_color = if final_pos > 0.5 { self.on_color } else { self.off_color };
409        track_color.a *= self.opacity;
410        if self.disabled {
411            track_color.a *= 0.5;
412        }
413
414        self.ui.draws.push(DrawCommand::Rect(RectDraw {
415            instance: RectInstance {
416                pos: [tx, ty],
417                size: [base_w, base_h],
418                color: track_color.to_array(),
419                radius: current_radius,
420                brightness: if is_hovered { self.hover_brightness } else { 1.0 },
421                shadow_color: if self.shadow_enabled {
422                    let mut a = self.shadow_color.to_array();
423                    a[3] *= self.shadow_opacity;
424                    a
425                } else if self.glow {
426                    let mut sc = self.on_color.to_array();
427                    sc[3] *= 0.4 * final_pos;
428                    sc
429                } else { [0.0, 0.0, 0.0, 0.0] },
430                shadow_offset: if self.shadow_enabled { self.shadow_offset } else { [0.0, 0.0] },
431                shadow_blur: if self.shadow_enabled { self.shadow_blur } else if self.glow { 12.0 * final_pos } else { 0.0 },
432                ..Default::default()
433            }
434        }));
435
436        // 6. Draw Inner Labels (clipped to track)
437        let clip = [tx, ty, base_w, base_h];
438        if let Some(on_text) = &self.inner_on {
439            if let Some(off_text) = &self.inner_off {
440                // Determine which text to show and where
441                let (text, is_on) = if final_pos > 0.5 { (on_text, true) } else { (off_text, false) };
442                
443                let mut text_w = 0.0;
444                let mut text_h = 0.0;
445                if let Some(fs) = self.ui.font_system.as_ref() {
446                    let mut adapter = zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
447                    let options = zenthra_text::prelude::TextOptions::new().font_size(self.inner_font_size * final_scale);
448                    let buffer = adapter.shape(text, &options);
449                    let (cw, ch) = buffer.content_size();
450                    text_w = cw;
451                    text_h = ch;
452                }
453
454                let thumb_h_adj = base_h - (self.padding * 2.0 * final_scale);
455                let thumb_w_adj = self.thumb_width.map(|w| w * final_scale).unwrap_or(thumb_h_adj);
456                let track_w_internal = base_w - thumb_w_adj - (self.padding * 2.0 * final_scale);
457                
458                // Position text in the space NOT occupied by the thumb
459                let inner_tx = if is_on {
460                    // ON state: thumb is on the right, text on the left
461                    tx + (self.padding * final_scale) + (track_w_internal - text_w) / 2.0
462                } else {
463                    // OFF state: thumb is on the left, text on the right
464                    tx + (self.padding * final_scale) + thumb_w_adj + (track_w_internal - text_w) / 2.0
465                };
466                let inner_ty = ty + (base_h - text_h) / 2.0;
467
468                // Adjust opacity based on animation progress to avoid flickering
469                let mut t_color = self.inner_text_color;
470                let opacity = if is_on { (final_pos - 0.5) * 2.0 } else { (0.5 - final_pos) * 2.0 };
471                t_color.a *= opacity.clamp(0.0, 1.0) * self.opacity;
472
473                if t_color.a > 0.05 {
474                    self.ui.draws.push(DrawCommand::Text(TextDraw {
475                        text: text.clone(),
476                        pos: [inner_tx, inner_ty],
477                        options: zenthra_text::prelude::TextOptions::new()
478                            .font_size(self.inner_font_size * final_scale)
479                            .color(t_color),
480                        clip,
481                    }));
482                }
483            }
484        }
485
486        // 7. Draw Thumb
487        let thumb_h_adj = base_h - (self.padding * 2.0 * final_scale);
488        let thumb_w_adj = self.thumb_width.map(|w| w * final_scale).unwrap_or(thumb_h_adj);
489        let move_range = base_w - thumb_w_adj - (self.padding * 2.0 * final_scale);
490        let thumb_x = tx + (self.padding * final_scale) + (final_pos * move_range);
491        let thumb_y = ty + (self.padding * final_scale);
492
493        let t_radius = self.thumb_radius.map(|mut r| { for i in 0..4 { r[i] *= final_scale; } r }).unwrap_or_else(|| {
494            let r = thumb_h_adj / 2.0;
495            if self.is_pill { [r; 4] } else { 
496                let tr = self.radius[0] * final_scale;
497                if tr < r { [tr; 4] } else { [r; 4] }
498            }
499        });
500
501        let mut t_color = self.thumb_color;
502        t_color.a *= self.opacity;
503        if self.disabled { t_color.a *= 0.8; }
504
505        self.ui.draws.push(DrawCommand::Rect(RectDraw {
506            instance: RectInstance {
507                pos: [thumb_x, thumb_y],
508                size: [thumb_w_adj, thumb_h_adj],
509                color: t_color.to_array(),
510                radius: t_radius,
511                ..Default::default()
512            }
513        }));
514
515        // 8. Draw External Label (Color Transition)
516        if let Some(label_text) = &self.label {
517            let lx = if let LabelSide::Left = self.label_side { x } else { toggle_x + self.width + self.label_gap };
518            let ly = y + (total_h - label_h) / 2.0;
519            
520            let mut l_color = self.label_color;
521            if let Some(active_c) = self.active_label_color {
522                if final_pos > 0.5 {
523                    l_color = active_c;
524                }
525            }
526            l_color.a *= self.opacity;
527
528            self.ui.draws.push(DrawCommand::Text(TextDraw {
529                text: label_text.clone(),
530                pos: [lx, ly],
531                options: zenthra_text::prelude::TextOptions::new()
532                    .font_size(self.label_size)
533                    .color(l_color),
534                clip: [0.0, 0.0, 9999.0, 9999.0],
535            }));
536        }
537
538        // 9. Register Semantic & Advance
539        self.ui.register_semantic(
540            SemanticNode::new(self.id, Role::Switch, Rect::new(x, y, total_w, total_h))
541                .with_label(self.label.clone().unwrap_or_default())
542                .with_value(if *self.state { 1.0 } else { 0.0 }),
543        );
544
545        self.ui.record_layout(self.id, Rect::new(x, y, total_w, total_h));
546        self.ui.advance(total_w, total_h, start_draw);
547
548        Response {
549            clicked,
550            hovered: is_hovered,
551            pressed: is_hovered && self.ui.mouse_down,
552        }
553    }
554}