Skip to main content

fui/controls/
radio_group.rs

1use 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>;