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