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