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::{
9 IconThemePartialExt,
10 arrow::ArrowIcon,
11 },
12 menu::MenuGroup,
13};
14
15define_theme! {
16 %[component]
17 pub Select {
18 %[fields]
19 width: Size,
20 margin: Gaps,
21 select_background: Color,
22 background_button: Color,
23 hover_background: Color,
24 border_fill: Color,
25 focus_border_fill: Color,
26 arrow_fill: Color,
27 color: Color,
28 }
29}
30
31#[derive(Debug, Default, PartialEq, Clone, Copy)]
32pub enum SelectStatus {
33 #[default]
34 Idle,
35 Hovering,
36}
37
38#[derive(Clone, PartialEq)]
67pub struct Select {
68 pub(crate) theme: Option<SelectThemePartial>,
69 selected_item: Option<Element>,
70 list_match_width: bool,
71 children: Vec<Element>,
72 cursor_icon: Option<CursorIcon>,
73 key: DiffKey,
74}
75
76impl ChildrenExt for Select {
77 fn children_mut(&mut self) -> &mut Vec<Element> {
78 &mut self.children
79 }
80}
81
82impl KeyExt for Select {
83 fn key_mut(&mut self) -> &mut DiffKey {
84 &mut self.key
85 }
86}
87
88impl Default for Select {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl Select {
95 pub fn new() -> Self {
96 Self {
97 theme: None,
98 selected_item: None,
99 list_match_width: false,
100 children: Vec::new(),
101 cursor_icon: None,
102 key: DiffKey::None,
103 }
104 }
105
106 pub fn theme(mut self, theme: SelectThemePartial) -> Self {
107 self.theme = Some(theme);
108 self
109 }
110
111 pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
112 self.selected_item = Some(item.into());
113 self
114 }
115
116 pub fn list_match_width(mut self, enabled: bool) -> Self {
118 self.list_match_width = enabled;
119 self
120 }
121
122 pub fn cursor(mut self, cursor_icon: impl Into<Option<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 focused = use_memo(move || {
141 a11y_id.is_focused()
142 || Platform::get()
143 .current_window()
144 .focused_accessibility_node
145 .read()
146 .member_of()
147 == Some(a11y_id)
148 });
149 let visible = open() && focused();
150
151 let animation = use_animation_with_dependencies(&visible, move |conf, visible| {
152 conf.on_change(OnChange::Rerun);
153 conf.on_creation(OnCreation::Finish);
154
155 let opacity = AnimNum::new(0., 1.)
156 .time(90)
157 .ease(Ease::Out)
158 .function(Function::Quad);
159 let offset_y = AnimNum::new(-3., 1.)
160 .time(90)
161 .ease(Ease::Out)
162 .function(Function::Quad);
163 if *visible {
164 (opacity, offset_y)
165 } else {
166 (opacity.into_reversed(), offset_y.into_reversed())
167 }
168 });
169
170 let (opacity, slide) = animation.read().value();
171
172 if !visible && opacity == 0. && list_size().is_some() {
174 let _ = list_size.take();
175 open.set_if_modified(false);
176 }
177
178 let on_press = move |e: Event<PressEventData>| {
179 if a11y_id.is_focused() {
180 open.toggle();
181 } else {
182 a11y_id.request_focus();
183 open.set(true);
184 }
185 e.prevent_default();
187 e.stop_propagation();
188 };
189
190 let on_pointer_enter = move |_| {
191 *status.write() = SelectStatus::Hovering;
192 };
193
194 let on_pointer_leave = move |_| {
195 *status.write() = SelectStatus::Idle;
196 };
197
198 let on_global_pointer_up = 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 a11y_id.is_focused() => {
208 open.toggle();
209 }
210 _ => {}
211 };
212
213 let offset_y = match (button_area(), list_size()) {
214 (Some(button), Some(list)) => {
215 let root_height = Platform::get().current_window().root_size.peek().height;
216 let space_below = root_height - button.max_y();
217 let space_above = button.min_y();
218 let flips = list.height > space_below && list.height <= space_above;
219 if flips {
220 -(button.height() + list.height) - slide
221 } else {
222 slide
223 }
224 }
225 _ => slide,
226 };
227
228 let opacity = if list_size().is_some() { opacity } else { 0. };
229
230 let background = match *status.read() {
231 SelectStatus::Hovering => theme.hover_background,
232 SelectStatus::Idle => theme.background_button,
233 };
234
235 let border = if focus() == Focus::Keyboard {
236 Border::new()
237 .fill(theme.focus_border_fill)
238 .width(2.)
239 .alignment(BorderAlignment::Inner)
240 } else {
241 Border::new()
242 .fill(theme.border_fill)
243 .width(1.)
244 .alignment(BorderAlignment::Inner)
245 };
246
247 rect()
248 .child(
249 rect()
250 .a11y_id(a11y_id)
251 .a11y_member_of(a11y_id)
252 .a11y_role(AccessibilityRole::ListBox)
253 .a11y_focusable(Focusable::Enabled)
254 .on_pointer_enter(on_pointer_enter)
255 .on_pointer_leave(on_pointer_leave)
256 .cursor(self.cursor_icon)
257 .on_press(on_press)
258 .on_global_key_down(on_global_key_down)
259 .on_global_pointer_up(on_global_pointer_up)
260 .on_sized(move |e: Event<SizedEventData>| {
261 button_area.set_if_modified(Some(e.area));
262 })
263 .width(theme.width)
264 .margin(theme.margin)
265 .background(background)
266 .padding((8., 18., 8., 18.))
267 .border(border)
268 .horizontal()
269 .content(Content::flex())
270 .center()
271 .color(theme.color)
272 .corner_radius(8.)
273 .maybe_child(self.selected_item.clone())
274 .child(
275 ArrowIcon::new()
276 .margin((0., 0., 0., 8.))
277 .rotate(0.)
278 .fill(theme.arrow_fill),
279 ),
280 )
281 .maybe_child((visible || opacity > 0.).then(|| {
282 rect().height(Size::px(0.)).width(Size::px(0.)).child(
283 rect()
284 .width(if self.list_match_width {
285 Size::px(button_area().map_or(0., |area| area.width()))
286 } else {
287 Size::window_percent(100.)
288 })
289 .margin(Gaps::new(4., 0., 4., 0.))
290 .offset_y(offset_y)
291 .on_sized(move |e: Event<SizedEventData>| {
292 list_size.set_if_modified(Some(e.area.size));
293 })
294 .child(
295 rect()
296 .layer(Layer::Overlay)
297 .maybe(self.list_match_width, |el| el.width(Size::fill()))
298 .border(
299 Border::new()
300 .fill(theme.border_fill)
301 .width(1.)
302 .alignment(BorderAlignment::Inner),
303 )
304 .overflow(Overflow::Clip)
305 .corner_radius(10.)
306 .background(theme.select_background)
307 .padding(6.)
308 .maybe(!self.list_match_width, |el| el.content(Content::Fit))
309 .opacity(opacity)
310 .children(self.children.clone()),
311 ),
312 )
313 }))
314 }
315
316 fn render_key(&self) -> DiffKey {
317 self.key.clone().or(self.default_key())
318 }
319}