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::{paint_subtree, Widget};
16use crate::widgets::label::Label;
17
18const DOT_R: f64 = 7.0; const GAP: f64 = 8.0;
20const ROW_H: f64 = 22.0;
21
22pub struct RadioGroup {
27 bounds: Rect,
28 children: Vec<Box<dyn Widget>>, base: WidgetBase,
30 options: Vec<String>,
31 selected: usize,
32 hovered: Option<usize>,
33 focused: bool,
34 font: Arc<Font>,
35 font_size: f64,
36 on_change: Option<Box<dyn FnMut(usize)>>,
37 label_widgets: Vec<Label>,
39 selected_cell: Option<Rc<Cell<usize>>>,
42}
43
44impl RadioGroup {
45 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
46 let font_size = 14.0;
47 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
48 let label_widgets = opts
49 .iter()
50 .map(|text| Label::new(text.as_str(), Arc::clone(&font)).with_font_size(font_size))
51 .collect();
52 Self {
53 bounds: Rect::default(),
54 children: Vec::new(),
55 base: WidgetBase::new(),
56 options: opts,
57 selected,
58 hovered: None,
59 focused: false,
60 font,
61 font_size,
62 on_change: None,
63 label_widgets,
64 selected_cell: None,
65 }
66 }
67
68 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
72 let n = self.options.len();
73 let v = cell.get();
74 if n > 0 {
75 self.selected = v.min(n - 1);
76 }
77 self.selected_cell = Some(cell);
78 self
79 }
80
81 pub fn with_font_size(mut self, size: f64) -> Self {
82 self.font_size = size;
83 self.label_widgets = self
85 .options
86 .iter()
87 .map(|text| Label::new(text.as_str(), Arc::clone(&self.font)).with_font_size(size))
88 .collect();
89 self
90 }
91
92 pub fn with_margin(mut self, m: Insets) -> Self {
93 self.base.margin = m;
94 self
95 }
96 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
97 self.base.h_anchor = h;
98 self
99 }
100 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
101 self.base.v_anchor = v;
102 self
103 }
104 pub fn with_min_size(mut self, s: Size) -> Self {
105 self.base.min_size = s;
106 self
107 }
108 pub fn with_max_size(mut self, s: Size) -> Self {
109 self.base.max_size = s;
110 self
111 }
112
113 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
114 self.on_change = Some(Box::new(cb));
115 self
116 }
117
118 pub fn selected(&self) -> usize {
119 self.selected
120 }
121
122 pub fn set_selected(&mut self, idx: usize) {
123 if idx < self.options.len() {
124 self.selected = idx;
125 if let Some(cell) = &self.selected_cell {
126 cell.set(idx);
127 }
128 }
129 }
130
131 fn fire(&mut self) {
132 let idx = self.selected;
133 if let Some(cell) = &self.selected_cell {
134 cell.set(idx);
135 }
136 if let Some(cb) = self.on_change.as_mut() {
137 cb(idx);
138 }
139 }
140
141 fn row_center_y(&self, i: usize, total_h: f64) -> f64 {
143 let n = self.options.len();
144 if n == 0 {
145 return total_h * 0.5;
146 }
147 let row_top_y = total_h - (i as f64) * ROW_H;
150 row_top_y - ROW_H * 0.5
151 }
152
153 fn row_for_y(&self, pos_y: f64) -> Option<usize> {
154 let h = self.bounds.height;
155 for i in 0..self.options.len() {
156 let cy = self.row_center_y(i, h);
157 if pos_y >= cy - ROW_H * 0.5 && pos_y < cy + ROW_H * 0.5 {
158 return Some(i);
159 }
160 }
161 None
162 }
163}
164
165impl Widget for RadioGroup {
166 fn type_name(&self) -> &'static str {
167 "RadioGroup"
168 }
169 fn bounds(&self) -> Rect {
170 self.bounds
171 }
172 fn set_bounds(&mut self, b: Rect) {
173 self.bounds = b;
174 }
175 fn children(&self) -> &[Box<dyn Widget>] {
176 &self.children
177 }
178 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
179 &mut self.children
180 }
181
182 fn is_focusable(&self) -> bool {
183 true
184 }
185
186 fn margin(&self) -> Insets {
187 self.base.margin
188 }
189 fn h_anchor(&self) -> HAnchor {
190 self.base.h_anchor
191 }
192 fn v_anchor(&self) -> VAnchor {
193 self.base.v_anchor
194 }
195 fn min_size(&self) -> Size {
196 self.base.min_size
197 }
198 fn max_size(&self) -> Size {
199 self.base.max_size
200 }
201
202 fn layout(&mut self, available: Size) -> Size {
203 if let Some(cell) = &self.selected_cell {
206 let n = self.options.len();
207 if n > 0 {
208 let v = cell.get().min(n - 1);
209 self.selected = v;
210 }
211 }
212 let h = self.options.len() as f64 * ROW_H;
213 self.bounds = Rect::new(0.0, 0.0, available.width, h);
214 let label_avail_w = (available.width - DOT_R * 2.0 - GAP).max(0.0);
215 for lw in self.label_widgets.iter_mut() {
216 let s = lw.layout(Size::new(label_avail_w, ROW_H));
217 lw.set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
218 }
219 Size::new(available.width, h)
220 }
221
222 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
223 let v = ctx.visuals();
224 let h = self.bounds.height;
225
226 if self.focused {
228 ctx.set_stroke_color(v.accent_focus);
229 ctx.set_line_width(1.5);
230 ctx.begin_path();
231 ctx.rounded_rect(-2.0, -2.0, self.bounds.width + 4.0, h + 4.0, 4.0);
232 ctx.stroke();
233 }
234
235 for i in 0..self.options.len() {
236 let cy = self.row_center_y(i, h);
237 let checked = i == self.selected;
238 let hovered = self.hovered == Some(i);
239
240 let border = if checked {
242 v.accent
243 } else if hovered {
244 v.widget_bg_hovered
245 } else {
246 v.widget_stroke
247 };
248 let bg = if checked { v.accent } else { v.widget_bg };
249
250 ctx.set_fill_color(bg);
251 ctx.begin_path();
252 ctx.circle(DOT_R, cy, DOT_R);
253 ctx.fill();
254
255 ctx.set_stroke_color(border);
256 ctx.set_line_width(1.5);
257 ctx.begin_path();
258 ctx.circle(DOT_R, cy, DOT_R);
259 ctx.stroke();
260
261 if checked {
263 ctx.set_fill_color(v.widget_bg);
264 ctx.begin_path();
265 ctx.circle(DOT_R, cy, DOT_R * 0.45);
266 ctx.fill();
267 }
268
269 self.label_widgets[i].set_color(v.text_color);
271
272 let lw = self.label_widgets[i].bounds().width;
273 let lh = self.label_widgets[i].bounds().height;
274 let lx = DOT_R * 2.0 + GAP;
275 let ly = cy - lh * 0.5;
276 self.label_widgets[i].set_bounds(Rect::new(lx, ly, lw, lh));
277
278 ctx.save();
279 ctx.translate(lx, ly);
280 paint_subtree(&mut self.label_widgets[i], ctx);
281 ctx.restore();
282 }
283 }
284
285 fn on_event(&mut self, event: &Event) -> EventResult {
286 match event {
287 Event::MouseMove { pos } => {
288 let was = self.hovered;
289 self.hovered = self.row_for_y(pos.y);
290 if was != self.hovered {
291 crate::animation::request_draw();
292 return EventResult::Consumed;
293 }
294 EventResult::Ignored
295 }
296 Event::MouseDown {
297 button: MouseButton::Left,
298 pos,
299 ..
300 } => {
301 if let Some(i) = self.row_for_y(pos.y) {
302 let was = self.selected;
303 self.selected = i;
304 self.fire();
305 if was != i {
306 crate::animation::request_draw();
307 }
308 return EventResult::Consumed;
309 }
310 EventResult::Ignored
311 }
312 Event::KeyDown { key, .. } => {
313 let n = self.options.len();
314 let changed = match key {
315 Key::ArrowUp | Key::ArrowLeft => {
316 if self.selected > 0 {
317 self.selected -= 1;
318 true
319 } else {
320 false
321 }
322 }
323 Key::ArrowDown | Key::ArrowRight => {
324 if self.selected + 1 < n {
325 self.selected += 1;
326 true
327 } else {
328 false
329 }
330 }
331 _ => false,
332 };
333 if changed {
334 self.fire();
335 crate::animation::request_draw();
336 EventResult::Consumed
337 } else {
338 EventResult::Ignored
339 }
340 }
341 Event::FocusGained => {
342 let was = self.focused;
343 self.focused = true;
344 if !was {
345 crate::animation::request_draw();
346 EventResult::Consumed
347 } else {
348 EventResult::Ignored
349 }
350 }
351 Event::FocusLost => {
352 let was = self.focused;
353 self.focused = false;
354 if was {
355 crate::animation::request_draw();
356 EventResult::Consumed
357 } else {
358 EventResult::Ignored
359 }
360 }
361 _ => EventResult::Ignored,
362 }
363 }
364}