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 cursor_icon: CursorIcon,
80 key: DiffKey,
81}
82
83impl ChildrenExt for Select {
84 fn get_children(&mut self) -> &mut Vec<Element> {
85 &mut self.children
86 }
87}
88
89impl KeyExt for Select {
90 fn write_key(&mut self) -> &mut DiffKey {
91 &mut self.key
92 }
93}
94
95impl Default for Select {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl Select {
102 pub fn new() -> Self {
103 Self {
104 theme: None,
105 selected_item: None,
106 children: Vec::new(),
107 cursor_icon: CursorIcon::default(),
108 key: DiffKey::None,
109 }
110 }
111
112 pub fn theme(mut self, theme: SelectThemePartial) -> Self {
113 self.theme = Some(theme);
114 self
115 }
116
117 pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
118 self.selected_item = Some(item.into());
119 self
120 }
121
122 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
124 self.cursor_icon = cursor_icon.into();
125 self
126 }
127}
128
129impl Component for Select {
130 fn render(&self) -> impl IntoElement {
131 let theme = get_theme!(&self.theme, SelectThemePreference, "select");
132 let a11y_id = use_a11y();
133 let focus = use_focus(a11y_id);
134 let mut status = use_state(SelectStatus::default);
135 let mut open = use_state(|| false);
136 let mut button_area = use_state(|| None::<Area>);
137 let mut list_size = use_state(|| None::<Size2D>);
138 use_provide_context(|| MenuGroup { group_id: a11y_id });
139
140 let animation = use_animation(move |conf| {
141 conf.on_change(OnChange::Rerun);
142 conf.on_creation(OnCreation::Finish);
143
144 let scale = AnimNum::new(0.9, 1.)
145 .time(125)
146 .ease(Ease::Out)
147 .function(Function::Quart);
148 let opacity = AnimNum::new(0., 1.)
149 .time(125)
150 .ease(Ease::Out)
151 .function(Function::Quart);
152 let offset_y = AnimNum::new(-8., 1.)
153 .time(125)
154 .ease(Ease::Out)
155 .function(Function::Quart);
156 if open() {
157 (scale, opacity, offset_y)
158 } else {
159 (
160 scale.into_reversed(),
161 opacity.into_reversed(),
162 offset_y.into_reversed(),
163 )
164 }
165 });
166
167 let (scale, opacity, slide) = animation.read().value();
168
169 if !open() && opacity == 0. && list_size().is_some() {
171 let _ = list_size.take();
172 }
173
174 let cursor_icon = self.cursor_icon;
175 use_drop(move || {
176 if status() == SelectStatus::Hovering {
177 Cursor::set(CursorIcon::default());
178 }
179 });
180
181 use_side_effect(move || {
183 let platform = Platform::get();
184 let focus_within =
185 platform.focused_accessibility_node.read().member_of() == Some(a11y_id);
186 if !focus_within && list_size.peek().is_some() {
187 open.set_if_modified(false);
188 }
189 });
190
191 let on_press = move |e: Event<PressEventData>| {
192 a11y_id.request_focus();
193 open.toggle();
194 e.prevent_default();
196 e.stop_propagation();
197 };
198
199 let on_pointer_enter = move |_| {
200 *status.write() = SelectStatus::Hovering;
201 Cursor::set(cursor_icon);
202 };
203
204 let on_pointer_leave = move |_| {
205 *status.write() = SelectStatus::Idle;
206 Cursor::set(CursorIcon::default());
207 };
208
209 let on_global_pointer_press = move |_: Event<PointerEventData>| {
211 open.set_if_modified(false);
212 };
213
214 let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
215 Key::Named(NamedKey::Escape) => {
216 open.set_if_modified(false);
217 }
218 Key::Named(NamedKey::Enter) if a11y_id.is_focused() => {
219 open.toggle();
220 }
221 _ => {}
222 };
223
224 let offset_y = match (button_area(), list_size()) {
225 (Some(button), Some(list)) => {
226 let root_height = Platform::get().root_size.peek().height;
227 let space_below = root_height - button.max_y();
228 let space_above = button.min_y();
229 let flips = list.height > space_below && list.height <= space_above;
230 if flips {
231 -(button.height() + list.height) - slide
232 } else {
233 slide
234 }
235 }
236 _ => slide,
237 };
238
239 let opacity = if list_size().is_some() { opacity } else { 0. };
240
241 let background = match *status.read() {
242 SelectStatus::Hovering => theme.hover_background,
243 SelectStatus::Idle => theme.background_button,
244 };
245
246 let border = if focus() == Focus::Keyboard {
247 Border::new()
248 .fill(theme.focus_border_fill)
249 .width(2.)
250 .alignment(BorderAlignment::Inner)
251 } else {
252 Border::new()
253 .fill(theme.border_fill)
254 .width(1.)
255 .alignment(BorderAlignment::Inner)
256 };
257
258 rect()
259 .child(
260 rect()
261 .a11y_id(a11y_id)
262 .a11y_member_of(a11y_id)
263 .a11y_role(AccessibilityRole::ListBox)
264 .a11y_focusable(Focusable::Enabled)
265 .on_pointer_enter(on_pointer_enter)
266 .on_pointer_leave(on_pointer_leave)
267 .on_press(on_press)
268 .on_global_key_down(on_global_key_down)
269 .on_global_pointer_press(on_global_pointer_press)
270 .on_sized(move |e: Event<SizedEventData>| {
271 button_area.set_if_modified(Some(e.area));
272 })
273 .width(theme.width)
274 .margin(theme.margin)
275 .background(background)
276 .padding((8., 18., 8., 18.))
277 .border(border)
278 .horizontal()
279 .center()
280 .color(theme.color)
281 .corner_radius(8.)
282 .maybe_child(self.selected_item.clone())
283 .child(
284 ArrowIcon::new()
285 .margin((0., 0., 0., 8.))
286 .rotate(0.)
287 .fill(theme.arrow_fill),
288 ),
289 )
290 .maybe_child((open() || opacity > 0.).then(|| {
291 rect().height(Size::px(0.)).width(Size::px(0.)).child(
292 rect()
293 .width(Size::window_percent(100.))
294 .margin(Gaps::new(4., 0., 4., 0.))
295 .offset_y(offset_y)
296 .on_sized(move |e: Event<SizedEventData>| {
297 list_size.set_if_modified(Some(e.area.size));
298 })
299 .child(
300 rect()
301 .layer(Layer::Overlay)
302 .border(
303 Border::new()
304 .fill(theme.border_fill)
305 .width(1.)
306 .alignment(BorderAlignment::Inner),
307 )
308 .overflow(Overflow::Clip)
309 .corner_radius(8.)
310 .background(theme.select_background)
311 .padding(4.)
312 .content(Content::Fit)
313 .opacity(opacity)
314 .scale(scale)
315 .children(self.children.clone()),
316 ),
317 )
318 }))
319 }
320
321 fn render_key(&self) -> DiffKey {
322 self.key.clone().or(self.default_key())
323 }
324}