agg_gui/widgets/
radio_group.rs1use std::cell::Cell;
7use std::rc::Rc;
8use std::sync::Arc;
9
10use crate::draw_ctx::DrawCtx;
11use crate::event::{Event, EventResult, Key, MouseButton};
12use crate::geometry::{Rect, Size};
13use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
14use crate::text::Font;
15use crate::widget::Widget;
16use crate::widgets::label::Label;
17
18const DOT_R: f64 = 7.0; const GAP: f64 = 8.0;
20const ROW_H: f64 = 22.0;
21const LEFT_INSET: f64 = 2.0;
28
29pub struct RadioGroup {
38 bounds: Rect,
39 children: Vec<Box<dyn Widget>>,
44 base: WidgetBase,
45 options: Vec<String>,
46 selected: usize,
47 hovered: Option<usize>,
48 focused: bool,
49 font: Arc<Font>,
50 font_size: f64,
51 on_change: Option<Box<dyn FnMut(usize)>>,
52 selected_cell: Option<Rc<Cell<usize>>>,
55}
56
57impl RadioGroup {
58 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
59 let font_size = 14.0;
60 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
61 let children: Vec<Box<dyn Widget>> = opts
62 .iter()
63 .map(|text| {
64 Box::new(
65 Label::new(text.as_str(), Arc::clone(&font)).with_font_size(font_size),
66 ) as Box<dyn Widget>
67 })
68 .collect();
69 Self {
70 bounds: Rect::default(),
71 children,
72 base: WidgetBase::new(),
73 options: opts,
74 selected,
75 hovered: None,
76 focused: false,
77 font,
78 font_size,
79 on_change: None,
80 selected_cell: None,
81 }
82 }
83
84 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
88 let n = self.options.len();
89 let v = cell.get();
90 if n > 0 {
91 self.selected = v.min(n - 1);
92 }
93 self.selected_cell = Some(cell);
94 self
95 }
96
97 pub fn with_font_size(mut self, size: f64) -> Self {
98 self.font_size = size;
99 self.children = self
101 .options
102 .iter()
103 .map(|text| {
104 Box::new(
105 Label::new(text.as_str(), Arc::clone(&self.font))
106 .with_font_size(size),
107 ) as Box<dyn Widget>
108 })
109 .collect();
110 self
111 }
112
113 pub fn with_margin(mut self, m: Insets) -> Self {
114 self.base.margin = m;
115 self
116 }
117 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
118 self.base.h_anchor = h;
119 self
120 }
121 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
122 self.base.v_anchor = v;
123 self
124 }
125 pub fn with_min_size(mut self, s: Size) -> Self {
126 self.base.min_size = s;
127 self
128 }
129 pub fn with_max_size(mut self, s: Size) -> Self {
130 self.base.max_size = s;
131 self
132 }
133
134 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
135 self.on_change = Some(Box::new(cb));
136 self
137 }
138
139 pub fn selected(&self) -> usize {
140 self.selected
141 }
142
143 pub fn set_selected(&mut self, idx: usize) {
144 if idx < self.options.len() {
145 self.selected = idx;
146 if let Some(cell) = &self.selected_cell {
147 cell.set(idx);
148 }
149 }
150 }
151
152 fn fire(&mut self) {
153 let idx = self.selected;
154 if let Some(cell) = &self.selected_cell {
155 cell.set(idx);
156 }
157 if let Some(cb) = self.on_change.as_mut() {
158 cb(idx);
159 }
160 }
161
162 fn row_center_y(&self, i: usize, total_h: f64) -> f64 {
164 let n = self.options.len();
165 if n == 0 {
166 return total_h * 0.5;
167 }
168 let row_top_y = total_h - (i as f64) * ROW_H;
171 row_top_y - ROW_H * 0.5
172 }
173
174 fn row_for_y(&self, pos_y: f64) -> Option<usize> {
175 let h = self.bounds.height;
176 for i in 0..self.options.len() {
177 let cy = self.row_center_y(i, h);
178 if pos_y >= cy - ROW_H * 0.5 && pos_y < cy + ROW_H * 0.5 {
179 return Some(i);
180 }
181 }
182 None
183 }
184}
185
186impl Widget for RadioGroup {
187 fn type_name(&self) -> &'static str {
188 "RadioGroup"
189 }
190 fn bounds(&self) -> Rect {
191 self.bounds
192 }
193 fn set_bounds(&mut self, b: Rect) {
194 self.bounds = b;
195 }
196 fn children(&self) -> &[Box<dyn Widget>] {
197 &self.children
198 }
199 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
200 &mut self.children
201 }
202
203 fn is_focusable(&self) -> bool {
204 true
205 }
206
207 fn margin(&self) -> Insets {
208 self.base.margin
209 }
210 fn widget_base(&self) -> Option<&WidgetBase> {
211 Some(&self.base)
212 }
213 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
214 Some(&mut self.base)
215 }
216 fn h_anchor(&self) -> HAnchor {
217 self.base.h_anchor
218 }
219 fn v_anchor(&self) -> VAnchor {
220 self.base.v_anchor
221 }
222 fn min_size(&self) -> Size {
223 self.base.min_size
224 }
225 fn max_size(&self) -> Size {
226 self.base.max_size
227 }
228
229 fn layout(&mut self, available: Size) -> Size {
230 if let Some(cell) = &self.selected_cell {
233 let n = self.options.len();
234 if n > 0 {
235 let v = cell.get().min(n - 1);
236 self.selected = v;
237 }
238 }
239 let h = self.options.len() as f64 * ROW_H;
240 self.bounds = Rect::new(0.0, 0.0, available.width, h);
241 let circle_extent = LEFT_INSET + DOT_R * 2.0;
244 let label_avail_w = (available.width - circle_extent - GAP).max(0.0);
245 let lx = circle_extent + GAP;
246 for (i, child) in self.children.iter_mut().enumerate() {
247 let s = child.layout(Size::new(label_avail_w, ROW_H));
248 let row_top_y = h - (i as f64) * ROW_H;
252 let cy = row_top_y - ROW_H * 0.5;
253 let ly = cy - s.height * 0.5;
254 child.set_bounds(Rect::new(lx, ly, s.width, s.height));
255 }
256 Size::new(available.width, h)
257 }
258
259 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
260 let v = ctx.visuals();
261 let h = self.bounds.height;
262
263 if self.focused {
267 ctx.set_stroke_color(v.accent_focus);
268 ctx.set_line_width(1.5);
269 ctx.begin_path();
270 ctx.rounded_rect(0.75, 0.75, self.bounds.width - 1.5, h - 1.5, 4.0);
271 ctx.stroke();
272 }
273
274 let text_color = v.text_color;
280 for i in 0..self.options.len() {
281 let cy = self.row_center_y(i, h);
282 let checked = i == self.selected;
283 let hovered = self.hovered == Some(i);
284
285 let border = if checked {
286 v.accent
287 } else if hovered {
288 v.widget_bg_hovered
289 } else {
290 v.widget_stroke
291 };
292 let bg = if checked { v.accent } else { v.widget_bg };
293
294 ctx.set_fill_color(bg);
295 ctx.begin_path();
296 ctx.circle(LEFT_INSET + DOT_R, cy, DOT_R);
297 ctx.fill();
298
299 ctx.set_stroke_color(border);
300 ctx.set_line_width(1.5);
301 ctx.begin_path();
302 ctx.circle(LEFT_INSET + DOT_R, cy, DOT_R);
303 ctx.stroke();
304
305 if checked {
308 ctx.set_fill_color(v.widget_bg);
309 ctx.begin_path();
310 ctx.circle(LEFT_INSET + DOT_R, cy, DOT_R * 0.45);
311 ctx.fill();
312 }
313
314 if let Some(child) = self.children.get_mut(i) {
315 child.set_label_color(text_color);
316 }
317 }
318 }
319
320 fn on_event(&mut self, event: &Event) -> EventResult {
321 match event {
322 Event::MouseMove { pos } => {
323 let was = self.hovered;
324 self.hovered = self.row_for_y(pos.y);
325 if was != self.hovered {
326 crate::animation::request_draw();
327 return EventResult::Consumed;
328 }
329 EventResult::Ignored
330 }
331 Event::MouseDown {
332 button: MouseButton::Left,
333 pos,
334 ..
335 } => {
336 if let Some(i) = self.row_for_y(pos.y) {
337 let was = self.selected;
338 self.selected = i;
339 self.fire();
340 if was != i {
341 crate::animation::request_draw();
342 }
343 return EventResult::Consumed;
344 }
345 EventResult::Ignored
346 }
347 Event::KeyDown { key, .. } => {
348 let n = self.options.len();
349 let changed = match key {
350 Key::ArrowUp | Key::ArrowLeft => {
351 if self.selected > 0 {
352 self.selected -= 1;
353 true
354 } else {
355 false
356 }
357 }
358 Key::ArrowDown | Key::ArrowRight => {
359 if self.selected + 1 < n {
360 self.selected += 1;
361 true
362 } else {
363 false
364 }
365 }
366 _ => false,
367 };
368 if changed {
369 self.fire();
370 crate::animation::request_draw();
371 EventResult::Consumed
372 } else {
373 EventResult::Ignored
374 }
375 }
376 Event::FocusGained => {
377 let was = self.focused;
378 self.focused = true;
379 if !was {
380 crate::animation::request_draw();
381 EventResult::Consumed
382 } else {
383 EventResult::Ignored
384 }
385 }
386 Event::FocusLost => {
387 let was = self.focused;
388 self.focused = false;
389 if was {
390 crate::animation::request_draw();
391 EventResult::Consumed
392 } else {
393 EventResult::Ignored
394 }
395 }
396 _ => EventResult::Ignored,
397 }
398 }
399}