freya_components/
radio_item.rs

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