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}