Skip to main content

fui/controls/
radio_button.rs

1use super::internal::pressable_labeled_control::{
2    PressableLabeledControlState, WeakPressableLabeledControl,
3};
4use super::internal::radio_indicator_presenter::{
5    create_default_radio_indicator_presenter, RadioIndicatorPresenter, RadioIndicatorTemplate,
6    RadioIndicatorVisualState,
7};
8use super::*;
9use crate::bindings::ui;
10use crate::controls::radio_group::RadioGroupEventTarget;
11use crate::ffi::KeyEventType;
12use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14
15fn create_indicator_presenter(
16    template: Option<Rc<dyn RadioIndicatorTemplate>>,
17    sizing: Option<LabeledControlSizing>,
18) -> Rc<dyn RadioIndicatorPresenter> {
19    if let Some(template) = template {
20        return template.create(sizing);
21    }
22    if let Some(template_set) = get_control_templates() {
23        if let Some(template) = template_set.radio_indicator {
24            return template.create(sizing);
25        }
26    }
27    create_default_radio_indicator_presenter(sizing)
28}
29
30#[derive(Clone)]
31pub struct RadioButton {
32    base: PressableLabeledControl,
33    root: FlexBox,
34    indicator_presenter: Rc<RefCell<Rc<dyn RadioIndicatorPresenter>>>,
35    template_override: Rc<RefCell<Option<Rc<dyn RadioIndicatorTemplate>>>>,
36    sizing_value: Rc<Cell<Option<LabeledControlSizing>>>,
37    colors_value: Rc<Cell<Option<LabeledControlColors>>>,
38    value_text: String,
39    checked: Rc<Cell<bool>>,
40    changed: Rc<RefCell<Option<RadioButtonChangedCallback>>>,
41    owner_group: Rc<RefCell<Option<WeakRadioGroupEventTarget>>>,
42    weak_root: Rc<WeakNodeRef>,
43}
44
45impl RadioButton {
46    pub fn new(value: impl Into<String>) -> Self {
47        let value = value.into();
48        Self::with_label(value.clone(), value)
49    }
50
51    pub fn with_label(value: impl Into<String>, label: impl Into<String>) -> Self {
52        let value = value.into();
53        let label = label.into();
54        let indicator_presenter = create_indicator_presenter(None, None);
55        let base =
56            PressableLabeledControl::new(SemanticRole::Radio, label, indicator_presenter.root());
57        let root = base.root();
58        let weak_root = Rc::new(root.node_ref().downgrade());
59        let control = Self {
60            base,
61            root,
62            indicator_presenter: Rc::new(RefCell::new(indicator_presenter)),
63            template_override: Rc::new(RefCell::new(None)),
64            sizing_value: Rc::new(Cell::new(None)),
65            colors_value: Rc::new(Cell::new(None)),
66            value_text: value,
67            checked: Rc::new(Cell::new(false)),
68            changed: Rc::new(RefCell::new(None)),
69            owner_group: Rc::new(RefCell::new(None)),
70            weak_root,
71        };
72        control.root.semantic_checked(SemanticCheckedState::False);
73        let activation = RadioEventTarget::from_radio(&control);
74        control.base.set_activation(move |state| {
75            activation.activate(state);
76        });
77        let state_sync = RadioEventTarget::from_radio(&control);
78        control.base.set_state_changed(move |state| {
79            state_sync.sync_visual(state, false, false);
80        });
81        let key_target = RadioEventTarget::from_radio(&control);
82        control
83            .base
84            .set_key_handler(move |event| key_target.handle_key_event(event));
85        control.sync_visual(control.base.snapshot_state(), false, false);
86        control
87    }
88
89    pub fn value(&self) -> &str {
90        &self.value_text
91    }
92
93    pub fn checked(&self, checked: bool) -> &Self {
94        self.check(checked);
95        self
96    }
97
98    pub fn check(&self, checked: bool) -> &Self {
99        self.update_checked(checked, true, false);
100        self
101    }
102
103    pub fn is_checked(&self) -> bool {
104        self.checked.get()
105    }
106
107    pub fn sizing(&self, sizing: LabeledControlSizing) -> &Self {
108        self.set_sizing(Some(sizing))
109    }
110
111    pub fn clear_sizing(&self) -> &Self {
112        self.set_sizing(None)
113    }
114
115    fn set_sizing(&self, sizing: Option<LabeledControlSizing>) -> &Self {
116        self.sizing_value.set(sizing);
117        self.base.set_label_font_size_override(
118            sizing
119                .filter(|sizing| sizing.has_label_font_size())
120                .map(|sizing| sizing.label_font_size_px())
121                .unwrap_or(0.0),
122        );
123        self.replace_indicator_presenter(create_indicator_presenter(
124            self.template_override.borrow().clone(),
125            self.sizing_value.get(),
126        ));
127        self.sync_visual(self.base.snapshot_state(), false, false);
128        self
129    }
130
131    pub fn template(&self, template: Rc<dyn RadioIndicatorTemplate>) -> &Self {
132        self.set_template(Some(template))
133    }
134
135    pub fn clear_template(&self) -> &Self {
136        self.set_template(None)
137    }
138
139    fn set_template(&self, template: Option<Rc<dyn RadioIndicatorTemplate>>) -> &Self {
140        self.template_override.replace(template.clone());
141        self.replace_indicator_presenter(create_indicator_presenter(
142            template,
143            self.sizing_value.get(),
144        ));
145        self.sync_visual(self.base.snapshot_state(), false, false);
146        self
147    }
148
149    pub fn colors(&self, colors: LabeledControlColors) -> &Self {
150        self.set_colors(Some(colors))
151    }
152
153    pub fn clear_colors(&self) -> &Self {
154        self.set_colors(None)
155    }
156
157    fn set_colors(&self, colors: Option<LabeledControlColors>) -> &Self {
158        self.colors_value.set(colors);
159        self.base.colors(colors);
160        self.sync_visual(self.base.snapshot_state(), false, false);
161        self
162    }
163
164    pub fn enabled(&self, enabled: bool) -> &Self {
165        self.base.enabled(enabled);
166        self
167    }
168
169    pub fn on_changed(&self, handler: impl Fn(RadioButtonChangedEventArgs) + 'static) -> &Self {
170        *self.changed.borrow_mut() = Some(Rc::new(handler));
171        self
172    }
173
174    pub fn focus_now(&self) {
175        ui::request_focus(self.root.retained_node_ref().handle().raw());
176    }
177
178    pub(crate) fn bind_group(&self, group: WeakRadioGroupEventTarget) {
179        *self.owner_group.borrow_mut() = Some(group);
180    }
181
182    pub(crate) fn is_enabled(&self) -> bool {
183        self.root.retained_node_ref().is_enabled_for_routing()
184    }
185
186    pub(crate) fn update_checked(&self, checked: bool, emit: bool, announce: bool) {
187        apply_radio_checked(
188            &self.indicator_presenter,
189            &self.weak_root,
190            &self.checked,
191            checked,
192            emit,
193            announce,
194            &self.changed,
195            self.colors_value.get(),
196            self.base.snapshot_state(),
197        );
198    }
199
200    fn replace_indicator_presenter(&self, next_presenter: Rc<dyn RadioIndicatorPresenter>) {
201        let previous = self.indicator_presenter.borrow().clone();
202        if Rc::ptr_eq(&previous, &next_presenter) {
203            return;
204        }
205        self.base.replace_indicator_root(next_presenter.root());
206        self.indicator_presenter.replace(next_presenter);
207    }
208
209    fn sync_visual(&self, base_state: PressableLabeledControlState, emit: bool, announce: bool) {
210        apply_radio_checked(
211            &self.indicator_presenter,
212            &self.weak_root,
213            &self.checked,
214            self.checked.get(),
215            emit,
216            announce,
217            &self.changed,
218            self.colors_value.get(),
219            base_state,
220        );
221    }
222}
223
224impl Clickable for RadioButton {
225    fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self {
226        self.base.on_click(handler);
227        self
228    }
229}
230
231impl LabeledControlTextStyle for RadioButton {
232    fn set_label_font_family(&self, family: crate::FontFamily) {
233        self.base.font_family(family);
234    }
235
236    fn set_label_font_size(&self, size: f32) {
237        self.base.font_size(size);
238    }
239
240    fn set_label_text_color(&self, color: u32) {
241        self.base.text_color(color);
242    }
243}
244
245impl Node for RadioButton {
246    fn retained_node_ref(&self) -> NodeRef {
247        self.root.retained_node_ref()
248    }
249
250    fn build_self(&self) {
251        self.sync_visual(self.base.snapshot_state(), false, false);
252        self.root.build_self();
253    }
254}
255
256impl HasFlexBoxRoot for RadioButton {
257    fn flex_box_root(&self) -> &FlexBox {
258        &self.root
259    }
260}
261
262impl ThemeBindable for RadioButton {
263    fn theme_binding_node(&self) -> NodeRef {
264        self.root.retained_node_ref()
265    }
266
267    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
268        let target = RadioEventTarget::from_radio(self);
269        Box::new(move || target.upgrade())
270    }
271}
272
273#[derive(Clone)]
274struct RadioEventTarget {
275    base: WeakPressableLabeledControl,
276    presenter: Rc<RefCell<Rc<dyn RadioIndicatorPresenter>>>,
277    template_override: Rc<RefCell<Option<Rc<dyn RadioIndicatorTemplate>>>>,
278    sizing_value: Rc<Cell<Option<LabeledControlSizing>>>,
279    value_text: String,
280    checked: Rc<Cell<bool>>,
281    colors_value: Rc<Cell<Option<LabeledControlColors>>>,
282    changed: Rc<RefCell<Option<RadioButtonChangedCallback>>>,
283    owner_group: Rc<RefCell<Option<WeakRadioGroupEventTarget>>>,
284    weak_root: Rc<WeakNodeRef>,
285}
286
287impl RadioEventTarget {
288    fn from_radio(radio: &RadioButton) -> Self {
289        Self {
290            base: radio.base.downgrade(),
291            presenter: radio.indicator_presenter.clone(),
292            template_override: radio.template_override.clone(),
293            sizing_value: radio.sizing_value.clone(),
294            value_text: radio.value_text.clone(),
295            checked: radio.checked.clone(),
296            colors_value: radio.colors_value.clone(),
297            changed: radio.changed.clone(),
298            owner_group: radio.owner_group.clone(),
299            weak_root: radio.weak_root.clone(),
300        }
301    }
302
303    fn upgrade(&self) -> Option<RadioButton> {
304        let base = self.base.upgrade()?;
305        Some(RadioButton {
306            root: base.root(),
307            base,
308            indicator_presenter: self.presenter.clone(),
309            template_override: self.template_override.clone(),
310            sizing_value: self.sizing_value.clone(),
311            colors_value: self.colors_value.clone(),
312            value_text: self.value_text.clone(),
313            checked: self.checked.clone(),
314            changed: self.changed.clone(),
315            owner_group: self.owner_group.clone(),
316            weak_root: self.weak_root.clone(),
317        })
318    }
319
320    fn activate(&self, base_state: PressableLabeledControlState) {
321        if let Some(group) = self.owner_group() {
322            if let Some(handle) = upgraded_handle(&self.weak_root) {
323                group.select_radio_handle(handle.raw(), true);
324                return;
325            }
326        }
327        apply_radio_checked(
328            &self.presenter,
329            &self.weak_root,
330            &self.checked,
331            true,
332            true,
333            true,
334            &self.changed,
335            self.colors_value.get(),
336            base_state,
337        );
338    }
339
340    fn handle_key_event(&self, event: &mut KeyEventArgs) -> bool {
341        if event.event_type != KeyEventType::Down || event.modifiers != 0 {
342            return false;
343        }
344        let Some(group) = self.owner_group() else {
345            return false;
346        };
347        let Some(handle) = upgraded_handle(&self.weak_root) else {
348            return false;
349        };
350        match event.key.as_str() {
351            "ArrowLeft" | "ArrowUp" => {
352                group.move_selection_from_handle(handle.raw(), -1);
353                true
354            }
355            "ArrowRight" | "ArrowDown" => {
356                group.move_selection_from_handle(handle.raw(), 1);
357                true
358            }
359            "Home" => {
360                group.select_first_enabled(true);
361                true
362            }
363            "End" => {
364                group.select_last_enabled(true);
365                true
366            }
367            _ => false,
368        }
369    }
370
371    fn sync_visual(&self, base_state: PressableLabeledControlState, emit: bool, announce: bool) {
372        apply_radio_checked(
373            &self.presenter,
374            &self.weak_root,
375            &self.checked,
376            self.checked.get(),
377            emit,
378            announce,
379            &self.changed,
380            self.colors_value.get(),
381            base_state,
382        );
383    }
384
385    fn owner_group(&self) -> Option<Rc<RadioGroupEventTarget>> {
386        self.owner_group
387            .borrow()
388            .as_ref()
389            .and_then(WeakRadioGroupEventTarget::upgrade)
390    }
391}
392
393#[allow(clippy::too_many_arguments)]
394fn apply_radio_checked(
395    presenter: &Rc<RefCell<Rc<dyn RadioIndicatorPresenter>>>,
396    weak_root: &Rc<WeakNodeRef>,
397    checked_cell: &Rc<Cell<bool>>,
398    checked: bool,
399    emit: bool,
400    announce: bool,
401    changed: &Rc<RefCell<Option<RadioButtonChangedCallback>>>,
402    colors: Option<LabeledControlColors>,
403    base_state: PressableLabeledControlState,
404) {
405    let previous = checked_cell.get();
406    checked_cell.set(checked);
407    update_semantic_checked(
408        weak_root,
409        if checked {
410            SemanticCheckedState::True
411        } else {
412            SemanticCheckedState::False
413        },
414        announce && previous != checked,
415    );
416    presenter.borrow().apply(
417        current_theme(),
418        RadioIndicatorVisualState::new(
419            checked,
420            base_state.hovered,
421            base_state.pressed,
422            base_state.focused,
423            base_state.enabled,
424        ),
425        colors,
426    );
427    if emit && previous != checked {
428        if let Some(callback) = changed.borrow().clone() {
429            callback(RadioButtonChangedEventArgs { checked });
430        }
431    }
432}