freya_components/
radio_item.rs1use 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#[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 KeyExt for RadioItem {
58 fn write_key(&mut self) -> &mut DiffKey {
59 &mut self.key
60 }
61}
62
63impl Default for RadioItem {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl RadioItem {
70 pub fn new() -> Self {
71 Self {
72 selected: false,
73 theme: None,
74 key: DiffKey::None,
75 size: 20.,
76 }
77 }
78
79 pub fn selected(mut self, selected: bool) -> Self {
80 self.selected = selected;
81 self
82 }
83
84 pub fn theme(mut self, theme: RadioItemThemePartial) -> Self {
85 self.theme = Some(theme);
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 Component 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 .a11y_role(AccessibilityRole::RadioButton)
149 .width(Size::px(self.size))
150 .height(Size::px(self.size))
151 .border(border)
152 .border(focused_border)
153 .padding(Gaps::new_all(4.0))
154 .main_align(Alignment::center())
155 .cross_align(Alignment::center())
156 .corner_radius(CornerRadius::new_all(99.))
157 .on_key_down(move |e: Event<KeyboardEventData>| {
158 if !Focus::is_pressed(&e) {
159 e.stop_propagation();
160 }
161 })
162 .maybe_child((self.selected || opacity > 0.).then(|| {
163 rect()
164 .opacity(opacity)
165 .scale(scale)
166 .width(Size::px((self.size * 0.55).floor()))
167 .height(Size::px((self.size * 0.55).floor()))
168 .background(fill)
169 .corner_radius(CornerRadius::new_all(99.))
170 }))
171 }
172
173 fn render_key(&self) -> DiffKey {
174 self.key.clone().or(self.default_key())
175 }
176}
177
178pub fn radio() -> RadioItem {
179 RadioItem::new()
180}