Skip to main content

floem/views/
radio_button.rs

1use crate::{
2    IntoView,
3    style::StyleSelector,
4    style_class,
5    view::View,
6    views::{self, ContainerExt, Decorators, Stack},
7};
8use floem_reactive::{SignalGet, SignalUpdate};
9
10use super::{ValueContainer, create_value_container_signals, value_container};
11
12style_class!(pub RadioButtonClass);
13style_class!(pub RadioButtonDotClass);
14style_class!(pub RadioButtonDotSelectedClass);
15style_class!(pub LabeledRadioButtonClass);
16
17fn radio_button_svg<T>(represented_value: T, actual_value: impl SignalGet<T> + 'static) -> impl View
18where
19    T: Eq + PartialEq + Clone + 'static,
20{
21    ().class(RadioButtonDotClass)
22        .style(move |s| {
23            s.apply_if(actual_value.get() != represented_value, |s| {
24                s.display(taffy::style::Display::None)
25            })
26        })
27        .container()
28        .class(RadioButtonClass)
29}
30
31/// The `RadioButton` struct provides various methods to create and manage radio buttons.
32///
33/// # Related Functions
34/// - [`radio_button`]
35/// - [`labeled_radio_button`]
36pub struct RadioButton;
37
38impl RadioButton {
39    /// Creates a new radio button with a closure that determines its selected state.
40    ///
41    /// This method is useful when you want a radio button whose state is determined by a closure.
42    /// The state can be dynamically updated by the closure, and the radio button will reflect these changes.
43    #[allow(clippy::new_ret_no_self)]
44    pub fn new<T>(represented_value: T, actual_value: impl Fn() -> T + 'static) -> ValueContainer<T>
45    where
46        T: Eq + PartialEq + Clone + 'static,
47    {
48        let (inbound_signal, outbound_signal) = create_value_container_signals(actual_value);
49
50        value_container(
51            radio_button_svg(represented_value.clone(), inbound_signal.read_only())
52                .style(|s| s.keyboard_navigable())
53                .action(move || {
54                    outbound_signal.set(represented_value.clone());
55                }),
56            move || outbound_signal.get(),
57        )
58    }
59
60    /// Creates a new radio button with a signal that provides its selected state.
61    ///
62    /// Use this method when you have a signal that provides the current state of the radio button.
63    /// The radio button will automatically update its state based on the signal.
64    pub fn new_get<T>(
65        represented_value: T,
66        actual_value: impl SignalGet<T> + Copy + 'static,
67    ) -> impl IntoView
68    where
69        T: Eq + PartialEq + Clone + 'static,
70    {
71        let clone = represented_value.clone();
72        radio_button_svg(represented_value, actual_value).style(move |s| {
73            s.keyboard_navigable()
74                .apply_if(clone == actual_value.get(), |s| s.set_selected(true))
75        })
76    }
77
78    /// Creates a new radio button with a signal that provides and updates its selected state.
79    ///
80    /// This method is ideal when you need a radio button that not only reflects a signal's state but also updates it.
81    /// Clicking the radio button will set the signal to the represented value.
82    pub fn new_rw<T>(
83        represented_value: T,
84        actual_value: impl SignalGet<T> + SignalUpdate<T> + Copy + 'static,
85    ) -> impl IntoView
86    where
87        T: Eq + PartialEq + Clone + 'static,
88    {
89        let cloneable_represented_value = represented_value.clone();
90        let cloneable_represented_value_ = represented_value.clone();
91
92        radio_button_svg(cloneable_represented_value.clone(), actual_value)
93            .style(move |s| {
94                s.keyboard_navigable()
95                    .apply_if(cloneable_represented_value_ == actual_value.get(), |s| {
96                        s.set_selected(true)
97                    })
98            })
99            .action(move || {
100                actual_value.set(cloneable_represented_value.clone());
101            })
102    }
103
104    /// Creates a new labeled radio button with a closure that determines its selected state.
105    ///
106    /// This method is useful when you want a labeled radio button whose state is determined by a closure.
107    /// The label is also provided by a closure, allowing for dynamic updates.
108    pub fn new_labeled<S: std::fmt::Display + 'static, T>(
109        represented_value: T,
110        actual_value: impl Fn() -> T + 'static,
111        label: impl Fn() -> S + 'static,
112    ) -> ValueContainer<T>
113    where
114        T: Eq + PartialEq + Clone + 'static,
115    {
116        let (inbound_signal, outbound_signal) = create_value_container_signals(actual_value);
117        let clone = represented_value.clone();
118
119        value_container(
120            Stack::horizontal((
121                radio_button_svg(represented_value.clone(), inbound_signal.read_only()),
122                views::Label::derived(label),
123            ))
124            .class(LabeledRadioButtonClass)
125            .style(move |s| {
126                s.items_center()
127                    .keyboard_navigable()
128                    .apply_if(clone == inbound_signal.get(), |s| {
129                        s.apply_selectors(&[StyleSelector::Selected])
130                    })
131            })
132            .action(move || {
133                outbound_signal.set(represented_value.clone());
134            }),
135            move || outbound_signal.get(),
136        )
137    }
138
139    /// Creates a new labeled radio button with a signal that provides its selected state.
140    ///
141    /// Use this method when you have a signal that provides the current state of the radio button and you also want a label.
142    /// The radio button and label will automatically update based on the signal.
143    pub fn new_labeled_get<S: std::fmt::Display + 'static, T>(
144        represented_value: T,
145        actual_value: impl SignalGet<T> + Copy + 'static,
146        label: impl Fn() -> S + 'static,
147    ) -> impl IntoView
148    where
149        T: Eq + PartialEq + Clone + 'static,
150    {
151        let clone = represented_value.clone();
152        Stack::horizontal((
153            radio_button_svg(represented_value, actual_value),
154            views::Label::derived(label),
155        ))
156        .class(LabeledRadioButtonClass)
157        .style(move |s| {
158            s.items_center()
159                .keyboard_navigable()
160                .apply_if(clone == actual_value.get(), |s| s.set_selected(true))
161        })
162    }
163
164    /// Creates a new labeled radio button with a signal that provides and updates its selected state.
165    ///
166    /// This method is ideal when you need a labeled radio button that not only reflects a signal's state but also updates it.
167    /// Clicking the radio button will set the signal to the represented value.
168    pub fn new_labeled_rw<S: std::fmt::Display + 'static, T>(
169        represented_value: T,
170        actual_value: impl SignalGet<T> + SignalUpdate<T> + Copy + 'static,
171        label: impl Fn() -> S + 'static,
172    ) -> impl IntoView
173    where
174        T: Eq + PartialEq + Clone + 'static,
175    {
176        let cloneable_represented_value = represented_value.clone();
177        let cloneable_represented_value_ = represented_value.clone();
178
179        Stack::horizontal((
180            radio_button_svg(cloneable_represented_value.clone(), actual_value),
181            views::Label::derived(label),
182        ))
183        .class(LabeledRadioButtonClass)
184        .style(move |s| {
185            s.items_center().keyboard_navigable().apply_if(
186                cloneable_represented_value_.clone() == actual_value.get(),
187                |s| s.set_selected(true),
188            )
189        })
190        .action(move || {
191            actual_value.set(cloneable_represented_value.clone());
192        })
193    }
194}
195
196/// Renders a radio button that appears as selected if the signal equals the given enum value.
197/// Can be combined with a label and a stack with a click event (as in `examples/widget-gallery`).
198pub fn radio_button<T>(
199    represented_value: T,
200    actual_value: impl Fn() -> T + 'static,
201) -> ValueContainer<T>
202where
203    T: Eq + PartialEq + Clone + 'static,
204{
205    RadioButton::new(represented_value, actual_value)
206}
207
208/// Renders a radio button that appears as selected if the signal equals the given enum value.
209pub fn labeled_radio_button<S: std::fmt::Display + 'static, T>(
210    represented_value: T,
211    actual_value: impl Fn() -> T + 'static,
212    label: impl Fn() -> S + 'static,
213) -> ValueContainer<T>
214where
215    T: Eq + PartialEq + Clone + 'static,
216{
217    RadioButton::new_labeled(represented_value, actual_value, label)
218}
219
220#[cfg(test)]
221mod test {
222    use super::*;
223    use floem_reactive::{RwSignal, SignalGet, SignalUpdate};
224
225    #[test]
226    fn test_radio_button_new_initial_value() {
227        let actual_value = RwSignal::new(String::from("Option1"));
228        let _radio_button = RadioButton::new_rw("Option1".to_string(), actual_value);
229        assert_eq!(actual_value.get(), "Option1");
230    }
231
232    #[test]
233    fn test_radio_button_new_changes_state() {
234        let actual_value = RwSignal::new(String::from("Option1"));
235        let _radio_button = RadioButton::new_rw("Option2".to_string(), actual_value);
236        actual_value.set("Option2".to_string());
237        assert_eq!(actual_value.get(), "Option2");
238    }
239
240    #[test]
241    fn test_labeled_radio_button_initial_value() {
242        let actual_value = RwSignal::new(String::from("OptionA"));
243        let _labeled_radio_button = RadioButton::new_labeled_rw(
244            "OptionA".to_string(),
245            actual_value,
246            || "Label for Option A",
247        );
248
249        assert_eq!(actual_value.get(), "OptionA");
250    }
251
252    #[test]
253    fn test_labeled_radio_button_changes_state() {
254        let actual_value = RwSignal::new(String::from("OptionA"));
255        let _labeled_radio_button = RadioButton::new_labeled_rw(
256            "OptionB".to_string(),
257            actual_value,
258            || "Label for Option B",
259        );
260
261        actual_value.set("OptionB".to_string());
262
263        assert_eq!(actual_value.get(), "OptionB");
264    }
265
266    #[test]
267    fn test_radio_button_new_get() {
268        let actual_value = RwSignal::new(String::from("Option1"));
269        let _radio_button = RadioButton::new_get("Option1".to_string(), actual_value);
270        assert_eq!(actual_value.get(), "Option1");
271    }
272
273    #[test]
274    fn test_radio_button_new_labeled_get() {
275        let actual_value = RwSignal::new(String::from("OptionA"));
276        let _labeled_radio_button = RadioButton::new_labeled_get(
277            "OptionA".to_string(),
278            actual_value,
279            || "Label for Option A",
280        );
281
282        assert_eq!(actual_value.get(), "OptionA");
283    }
284}