fui/controls/
radio_group.rs1use super::*;
2use crate::logger;
3use std::cell::{Cell, RefCell};
4use std::rc::{Rc, Weak};
5
6#[derive(Clone)]
7pub(crate) struct RadioGroupEventTarget {
8 radios: Rc<RefCell<Vec<RadioButton>>>,
9 selected_index: Rc<Cell<i32>>,
10 changed: Rc<RefCell<Option<RadioGroupChangedCallback>>>,
11}
12
13impl RadioGroupEventTarget {
14 pub(crate) fn select_radio_handle(&self, radio_handle: u64, focus: bool) {
15 let index = self.index_of_radio_handle(radio_handle);
16 if index >= 0 {
17 self.select_index_internal(index, focus, true, true);
18 }
19 }
20
21 pub(crate) fn move_selection_from_handle(&self, radio_handle: u64, delta: i32) {
22 let start_index = self.index_of_radio_handle(radio_handle);
23 if start_index < 0 || self.radios.borrow().is_empty() {
24 return;
25 }
26 let next_index = self.find_enabled_index(start_index, delta);
27 if next_index >= 0 {
28 self.select_index_internal(next_index, true, true, true);
29 }
30 }
31
32 pub(crate) fn select_first_enabled(&self, focus: bool) {
33 let next_index = self.find_boundary_index(true);
34 if next_index >= 0 {
35 self.select_index_internal(next_index, focus, true, true);
36 }
37 }
38
39 pub(crate) fn select_last_enabled(&self, focus: bool) {
40 let next_index = self.find_boundary_index(false);
41 if next_index >= 0 {
42 self.select_index_internal(next_index, focus, true, true);
43 }
44 }
45
46 pub(crate) fn select_index(&self, index: i32) {
47 if index == -1 {
48 let previous_index = self.selected_index.get();
49 let changed = previous_index != -1;
50 if let Some(previous) = self.radio_at(previous_index) {
51 previous.update_checked(false, true, false);
52 }
53 self.selected_index.set(-1);
54 if changed {
55 self.emit_changed(String::new());
56 }
57 return;
58 }
59 let radio_count = self.radios.borrow().len() as i32;
60 if radio_count == 0 {
61 if index != -1 {
62 logger::warn(
63 "Layout",
64 &format!(
65 "RadioGroup.select_index() received {index} before any radios were added."
66 ),
67 );
68 }
69 return;
70 }
71 let clamped_index = if index < 0 {
72 0
73 } else if index >= radio_count {
74 radio_count - 1
75 } else {
76 index
77 };
78 if clamped_index != index {
79 logger::warn(
80 "Layout",
81 &format!(
82 "RadioGroup.select_index() received {index}; clamping to {clamped_index}."
83 ),
84 );
85 }
86 self.select_index_internal(clamped_index, false, true, false);
87 }
88
89 fn select_index_internal(&self, index: i32, focus: bool, emit: bool, announce: bool) {
90 let Some(radio) = self.radio_at(index) else {
91 return;
92 };
93 if !radio.is_enabled() {
94 return;
95 }
96 let previous_index = self.selected_index.get();
97 if previous_index == index {
98 if focus {
99 radio.focus_now();
100 }
101 return;
102 }
103 if let Some(previous) = self.radio_at(previous_index) {
104 previous.update_checked(false, emit, false);
105 }
106 self.selected_index.set(index);
107 radio.update_checked(true, emit, announce);
108 if focus {
109 radio.focus_now();
110 }
111 if emit {
112 self.emit_changed(radio.value().to_string());
113 }
114 }
115
116 fn emit_changed(&self, value: String) {
117 if let Some(callback) = self.changed.borrow().clone() {
118 callback(RadioGroupChangedEventArgs { value });
119 }
120 }
121
122 fn index_of_radio_handle(&self, target_handle: u64) -> i32 {
123 self.radios
124 .borrow()
125 .iter()
126 .position(|radio| radio.retained_node_ref().handle().raw() == target_handle)
127 .map(|index| index as i32)
128 .unwrap_or(-1)
129 }
130
131 fn find_enabled_index(&self, start_index: i32, delta: i32) -> i32 {
132 let radios = self.radios.borrow();
133 if radios.is_empty() {
134 return -1;
135 }
136 let len = radios.len() as i32;
137 let mut cursor = start_index;
138 for _ in 0..len {
139 cursor += delta;
140 if cursor < 0 {
141 cursor = len - 1;
142 } else if cursor >= len {
143 cursor = 0;
144 }
145 if radios[cursor as usize].is_enabled() {
146 return cursor;
147 }
148 }
149 -1
150 }
151
152 fn find_boundary_index(&self, first: bool) -> i32 {
153 let radios = self.radios.borrow();
154 if first {
155 radios
156 .iter()
157 .position(|radio| radio.is_enabled())
158 .map(|index| index as i32)
159 .unwrap_or(-1)
160 } else {
161 radios
162 .iter()
163 .rposition(|radio| radio.is_enabled())
164 .map(|index| index as i32)
165 .unwrap_or(-1)
166 }
167 }
168
169 fn radio_at(&self, index: i32) -> Option<RadioButton> {
170 if index < 0 {
171 return None;
172 }
173 self.radios.borrow().get(index as usize).cloned()
174 }
175}
176
177#[derive(Clone)]
178pub struct RadioGroup {
179 root: FlexBox,
180 event_target: Rc<RadioGroupEventTarget>,
181}
182
183impl RadioGroup {
184 pub fn new() -> Self {
185 let root = flex_box();
186 root.semantic_role(SemanticRole::RadioGroup)
187 .flex_direction(FlexDirection::Column);
188 let event_target = Rc::new(RadioGroupEventTarget {
189 radios: Rc::new(RefCell::new(Vec::new())),
190 selected_index: Rc::new(Cell::new(-1)),
191 changed: Rc::new(RefCell::new(None)),
192 });
193 root.retained_node_ref()
194 .retain_attachment(event_target.clone());
195 let control = Self { root, event_target };
196 let selected_index = control.event_target.selected_index.clone();
197 let restore_target = control.event_target.clone();
198 control.persist_state(crate::persisted::persisted_value_adapter(
199 "radio-group-selected-index",
200 crate::persisted::PersistedInt32Codec,
201 1,
202 move || {
203 let index = selected_index.get();
204 if index >= 0 {
205 Some(index)
206 } else {
207 None
208 }
209 },
210 move |index| {
211 restore_target.select_index(index);
212 },
213 ));
214 control
215 }
216
217 pub fn add_radio(&self, radio: RadioButton) -> &Self {
218 radio.bind_group(Rc::downgrade(&self.event_target));
219 if self.selected_index() < 0 && radio.is_checked() {
220 self.event_target
221 .selected_index
222 .set(self.event_target.radios.borrow().len() as i32);
223 }
224 self.root.child(&radio);
225 self.event_target.radios.borrow_mut().push(radio);
226 self
227 }
228
229 pub fn add_option(&self, value: impl Into<String>, label: impl Into<String>) -> RadioButton {
230 let radio = RadioButton::with_label(value, label);
231 self.add_radio(radio.clone());
232 radio
233 }
234
235 pub fn add_options<I>(&self, radios: I) -> &Self
236 where
237 I: IntoIterator<Item = RadioButton>,
238 {
239 for radio in radios {
240 self.add_radio(radio);
241 }
242 self
243 }
244
245 pub fn on_changed(&self, handler: impl Fn(RadioGroupChangedEventArgs) + 'static) -> &Self {
246 *self.event_target.changed.borrow_mut() = Some(Rc::new(handler));
247 self
248 }
249
250 pub fn selected_index(&self) -> i32 {
251 self.event_target.selected_index.get()
252 }
253
254 pub fn selected_value(&self) -> String {
255 self.event_target
256 .radio_at(self.selected_index())
257 .map(|radio| radio.value().to_string())
258 .unwrap_or_default()
259 }
260
261 pub fn select_index(&self, index: i32) -> &Self {
262 self.event_target.select_index(index);
263 self
264 }
265}
266
267impl Default for RadioGroup {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273impl Node for RadioGroup {
274 fn retained_node_ref(&self) -> NodeRef {
275 self.root.retained_node_ref()
276 }
277
278 fn build_self(&self) {
279 self.root.build_self();
280 }
281}
282
283impl crate::node::HasFlexBoxRoot for RadioGroup {
284 fn flex_box_root(&self) -> &FlexBox {
285 &self.root
286 }
287}
288
289impl crate::node::ThemeBindable for RadioGroup {
290 fn theme_binding_node(&self) -> NodeRef {
291 self.root.retained_node_ref()
292 }
293
294 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
295 let root = self.root.downgrade();
296 let event_target = Rc::downgrade(&self.event_target);
297 Box::new(move || {
298 Some(Self {
299 root: root.upgrade()?,
300 event_target: event_target.upgrade()?,
301 })
302 })
303 }
304}
305
306pub(crate) type WeakRadioGroupEventTarget = Weak<RadioGroupEventTarget>;