agg_gui/widgets/
radio_group.rs1use std::cell::Cell;
7use std::rc::Rc;
8use std::sync::Arc;
9
10use crate::event::{Event, EventResult, Key, MouseButton};
11use crate::geometry::{Rect, Size};
12use crate::draw_ctx::DrawCtx;
13use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
14use crate::text::Font;
15use crate::widget::{Widget, paint_subtree};
16use crate::widgets::label::Label;
17
18const DOT_R: f64 = 8.0; const GAP: f64 = 8.0;
20const ROW_H: f64 = 28.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.iter().map(|text| {
49 Label::new(text.as_str(), Arc::clone(&font))
50 .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 { self.selected = v.min(n - 1); }
75 self.selected_cell = Some(cell);
76 self
77 }
78
79 pub fn with_font_size(mut self, size: f64) -> Self {
80 self.font_size = size;
81 self.label_widgets = self.options.iter().map(|text| {
83 Label::new(text.as_str(), Arc::clone(&self.font))
84 .with_font_size(size)
85 }).collect();
86 self
87 }
88
89 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
90 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
91 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
92 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
93 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
94
95 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
96 self.on_change = Some(Box::new(cb));
97 self
98 }
99
100 pub fn selected(&self) -> usize { self.selected }
101
102 pub fn set_selected(&mut self, idx: usize) {
103 if idx < self.options.len() {
104 self.selected = idx;
105 if let Some(cell) = &self.selected_cell { cell.set(idx); }
106 }
107 }
108
109 fn fire(&mut self) {
110 let idx = self.selected;
111 if let Some(cell) = &self.selected_cell { cell.set(idx); }
112 if let Some(cb) = self.on_change.as_mut() { cb(idx); }
113 }
114
115 fn row_center_y(&self, i: usize, total_h: f64) -> f64 {
117 let n = self.options.len();
118 if n == 0 { return total_h * 0.5; }
119 let row_top_y = total_h - (i as f64) * ROW_H;
122 row_top_y - ROW_H * 0.5
123 }
124
125 fn row_for_y(&self, pos_y: f64) -> Option<usize> {
126 let h = self.bounds.height;
127 for i in 0..self.options.len() {
128 let cy = self.row_center_y(i, h);
129 if pos_y >= cy - ROW_H * 0.5 && pos_y < cy + ROW_H * 0.5 {
130 return Some(i);
131 }
132 }
133 None
134 }
135}
136
137impl Widget for RadioGroup {
138 fn type_name(&self) -> &'static str { "RadioGroup" }
139 fn bounds(&self) -> Rect { self.bounds }
140 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
141 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
142 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
143
144 fn is_focusable(&self) -> bool { true }
145
146 fn margin(&self) -> Insets { self.base.margin }
147 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
148 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
149 fn min_size(&self) -> Size { self.base.min_size }
150 fn max_size(&self) -> Size { self.base.max_size }
151
152 fn layout(&mut self, available: Size) -> Size {
153 if let Some(cell) = &self.selected_cell {
156 let n = self.options.len();
157 if n > 0 {
158 let v = cell.get().min(n - 1);
159 self.selected = v;
160 }
161 }
162 let h = self.options.len() as f64 * ROW_H;
163 self.bounds = Rect::new(0.0, 0.0, available.width, h);
164 let label_avail_w = (available.width - DOT_R * 2.0 - GAP).max(0.0);
165 for lw in self.label_widgets.iter_mut() {
166 let s = lw.layout(Size::new(label_avail_w, ROW_H));
167 lw.set_bounds(Rect::new(0.0, 0.0, s.width, s.height));
168 }
169 Size::new(available.width, h)
170 }
171
172 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
173 let v = ctx.visuals();
174 let h = self.bounds.height;
175
176 if self.focused {
178 ctx.set_stroke_color(v.accent_focus);
179 ctx.set_line_width(1.5);
180 ctx.begin_path();
181 ctx.rounded_rect(-2.0, -2.0, self.bounds.width + 4.0, h + 4.0, 4.0);
182 ctx.stroke();
183 }
184
185 for i in 0..self.options.len() {
186 let cy = self.row_center_y(i, h);
187 let checked = i == self.selected;
188 let hovered = self.hovered == Some(i);
189
190 let border = if checked { v.accent }
192 else if hovered { v.widget_bg_hovered }
193 else { v.widget_stroke };
194 let bg = if checked { v.accent } else { v.widget_bg };
195
196 ctx.set_fill_color(bg);
197 ctx.begin_path();
198 ctx.circle(DOT_R, cy, DOT_R);
199 ctx.fill();
200
201 ctx.set_stroke_color(border);
202 ctx.set_line_width(1.5);
203 ctx.begin_path();
204 ctx.circle(DOT_R, cy, DOT_R);
205 ctx.stroke();
206
207 if checked {
209 ctx.set_fill_color(v.widget_bg);
210 ctx.begin_path();
211 ctx.circle(DOT_R, cy, DOT_R * 0.45);
212 ctx.fill();
213 }
214
215 self.label_widgets[i].set_color(v.text_color);
217
218 let lw = self.label_widgets[i].bounds().width;
219 let lh = self.label_widgets[i].bounds().height;
220 let lx = DOT_R * 2.0 + GAP;
221 let ly = cy - lh * 0.5;
222 self.label_widgets[i].set_bounds(Rect::new(lx, ly, lw, lh));
223
224 ctx.save();
225 ctx.translate(lx, ly);
226 paint_subtree(&mut self.label_widgets[i], ctx);
227 ctx.restore();
228 }
229 }
230
231 fn on_event(&mut self, event: &Event) -> EventResult {
232 match event {
233 Event::MouseMove { pos } => {
234 let was = self.hovered;
235 self.hovered = self.row_for_y(pos.y);
236 if was != self.hovered { crate::animation::request_tick(); }
237 EventResult::Ignored
238 }
239 Event::MouseDown { button: MouseButton::Left, pos, .. } => {
240 if let Some(i) = self.row_for_y(pos.y) {
241 let was = self.selected;
242 self.selected = i;
243 self.fire();
244 if was != i { crate::animation::request_tick(); }
245 return EventResult::Consumed;
246 }
247 EventResult::Ignored
248 }
249 Event::KeyDown { key, .. } => {
250 let n = self.options.len();
251 let changed = match key {
252 Key::ArrowUp | Key::ArrowLeft => {
253 if self.selected > 0 { self.selected -= 1; true } else { false }
254 }
255 Key::ArrowDown | Key::ArrowRight => {
256 if self.selected + 1 < n { self.selected += 1; true } else { false }
257 }
258 _ => false,
259 };
260 if changed {
261 self.fire();
262 crate::animation::request_tick();
263 EventResult::Consumed
264 } else {
265 EventResult::Ignored
266 }
267 }
268 Event::FocusGained => {
269 self.focused = true;
270 crate::animation::request_tick();
271 EventResult::Ignored
272 }
273 Event::FocusLost => {
274 self.focused = false;
275 crate::animation::request_tick();
276 EventResult::Ignored
277 }
278 _ => EventResult::Ignored,
279 }
280 }
281}