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