1use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14pub struct ToggleSwitch {
16 pub rect: Rect,
18 pub on: bool,
20 pub focused: bool,
22 pub label: Option<String>,
24 pub label_size: f32,
26 pub label_color: ColorLinPremul,
28 pub on_color: ColorLinPremul,
30 pub off_color: ColorLinPremul,
32 pub thumb_color: ColorLinPremul,
34 pub border_color: ColorLinPremul,
36 pub border_width: f32,
38 pub validation_error: Option<String>,
40 pub focus_id: FocusId,
42}
43
44impl ToggleSwitch {
45 pub const DEFAULT_WIDTH: f32 = 44.0;
47 pub const DEFAULT_HEIGHT: f32 = 24.0;
49 const THUMB_PADDING: f32 = 2.0;
51
52 pub fn new() -> Self {
54 Self {
55 rect: Rect {
56 x: 0.0,
57 y: 0.0,
58 w: Self::DEFAULT_WIDTH,
59 h: Self::DEFAULT_HEIGHT,
60 },
61 on: false,
62 focused: false,
63 label: None,
64 label_size: 14.0,
65 label_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
66 on_color: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
67 off_color: ColorLinPremul::from_srgba_u8([120, 120, 120, 255]),
68 thumb_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
69 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
70 border_width: 0.0,
71 validation_error: None,
72 focus_id: FocusId(0),
73 }
74 }
75
76 pub fn toggle(&mut self) {
78 self.on = !self.on;
79 }
80
81 pub fn hit_test_track(&self, x: f32, y: f32) -> bool {
83 x >= self.rect.x
84 && x <= self.rect.x + self.rect.w
85 && y >= self.rect.y
86 && y <= self.rect.y + self.rect.h
87 }
88
89 pub fn hit_test_label(&self, x: f32, y: f32) -> bool {
91 if let Some(label) = &self.label {
92 let label_x = self.rect.x + self.rect.w + 8.0;
93 let char_width = self.label_size * 0.5;
94 let label_width = label.len() as f32 * char_width;
95 let clickable_height = self.rect.h.max(self.label_size * 1.2);
96
97 x >= label_x
98 && x <= label_x + label_width
99 && y >= self.rect.y
100 && y <= self.rect.y + clickable_height
101 } else {
102 false
103 }
104 }
105}
106
107impl Default for ToggleSwitch {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl Element for ToggleSwitch {
118 fn rect(&self) -> Rect {
119 self.rect
120 }
121
122 fn set_rect(&mut self, rect: Rect) {
123 self.rect = rect;
124 }
125
126 fn render(&self, canvas: &mut Canvas, z: i32) {
127 let track_height = self.rect.h;
128 let corner_radius = track_height * 0.5; let track_color = if self.on {
131 self.on_color
132 } else {
133 self.off_color
134 };
135
136 let has_error = self.validation_error.is_some();
137 let border_color = if has_error {
138 Color::rgba(220, 38, 38, 255)
139 } else {
140 self.border_color
141 };
142 let border_width = if has_error {
143 self.border_width.max(2.0)
144 } else {
145 self.border_width
146 };
147
148 let track_rrect = RoundedRect {
149 rect: self.rect,
150 radii: RoundedRadii {
151 tl: corner_radius,
152 tr: corner_radius,
153 br: corner_radius,
154 bl: corner_radius,
155 },
156 };
157
158 if border_width > 0.0 {
159 jag_surface::shapes::draw_snapped_rounded_rectangle(
160 canvas,
161 track_rrect,
162 Some(Brush::Solid(track_color)),
163 Some(border_width),
164 Some(Brush::Solid(border_color)),
165 z,
166 );
167 } else {
168 canvas.rounded_rect(track_rrect, Brush::Solid(track_color), z);
169 }
170
171 if self.focused {
173 let focus_rr = RoundedRect {
174 rect: self.rect,
175 radii: RoundedRadii {
176 tl: corner_radius,
177 tr: corner_radius,
178 br: corner_radius,
179 bl: corner_radius,
180 },
181 };
182 jag_surface::shapes::draw_snapped_rounded_rectangle(
183 canvas,
184 focus_rr,
185 None,
186 Some(2.0),
187 Some(Brush::Solid(Color::rgba(63, 130, 246, 255))),
188 z + 2,
189 );
190 }
191
192 let thumb_diameter = track_height - Self::THUMB_PADDING * 2.0;
194 let thumb_radius = thumb_diameter * 0.5;
195
196 let thumb_x = if self.on {
197 self.rect.x + self.rect.w - Self::THUMB_PADDING - thumb_diameter
198 } else {
199 self.rect.x + Self::THUMB_PADDING
200 };
201 let thumb_y = self.rect.y + Self::THUMB_PADDING;
202
203 let thumb_rect = Rect {
204 x: thumb_x,
205 y: thumb_y,
206 w: thumb_diameter,
207 h: thumb_diameter,
208 };
209 let thumb_rrect = RoundedRect {
210 rect: thumb_rect,
211 radii: RoundedRadii {
212 tl: thumb_radius,
213 tr: thumb_radius,
214 br: thumb_radius,
215 bl: thumb_radius,
216 },
217 };
218
219 let shadow_rect = Rect {
221 x: thumb_rect.x,
222 y: thumb_rect.y + 1.0,
223 w: thumb_rect.w,
224 h: thumb_rect.h,
225 };
226 let shadow_rrect = RoundedRect {
227 rect: shadow_rect,
228 radii: thumb_rrect.radii,
229 };
230 canvas.rounded_rect(shadow_rrect, Brush::Solid(Color::rgba(0, 0, 0, 40)), z + 2);
231
232 canvas.rounded_rect(thumb_rrect, Brush::Solid(self.thumb_color), z + 3);
234
235 if let Some(text) = &self.label {
237 let tx = self.rect.x + self.rect.w + 8.0;
238 let ty = self.rect.y + self.rect.h * 0.5 + self.label_size * 0.32;
239 canvas.draw_text_run_weighted(
240 [tx, ty],
241 text.clone(),
242 self.label_size,
243 400.0,
244 self.label_color,
245 z + 3,
246 );
247 }
248
249 if let Some(ref error_msg) = self.validation_error {
251 let error_size = (self.label_size * 0.85).max(12.0);
252 let baseline_offset = error_size * 0.8;
253 let top_gap = 3.0;
254 let control_height = self.rect.h.max(self.label_size * 1.2);
255 let error_y = self.rect.y + control_height + top_gap + baseline_offset;
256 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
257
258 canvas.draw_text_run_weighted(
259 [self.rect.x, error_y],
260 error_msg.clone(),
261 error_size,
262 400.0,
263 error_color,
264 z + 4,
265 );
266 }
267 }
268
269 fn focus_id(&self) -> Option<FocusId> {
270 Some(self.focus_id)
271 }
272}
273
274impl EventHandler for ToggleSwitch {
279 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
280 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
281 return EventResult::Ignored;
282 }
283 if self.hit_test_track(event.x, event.y) || self.hit_test_label(event.x, event.y) {
284 self.toggle();
285 EventResult::Handled
286 } else {
287 EventResult::Ignored
288 }
289 }
290
291 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
292 if event.state != ElementState::Pressed || !self.focused {
293 return EventResult::Ignored;
294 }
295 match event.key {
296 KeyCode::Space | KeyCode::Enter => {
297 self.toggle();
298 EventResult::Handled
299 }
300 _ => EventResult::Ignored,
301 }
302 }
303
304 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
305 EventResult::Ignored
306 }
307
308 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
309 EventResult::Ignored
310 }
311
312 fn is_focused(&self) -> bool {
313 self.focused
314 }
315
316 fn set_focused(&mut self, focused: bool) {
317 self.focused = focused;
318 }
319
320 fn contains_point(&self, x: f32, y: f32) -> bool {
321 self.hit_test_track(x, y) || self.hit_test_label(x, y)
322 }
323}
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn toggle_new_defaults() {
335 let ts = ToggleSwitch::new();
336 assert!(!ts.on);
337 assert!(!ts.focused);
338 assert!(ts.label.is_none());
339 }
340
341 #[test]
342 fn toggle_toggle() {
343 let mut ts = ToggleSwitch::new();
344 assert!(!ts.on);
345 ts.toggle();
346 assert!(ts.on);
347 ts.toggle();
348 assert!(!ts.on);
349 }
350
351 #[test]
352 fn toggle_hit_test_track() {
353 let mut ts = ToggleSwitch::new();
354 ts.rect = Rect {
355 x: 10.0,
356 y: 10.0,
357 w: 44.0,
358 h: 24.0,
359 };
360 assert!(ts.hit_test_track(30.0, 20.0));
361 assert!(!ts.hit_test_track(0.0, 0.0));
362 }
363
364 #[test]
365 fn toggle_keyboard() {
366 let mut ts = ToggleSwitch::new();
367 ts.focused = true;
368 let evt = KeyboardEvent {
369 key: KeyCode::Space,
370 state: ElementState::Pressed,
371 modifiers: Default::default(),
372 text: None,
373 };
374 assert!(!ts.on);
375 assert_eq!(ts.handle_keyboard(&evt), EventResult::Handled);
376 assert!(ts.on);
377 }
378
379 #[test]
380 fn toggle_focus() {
381 let mut ts = ToggleSwitch::new();
382 assert!(!ts.is_focused());
383 ts.set_focused(true);
384 assert!(ts.is_focused());
385 }
386}