Skip to main content

freya_components/
radio_item.rs

1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6    define_theme,
7    get_theme,
8};
9
10define_theme! {
11    %[component]
12    pub RadioItem {
13        %[fields]
14        unselected_fill: Color,
15        selected_fill: Color,
16        border_fill: Color,
17    }
18}
19
20/// Radio component.
21///
22/// # Example
23///
24/// ```rust
25/// # use std::collections::HashSet;
26/// # use freya::prelude::*;
27/// fn app() -> impl IntoElement {
28///     let mut checked = use_state(|| false);
29///
30///     rect()
31///         .child(
32///             Tile::new()
33///                 .on_select(move |_| checked.toggle())
34///                 .child(RadioItem::new().selected(checked()))
35///                 .leading("Click to check"),
36///         )
37///         .child(
38///             Tile::new()
39///                 .on_select(move |_| checked.toggle())
40///                 .child(RadioItem::new().selected(!checked()))
41///                 .child("Click to check"),
42///         )
43/// }
44///
45/// # use freya_testing::prelude::*;
46/// # launch_doc(|| {
47/// #   rect().spacing(8.).center().expanded().child(app())
48/// # }, "./images/gallery_radio.png").render();
49/// ```
50///
51/// # Preview
52/// ![Radio Preview][radio]
53#[cfg_attr(feature = "docs",
54    doc = embed_doc_image::embed_image!("radio", "images/gallery_radio.png")
55)]
56#[derive(Clone, PartialEq)]
57pub struct RadioItem {
58    pub(crate) theme: Option<RadioItemThemePartial>,
59    key: DiffKey,
60    selected: bool,
61    size: f32,
62}
63
64impl KeyExt for RadioItem {
65    fn write_key(&mut self) -> &mut DiffKey {
66        &mut self.key
67    }
68}
69
70impl Default for RadioItem {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl RadioItem {
77    pub fn new() -> Self {
78        Self {
79            selected: false,
80            theme: None,
81            key: DiffKey::None,
82            size: 20.,
83        }
84    }
85
86    pub fn selected(mut self, selected: bool) -> Self {
87        self.selected = selected;
88        self
89    }
90
91    pub fn theme(mut self, theme: RadioItemThemePartial) -> Self {
92        self.theme = Some(theme);
93        self
94    }
95
96    pub fn size(mut self, size: impl Into<f32>) -> Self {
97        self.size = size.into();
98        self
99    }
100}
101
102impl Component for RadioItem {
103    fn render(&self) -> impl IntoElement {
104        let focus = use_focus();
105        let focus_status = use_focus_status(focus);
106        let RadioItemTheme {
107            unselected_fill,
108            selected_fill,
109            border_fill,
110        } = get_theme!(&self.theme, RadioItemThemePreference, "radio");
111
112        let animation = use_animation_with_dependencies(&self.selected, move |conf, selected| {
113            conf.on_change(OnChange::Rerun);
114            conf.on_creation(OnCreation::Finish);
115
116            let scale = AnimNum::new(0.7, 1.)
117                .time(250)
118                .ease(Ease::Out)
119                .function(Function::Expo);
120            let opacity = AnimNum::new(0., 1.)
121                .time(250)
122                .ease(Ease::Out)
123                .function(Function::Expo);
124
125            if *selected {
126                (scale, opacity)
127            } else {
128                (scale.into_reversed(), opacity.into_reversed())
129            }
130        });
131
132        let (scale, opacity) = animation.read().value();
133
134        let fill = if self.selected {
135            selected_fill
136        } else {
137            unselected_fill
138        };
139
140        let border = Border::new()
141            .fill(fill)
142            .width(2.)
143            .alignment(BorderAlignment::Inner);
144
145        let focused_border = (focus_status() == FocusStatus::Keyboard).then(|| {
146            Border::new()
147                .fill(border_fill)
148                .width((self.size * 0.15).ceil())
149                .alignment(BorderAlignment::Outer)
150        });
151
152        rect()
153            .a11y_id(focus.a11y_id())
154            .a11y_focusable(Focusable::Enabled)
155            .a11y_role(AccessibilityRole::RadioButton)
156            .width(Size::px(self.size))
157            .height(Size::px(self.size))
158            .border(border)
159            .border(focused_border)
160            .padding(Gaps::new_all(4.0))
161            .main_align(Alignment::center())
162            .cross_align(Alignment::center())
163            .corner_radius(CornerRadius::new_all(99.))
164            .on_key_down(move |e: Event<KeyboardEventData>| {
165                if !Focus::is_pressed(&e) {
166                    e.stop_propagation();
167                }
168            })
169            .maybe_child((self.selected || opacity > 0.).then(|| {
170                rect()
171                    .opacity(opacity)
172                    .scale(scale)
173                    .width(Size::px((self.size * 0.55).floor()))
174                    .height(Size::px((self.size * 0.55).floor()))
175                    .background(fill)
176                    .corner_radius(CornerRadius::new_all(99.))
177            }))
178    }
179
180    fn render_key(&self) -> DiffKey {
181        self.key.clone().or(self.default_key())
182    }
183}
184
185pub fn radio() -> RadioItem {
186    RadioItem::new()
187}