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
31pub struct RadioButton;
37
38impl RadioButton {
39 #[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 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 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 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 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 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
196pub 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
208pub 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}