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 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}