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