freya_components/
select.rs1use freya_animation::prelude::*;
2use freya_core::prelude::*;
3use torin::prelude::*;
4
5use crate::{
6 define_theme,
7 get_theme,
8 icons::arrow::ArrowIcon,
9 menu::MenuGroup,
10};
11
12define_theme! {
13 %[component]
14 pub Select {
15 %[fields]
16 width: Size,
17 margin: Gaps,
18 select_background: Color,
19 background_button: Color,
20 hover_background: Color,
21 border_fill: Color,
22 focus_border_fill: Color,
23 arrow_fill: Color,
24 color: Color,
25 }
26}
27
28#[derive(Debug, Default, PartialEq, Clone, Copy)]
29pub enum SelectStatus {
30 #[default]
31 Idle,
32 Hovering,
33}
34
35#[cfg_attr(feature = "docs",
72 doc = embed_doc_image::embed_image!("select", "images/gallery_select.png")
73)]
74#[derive(Clone, PartialEq)]
75pub struct Select {
76 pub(crate) theme: Option<SelectThemePartial>,
77 selected_item: Option<Element>,
78 children: Vec<Element>,
79 key: DiffKey,
80}
81
82impl ChildrenExt for Select {
83 fn get_children(&mut self) -> &mut Vec<Element> {
84 &mut self.children
85 }
86}
87
88impl KeyExt for Select {
89 fn write_key(&mut self) -> &mut DiffKey {
90 &mut self.key
91 }
92}
93
94impl Default for Select {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100impl Select {
101 pub fn new() -> Self {
102 Self {
103 theme: None,
104 selected_item: None,
105 children: Vec::new(),
106 key: DiffKey::None,
107 }
108 }
109
110 pub fn theme(mut self, theme: SelectThemePartial) -> Self {
111 self.theme = Some(theme);
112 self
113 }
114
115 pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
116 self.selected_item = Some(item.into());
117 self
118 }
119}
120
121impl Component for Select {
122 fn render(&self) -> impl IntoElement {
123 let theme = get_theme!(&self.theme, SelectThemePreference, "select");
124 let focus = use_focus();
125 let focus_status = use_focus_status(focus);
126 let mut status = use_state(SelectStatus::default);
127 let mut open = use_state(|| false);
128 use_provide_context(|| MenuGroup {
129 group_id: focus.a11y_id(),
130 });
131
132 let animation = use_animation(move |conf| {
133 conf.on_change(OnChange::Rerun);
134 conf.on_creation(OnCreation::Finish);
135
136 let scale = AnimNum::new(0.9, 1.)
137 .time(125)
138 .ease(Ease::Out)
139 .function(Function::Quart);
140 let opacity = AnimNum::new(0., 1.)
141 .time(125)
142 .ease(Ease::Out)
143 .function(Function::Quart);
144 let offset_y = AnimNum::new(-8., 1.)
145 .time(125)
146 .ease(Ease::Out)
147 .function(Function::Quart);
148 if open() {
149 (scale, opacity, offset_y)
150 } else {
151 (
152 scale.into_reversed(),
153 opacity.into_reversed(),
154 offset_y.into_reversed(),
155 )
156 }
157 });
158
159 use_drop(move || {
160 if status() == SelectStatus::Hovering {
161 Cursor::set(CursorIcon::default());
162 }
163 });
164
165 use_side_effect(move || {
167 let platform = Platform::get();
168 if *platform.navigation_mode.read() == NavigationMode::Keyboard {
169 let should_close = platform
170 .focused_accessibility_node
171 .read()
172 .member_of()
173 .is_none_or(|member_of| member_of != focus.a11y_id());
174 if should_close {
175 open.set_if_modified(false);
176 }
177 }
178 });
179
180 let on_press = move |e: Event<PressEventData>| {
181 focus.request_focus();
182 open.toggle();
183 e.prevent_default();
185 e.stop_propagation();
186 };
187
188 let on_pointer_enter = move |_| {
189 *status.write() = SelectStatus::Hovering;
190 Cursor::set(CursorIcon::Pointer);
191 };
192
193 let on_pointer_leave = move |_| {
194 *status.write() = SelectStatus::Idle;
195 Cursor::set(CursorIcon::default());
196 };
197
198 let on_global_pointer_press = move |_: Event<PointerEventData>| {
200 open.set_if_modified(false);
201 };
202
203 let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
204 Key::Named(NamedKey::Escape) => {
205 open.set_if_modified(false);
206 }
207 Key::Named(NamedKey::Enter) if focus.is_focused() => {
208 open.toggle();
209 }
210 _ => {}
211 };
212
213 let (scale, opacity, offset_y) = animation.read().value();
214
215 let background = match *status.read() {
216 SelectStatus::Hovering => theme.hover_background,
217 SelectStatus::Idle => theme.background_button,
218 };
219
220 let border = if focus_status() == FocusStatus::Keyboard {
221 Border::new()
222 .fill(theme.focus_border_fill)
223 .width(2.)
224 .alignment(BorderAlignment::Inner)
225 } else {
226 Border::new()
227 .fill(theme.border_fill)
228 .width(1.)
229 .alignment(BorderAlignment::Inner)
230 };
231
232 rect()
233 .child(
234 rect()
235 .a11y_id(focus.a11y_id())
236 .a11y_member_of(focus.a11y_id())
237 .a11y_role(AccessibilityRole::ListBox)
238 .a11y_focusable(Focusable::Enabled)
239 .on_pointer_enter(on_pointer_enter)
240 .on_pointer_leave(on_pointer_leave)
241 .on_press(on_press)
242 .on_global_key_down(on_global_key_down)
243 .on_global_pointer_press(on_global_pointer_press)
244 .width(theme.width)
245 .margin(theme.margin)
246 .background(background)
247 .padding((8., 18., 8., 18.))
248 .border(border)
249 .horizontal()
250 .center()
251 .color(theme.color)
252 .corner_radius(8.)
253 .maybe_child(self.selected_item.clone())
254 .child(
255 ArrowIcon::new()
256 .margin((0., 0., 0., 8.))
257 .rotate(0.)
258 .fill(theme.arrow_fill),
259 ),
260 )
261 .maybe_child((open() || opacity > 0.).then(|| {
262 rect().height(Size::px(0.)).width(Size::px(0.)).child(
263 rect()
264 .width(Size::window_percent(100.))
265 .margin(Gaps::new(4., 0., 0., 0.))
266 .offset_y(offset_y)
267 .child(
268 rect()
269 .layer(Layer::Overlay)
270 .border(
271 Border::new()
272 .fill(theme.border_fill)
273 .width(1.)
274 .alignment(BorderAlignment::Inner),
275 )
276 .overflow(Overflow::Clip)
277 .corner_radius(8.)
278 .background(theme.select_background)
279 .padding(4.)
280 .content(Content::Fit)
281 .opacity(opacity)
282 .scale(scale)
283 .children(self.children.clone()),
284 ),
285 )
286 }))
287 }
288
289 fn render_key(&self) -> DiffKey {
290 self.key.clone().or(self.default_key())
291 }
292}