1use 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 width: f32,
21 height: f32,
22 radius: [f32; 4],
23 padding: f32,
24
25 on_color: Color,
27 off_color: Color,
28
29 thumb_color: Color,
31 thumb_radius: Option<[f32; 4]>,
32 thumb_width: Option<f32>,
33
34 inner_on: Option<String>,
36 inner_off: Option<String>,
37 inner_text_color: Color,
38 inner_font_size: f32,
39
40 label_size: f32,
42 label_color: Color,
43 active_label_color: Option<Color>,
44 label_gap: f32,
45
46 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 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 let mut label_w = 0.0;
317 let mut label_h = 0.0;
318 if let Some(label_text) = &self.label {
319 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 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 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 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 let mid_x = toggle_x + self.width / 2.0;
401 let mid_y = toggle_y_orig + self.height / 2.0;
402
403 let tx = mid_x - base_w / 2.0;
405 let ty = mid_y - base_h / 2.0;
406
407 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 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 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 let inner_tx = if is_on {
460 tx + (self.padding * final_scale) + (track_w_internal - text_w) / 2.0
462 } else {
463 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 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 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 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 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}