freya_components/
radio_item.rs1use 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#[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}