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().spacing(8.).center().expanded().child(app())
41/// # }, "./images/gallery_radio.png").render();
42/// ```
43///
44/// # Preview
45/// ![Radio Preview][radio]
46#[cfg_attr(feature = "docs",
47    doc = embed_doc_image::embed_image!("radio", "images/gallery_radio.png")
48)]
49#[derive(Clone, PartialEq)]
50pub struct RadioItem {
51    pub(crate) theme: Option<RadioItemThemePartial>,
52    key: DiffKey,
53    selected: bool,
54    size: f32,
55}
56
57impl Default for RadioItem {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63impl RadioItem {
64    pub fn new() -> Self {
65        Self {
66            selected: false,
67            theme: None,
68            key: DiffKey::None,
69            size: 20.,
70        }
71    }
72
73    pub fn selected(mut self, selected: bool) -> Self {
74        self.selected = selected;
75        self
76    }
77
78    pub fn theme(mut self, theme: RadioItemThemePartial) -> Self {
79        self.theme = Some(theme);
80        self
81    }
82
83    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
84        self.key = key.into();
85        self
86    }
87
88    pub fn size(mut self, size: impl Into<f32>) -> Self {
89        self.size = size.into();
90        self
91    }
92}
93
94impl Component for RadioItem {
95    fn render(&self) -> impl IntoElement {
96        let focus = use_focus();
97        let focus_status = use_focus_status(focus);
98        let RadioItemTheme {
99            unselected_fill,
100            selected_fill,
101            border_fill,
102        } = get_theme!(&self.theme, radio);
103
104        let animation = use_animation_with_dependencies(&self.selected, move |conf, selected| {
105            conf.on_change(OnChange::Rerun);
106            conf.on_creation(OnCreation::Finish);
107
108            let scale = AnimNum::new(0.7, 1.)
109                .time(250)
110                .ease(Ease::Out)
111                .function(Function::Expo);
112            let opacity = AnimNum::new(0., 1.)
113                .time(250)
114                .ease(Ease::Out)
115                .function(Function::Expo);
116
117            if *selected {
118                (scale, opacity)
119            } else {
120                (scale.into_reversed(), opacity.into_reversed())
121            }
122        });
123
124        let (scale, opacity) = animation.read().value();
125
126        let fill = if self.selected {
127            selected_fill
128        } else {
129            unselected_fill
130        };
131
132        let border = Border::new()
133            .fill(fill)
134            .width(2.)
135            .alignment(BorderAlignment::Inner);
136
137        let focused_border = (focus_status() == FocusStatus::Keyboard).then(|| {
138            Border::new()
139                .fill(border_fill)
140                .width((self.size * 0.15).ceil())
141                .alignment(BorderAlignment::Outer)
142        });
143
144        rect()
145            .a11y_id(focus.a11y_id())
146            .a11y_focusable(Focusable::Enabled)
147            .a11y_role(AccessibilityRole::RadioButton)
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}