1use jag_draw::{Brush, Color, ColorLinPremul, Rect};
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 Radio {
16 pub center: [f32; 2],
18 pub radius: f32,
20 pub selected: bool,
22 pub label: Option<String>,
24 pub label_size: f32,
26 pub label_color: ColorLinPremul,
28 pub bg: ColorLinPremul,
30 pub border_color: ColorLinPremul,
32 pub border_width: f32,
34 pub dot_color: ColorLinPremul,
36 pub focused: bool,
38 pub validation_error: Option<String>,
40 pub focus_id: FocusId,
42}
43
44impl Radio {
45 pub fn new() -> Self {
47 Self {
48 center: [9.0, 9.0],
49 radius: 9.0,
50 selected: false,
51 label: None,
52 label_size: 14.0,
53 label_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
54 bg: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
55 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
56 border_width: 1.0,
57 dot_color: ColorLinPremul::from_srgba_u8([59, 130, 246, 255]),
58 focused: false,
59 validation_error: None,
60 focus_id: FocusId(0),
61 }
62 }
63
64 pub fn select(&mut self) {
66 self.selected = true;
67 }
68
69 pub fn deselect(&mut self) {
71 self.selected = false;
72 }
73
74 pub fn hit_test_circle(&self, x: f32, y: f32) -> bool {
76 let dx = x - self.center[0];
77 let dy = y - self.center[1];
78 dx * dx + dy * dy <= self.radius * self.radius
79 }
80
81 pub fn hit_test_label(&self, x: f32, y: f32) -> bool {
83 if let Some(label) = &self.label {
84 let label_x = self.center[0] + self.radius + 8.0;
85 let char_width = self.label_size * 0.5;
86 let label_width = label.len() as f32 * char_width;
87 let clickable_height = (self.radius * 2.0).max(self.label_size * 1.2);
88
89 x >= label_x
90 && x <= label_x + label_width
91 && y >= self.center[1] - clickable_height / 2.0
92 && y <= self.center[1] + clickable_height / 2.0
93 } else {
94 false
95 }
96 }
97}
98
99impl Default for Radio {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl Element for Radio {
110 fn rect(&self) -> Rect {
111 let d = self.radius * 2.0;
112 Rect {
113 x: self.center[0] - self.radius,
114 y: self.center[1] - self.radius,
115 w: d,
116 h: d,
117 }
118 }
119
120 fn set_rect(&mut self, rect: Rect) {
121 self.center = [rect.x + rect.w * 0.5, rect.y + rect.h * 0.5];
122 self.radius = rect.w.min(rect.h) * 0.5;
123 }
124
125 fn render(&self, canvas: &mut Canvas, z: i32) {
126 canvas.ellipse(
128 self.center,
129 [self.radius, self.radius],
130 Brush::Solid(self.bg),
131 z,
132 );
133
134 if self.border_width > 0.0 {
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 jag_surface::shapes::draw_ellipse(
148 canvas,
149 self.center,
150 [self.radius, self.radius],
151 None,
152 Some(border_width),
153 Some(Brush::Solid(border_color)),
154 z + 1,
155 );
156 }
157
158 if self.selected {
160 let inner = self.radius * 0.6;
161 canvas.ellipse(
162 self.center,
163 [inner, inner],
164 Brush::Solid(self.dot_color),
165 z + 2,
166 );
167 }
168
169 if self.focused {
171 let focus_radius = self.radius + 2.0;
172 jag_surface::shapes::draw_ellipse(
173 canvas,
174 self.center,
175 [focus_radius, focus_radius],
176 None,
177 Some(2.0),
178 Some(Brush::Solid(Color::rgba(63, 130, 246, 255))),
179 z + 3,
180 );
181 }
182
183 if let Some(text) = &self.label {
185 let pos = [
186 self.center[0] + self.radius + 8.0,
187 self.center[1] + self.label_size * 0.35,
188 ];
189 canvas.draw_text_run_weighted(
190 pos,
191 text.clone(),
192 self.label_size,
193 400.0,
194 self.label_color,
195 z + 3,
196 );
197 }
198
199 if let Some(ref error_msg) = self.validation_error {
201 let error_size = (self.label_size * 0.85).max(12.0);
202 let baseline_offset = error_size * 0.8;
203 let top_gap = 3.0;
204 let control_height = self.radius * 2.0;
205 let error_y = self.center[1] + control_height * 0.5 + top_gap + baseline_offset;
206 let error_x = self.center[0] - self.radius;
207 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
208
209 canvas.draw_text_run_weighted(
210 [error_x, error_y],
211 error_msg.clone(),
212 error_size,
213 400.0,
214 error_color,
215 z + 4,
216 );
217 }
218 }
219
220 fn focus_id(&self) -> Option<FocusId> {
221 Some(self.focus_id)
222 }
223}
224
225impl EventHandler for Radio {
230 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
231 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
232 return EventResult::Ignored;
233 }
234 if self.hit_test_circle(event.x, event.y) || self.hit_test_label(event.x, event.y) {
235 self.select();
236 EventResult::Handled
237 } else {
238 EventResult::Ignored
239 }
240 }
241
242 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
243 if event.state != ElementState::Pressed || !self.focused {
244 return EventResult::Ignored;
245 }
246 match event.key {
247 KeyCode::Space | KeyCode::Enter => {
248 self.select();
249 EventResult::Handled
250 }
251 _ => EventResult::Ignored,
252 }
253 }
254
255 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
256 EventResult::Ignored
257 }
258
259 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
260 EventResult::Ignored
261 }
262
263 fn is_focused(&self) -> bool {
264 self.focused
265 }
266
267 fn set_focused(&mut self, focused: bool) {
268 self.focused = focused;
269 }
270
271 fn contains_point(&self, x: f32, y: f32) -> bool {
272 self.hit_test_circle(x, y) || self.hit_test_label(x, y)
273 }
274}
275
276#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn radio_new_defaults() {
286 let r = Radio::new();
287 assert!(!r.selected);
288 assert!(!r.focused);
289 assert!(r.label.is_none());
290 }
291
292 #[test]
293 fn radio_select_deselect() {
294 let mut r = Radio::new();
295 assert!(!r.selected);
296 r.select();
297 assert!(r.selected);
298 r.deselect();
299 assert!(!r.selected);
300 }
301
302 #[test]
303 fn radio_hit_test_circle() {
304 let mut r = Radio::new();
305 r.center = [50.0, 50.0];
306 r.radius = 10.0;
307 assert!(r.hit_test_circle(50.0, 50.0)); assert!(r.hit_test_circle(55.0, 50.0)); assert!(!r.hit_test_circle(70.0, 50.0)); }
311
312 #[test]
313 fn radio_hit_test_label() {
314 let mut r = Radio::new();
315 r.center = [50.0, 50.0];
316 r.radius = 10.0;
317 r.label = Some("Option A".to_string());
318 assert!(r.hit_test_label(70.0, 50.0));
320 assert!(!r.hit_test_label(40.0, 50.0));
321 }
322
323 #[test]
324 fn radio_focus() {
325 let mut r = Radio::new();
326 assert!(!r.is_focused());
327 r.set_focused(true);
328 assert!(r.is_focused());
329 }
330
331 #[test]
332 fn radio_keyboard_select() {
333 let mut r = Radio::new();
334 r.focused = true;
335 let evt = KeyboardEvent {
336 key: KeyCode::Space,
337 state: ElementState::Pressed,
338 modifiers: Default::default(),
339 text: None,
340 };
341 assert!(!r.selected);
342 assert_eq!(r.handle_keyboard(&evt), EventResult::Handled);
343 assert!(r.selected);
344 }
345}