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