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