1use std::sync::Arc;
10use std::sync::Mutex;
11
12use crate::prelude::Model;
13use fret_ui::action::{DismissReason, OnActivate, OnCloseAutoFocus, OnDismissRequest};
14use fret_ui::elements::GlobalElementId;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ComboboxOpenChangeReason {
19 TriggerPress,
20 OutsidePress,
21 ItemPress,
22 EscapeKey,
23 FocusOut,
24 None,
25}
26
27pub fn open_change_reason_from_dismiss_reason(reason: DismissReason) -> ComboboxOpenChangeReason {
28 match reason {
29 DismissReason::Escape => ComboboxOpenChangeReason::EscapeKey,
30 DismissReason::OutsidePress { .. } => ComboboxOpenChangeReason::OutsidePress,
31 DismissReason::FocusOutside => ComboboxOpenChangeReason::FocusOut,
32 DismissReason::Scroll => ComboboxOpenChangeReason::None,
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ComboboxCloseAutoFocusDecision {
38 Default,
40 PreventDefault,
42 RestoreTrigger,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct ComboboxCloseAutoFocusPolicy {
49 pub on_item_press: ComboboxCloseAutoFocusDecision,
50 pub on_escape: ComboboxCloseAutoFocusDecision,
51 pub on_trigger_press: ComboboxCloseAutoFocusDecision,
52 pub on_outside_press: ComboboxCloseAutoFocusDecision,
53 pub on_focus_out: ComboboxCloseAutoFocusDecision,
54 pub on_none: ComboboxCloseAutoFocusDecision,
55}
56
57impl Default for ComboboxCloseAutoFocusPolicy {
58 fn default() -> Self {
59 Self {
65 on_item_press: ComboboxCloseAutoFocusDecision::RestoreTrigger,
66 on_escape: ComboboxCloseAutoFocusDecision::RestoreTrigger,
67 on_trigger_press: ComboboxCloseAutoFocusDecision::RestoreTrigger,
68 on_outside_press: ComboboxCloseAutoFocusDecision::RestoreTrigger,
69 on_focus_out: ComboboxCloseAutoFocusDecision::PreventDefault,
70 on_none: ComboboxCloseAutoFocusDecision::Default,
71 }
72 }
73}
74
75pub fn close_auto_focus_decision_for_reason(
76 policy: ComboboxCloseAutoFocusPolicy,
77 reason: ComboboxOpenChangeReason,
78) -> ComboboxCloseAutoFocusDecision {
79 match reason {
80 ComboboxOpenChangeReason::ItemPress => policy.on_item_press,
81 ComboboxOpenChangeReason::EscapeKey => policy.on_escape,
82 ComboboxOpenChangeReason::TriggerPress => policy.on_trigger_press,
83 ComboboxOpenChangeReason::OutsidePress => policy.on_outside_press,
84 ComboboxOpenChangeReason::FocusOut => policy.on_focus_out,
85 ComboboxOpenChangeReason::None => policy.on_none,
86 }
87}
88
89#[derive(Debug, Default, Clone, Copy)]
91pub struct ClearQueryOnCloseState {
92 was_open: bool,
93}
94
95pub fn should_clear_query_on_close(state: &mut ClearQueryOnCloseState, open: bool) -> bool {
96 let should_clear = state.was_open && !open;
97 state.was_open = open;
98 should_clear
99}
100
101#[derive(Debug, Default, Clone)]
105pub struct OpenChangeCallbackState {
106 initialized: bool,
107 last_open: bool,
108 pending_complete: Option<bool>,
109}
110
111pub fn open_change_events(
112 state: &mut OpenChangeCallbackState,
113 open: bool,
114 present: bool,
115 animating: bool,
116) -> (Option<bool>, Option<bool>) {
117 let mut changed = None;
118 let mut completed = None;
119
120 if !state.initialized {
121 state.initialized = true;
122 state.last_open = open;
123 } else if state.last_open != open {
124 state.last_open = open;
125 state.pending_complete = Some(open);
126 changed = Some(open);
127 }
128
129 if state.pending_complete == Some(open) && present == open && !animating {
130 state.pending_complete = None;
131 completed = Some(open);
132 }
133
134 (changed, completed)
135}
136
137#[derive(Debug, Default, Clone)]
139pub struct ValueChangeCallbackState<T> {
140 initialized: bool,
141 last_value: Option<T>,
142}
143
144pub fn value_change_event<T: Clone + PartialEq>(
145 state: &mut ValueChangeCallbackState<T>,
146 value: Option<T>,
147) -> Option<Option<T>> {
148 if !state.initialized {
149 state.initialized = true;
150 state.last_value = value;
151 return None;
152 }
153
154 if state.last_value != value {
155 state.last_value = value.clone();
156 return Some(value);
157 }
158
159 None
160}
161
162pub type OnOpenChange = Arc<dyn Fn(bool) + Send + Sync + 'static>;
163pub type OnOpenChangeWithReason =
164 Arc<dyn Fn(bool, ComboboxOpenChangeReason) + Send + Sync + 'static>;
165
166#[derive(Debug, Clone, Copy)]
168pub struct SelectionCommitPolicy {
169 pub toggle_selected_to_none: bool,
171 pub close_on_commit: bool,
173 pub clear_query_on_commit: bool,
175}
176
177impl Default for SelectionCommitPolicy {
178 fn default() -> Self {
179 Self {
180 toggle_selected_to_none: true,
181 close_on_commit: true,
182 clear_query_on_commit: true,
183 }
184 }
185}
186
187pub fn set_open_change_reason_on_activate(
188 open_change_reason: Model<Option<ComboboxOpenChangeReason>>,
189 reason: ComboboxOpenChangeReason,
190) -> OnActivate {
191 #[allow(clippy::arc_with_non_send_sync)]
192 Arc::new(move |host, action_cx, _activate_reason| {
193 let _ = host
194 .models_mut()
195 .update(&open_change_reason, |v| *v = Some(reason));
196 host.request_redraw(action_cx.window);
197 })
198}
199
200pub fn set_open_change_reason_on_dismiss_request(
201 open_change_reason: Model<Option<ComboboxOpenChangeReason>>,
202) -> OnDismissRequest {
203 #[allow(clippy::arc_with_non_send_sync)]
204 Arc::new(move |host, action_cx, req| {
205 let reason = open_change_reason_from_dismiss_reason(req.reason);
206 let _ = host
207 .models_mut()
208 .update(&open_change_reason, |v| *v = Some(reason));
209 host.request_redraw(action_cx.window);
210 })
211}
212
213pub fn commit_selection_on_activate<T: Clone + PartialEq + 'static>(
214 policy: SelectionCommitPolicy,
215 value: Model<Option<T>>,
216 open: Model<bool>,
217 query: Model<String>,
218 open_change_reason: Model<Option<ComboboxOpenChangeReason>>,
219 selected_value: T,
220) -> OnActivate {
221 #[allow(clippy::arc_with_non_send_sync)]
222 Arc::new(move |host, action_cx, _activate_reason| {
223 let _ = host.models_mut().update(&value, |v| {
224 if policy.toggle_selected_to_none
225 && v.as_ref().is_some_and(|cur| cur == &selected_value)
226 {
227 *v = None;
228 } else {
229 *v = Some(selected_value.clone());
230 }
231 });
232 let _ = host.models_mut().update(&open_change_reason, |v| {
233 *v = Some(ComboboxOpenChangeReason::ItemPress);
234 });
235 if policy.close_on_commit {
236 let _ = host.models_mut().update(&open, |v| *v = false);
237 }
238 if policy.clear_query_on_commit {
239 let _ = host.models_mut().update(&query, |v| v.clear());
240 }
241 host.request_redraw(action_cx.window);
242 })
243}
244
245pub fn commit_multi_selection_on_activate<T: Clone + PartialEq + 'static>(
250 value: Model<Vec<T>>,
251 open: Model<bool>,
252 query: Model<String>,
253 open_change_reason: Model<Option<ComboboxOpenChangeReason>>,
254 selected_value: T,
255 close_on_commit: bool,
256 clear_query_on_commit: bool,
257) -> OnActivate {
258 #[allow(clippy::arc_with_non_send_sync)]
259 Arc::new(move |host, action_cx, _activate_reason| {
260 let _ = host.models_mut().update(&value, |values| {
261 if let Some(idx) = values.iter().position(|v| v == &selected_value) {
262 values.remove(idx);
263 } else {
264 values.push(selected_value.clone());
265 }
266 });
267 let _ = host.models_mut().update(&open_change_reason, |v| {
268 *v = Some(ComboboxOpenChangeReason::ItemPress);
269 });
270 if close_on_commit {
271 let _ = host.models_mut().update(&open, |v| *v = false);
272 }
273 if clear_query_on_commit {
274 let _ = host.models_mut().update(&query, |v| v.clear());
275 }
276 host.request_redraw(action_cx.window);
277 })
278}
279
280pub fn on_close_auto_focus_with_reason(
281 open_change_reason: Model<Option<ComboboxOpenChangeReason>>,
282 trigger_id: Arc<Mutex<Option<GlobalElementId>>>,
283 policy: ComboboxCloseAutoFocusPolicy,
284) -> OnCloseAutoFocus {
285 #[allow(clippy::arc_with_non_send_sync)]
286 Arc::new(move |host, _action_cx, req| {
287 let reason = host
288 .models_mut()
289 .get_copied(&open_change_reason)
290 .unwrap_or(None)
291 .unwrap_or(ComboboxOpenChangeReason::None);
292 let _ = host.models_mut().update(&open_change_reason, |v| *v = None);
294
295 match close_auto_focus_decision_for_reason(policy, reason) {
296 ComboboxCloseAutoFocusDecision::Default => {}
297 ComboboxCloseAutoFocusDecision::PreventDefault => {
298 req.prevent_default();
299 }
300 ComboboxCloseAutoFocusDecision::RestoreTrigger => {
301 req.prevent_default();
302 let target = *trigger_id.lock().unwrap_or_else(|e| e.into_inner());
303 if let Some(target) = target {
304 host.request_focus(target);
305 }
306 }
307 }
308 })
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn open_change_events_emit_change_and_complete_after_settle() {
317 let mut state = OpenChangeCallbackState::default();
318
319 let (changed, completed) = open_change_events(&mut state, false, false, false);
320 assert_eq!((changed, completed), (None, None));
321
322 let (changed, completed) = open_change_events(&mut state, true, true, true);
323 assert_eq!((changed, completed), (Some(true), None));
324
325 let (changed, completed) = open_change_events(&mut state, true, true, false);
326 assert_eq!((changed, completed), (None, Some(true)));
327
328 let (changed, completed) = open_change_events(&mut state, false, true, true);
329 assert_eq!((changed, completed), (Some(false), None));
330
331 let (changed, completed) = open_change_events(&mut state, false, false, false);
332 assert_eq!((changed, completed), (None, Some(false)));
333 }
334
335 #[test]
336 fn open_change_events_complete_without_animation() {
337 let mut state = OpenChangeCallbackState::default();
338
339 let _ = open_change_events(&mut state, false, false, false);
340 let (changed, completed) = open_change_events(&mut state, true, true, false);
341 assert_eq!((changed, completed), (Some(true), Some(true)));
342
343 let (changed, completed) = open_change_events(&mut state, false, false, false);
344 assert_eq!((changed, completed), (Some(false), Some(false)));
345 }
346
347 #[test]
348 fn open_change_reason_maps_dismiss_reasons() {
349 assert_eq!(
350 open_change_reason_from_dismiss_reason(DismissReason::Escape),
351 ComboboxOpenChangeReason::EscapeKey
352 );
353 assert_eq!(
354 open_change_reason_from_dismiss_reason(DismissReason::OutsidePress { pointer: None }),
355 ComboboxOpenChangeReason::OutsidePress
356 );
357 assert_eq!(
358 open_change_reason_from_dismiss_reason(DismissReason::FocusOutside),
359 ComboboxOpenChangeReason::FocusOut
360 );
361 assert_eq!(
362 open_change_reason_from_dismiss_reason(DismissReason::Scroll),
363 ComboboxOpenChangeReason::None
364 );
365 }
366
367 #[test]
368 fn value_change_event_emits_only_on_state_change() {
369 let mut state: ValueChangeCallbackState<Arc<str>> = ValueChangeCallbackState::default();
370
371 let changed = value_change_event(&mut state, None);
372 assert_eq!(changed, None);
373
374 let changed = value_change_event(&mut state, Some(Arc::from("beta")));
375 assert_eq!(changed, Some(Some(Arc::from("beta"))));
376
377 let changed = value_change_event(&mut state, Some(Arc::from("beta")));
378 assert_eq!(changed, None);
379
380 let changed = value_change_event(&mut state, Some(Arc::from("alpha")));
381 assert_eq!(changed, Some(Some(Arc::from("alpha"))));
382
383 let changed = value_change_event(&mut state, None);
384 assert_eq!(changed, Some(None));
385 }
386
387 #[test]
388 fn should_clear_query_on_close_emits_only_on_open_to_closed() {
389 let mut state = ClearQueryOnCloseState::default();
390
391 assert_eq!(should_clear_query_on_close(&mut state, false), false);
392 assert_eq!(should_clear_query_on_close(&mut state, true), false);
393 assert_eq!(should_clear_query_on_close(&mut state, true), false);
394 assert_eq!(should_clear_query_on_close(&mut state, false), true);
395 assert_eq!(should_clear_query_on_close(&mut state, false), false);
396 }
397
398 #[test]
399 fn close_auto_focus_decision_maps_reasons() {
400 let policy = ComboboxCloseAutoFocusPolicy {
401 on_item_press: ComboboxCloseAutoFocusDecision::RestoreTrigger,
402 on_escape: ComboboxCloseAutoFocusDecision::RestoreTrigger,
403 on_trigger_press: ComboboxCloseAutoFocusDecision::RestoreTrigger,
404 on_outside_press: ComboboxCloseAutoFocusDecision::PreventDefault,
405 on_focus_out: ComboboxCloseAutoFocusDecision::PreventDefault,
406 on_none: ComboboxCloseAutoFocusDecision::Default,
407 };
408
409 assert_eq!(
410 close_auto_focus_decision_for_reason(policy, ComboboxOpenChangeReason::ItemPress),
411 ComboboxCloseAutoFocusDecision::RestoreTrigger
412 );
413 assert_eq!(
414 close_auto_focus_decision_for_reason(policy, ComboboxOpenChangeReason::EscapeKey),
415 ComboboxCloseAutoFocusDecision::RestoreTrigger
416 );
417 assert_eq!(
418 close_auto_focus_decision_for_reason(policy, ComboboxOpenChangeReason::OutsidePress),
419 ComboboxCloseAutoFocusDecision::PreventDefault
420 );
421 assert_eq!(
422 close_auto_focus_decision_for_reason(policy, ComboboxOpenChangeReason::FocusOut),
423 ComboboxCloseAutoFocusDecision::PreventDefault
424 );
425 assert_eq!(
426 close_auto_focus_decision_for_reason(policy, ComboboxOpenChangeReason::None),
427 ComboboxCloseAutoFocusDecision::Default
428 );
429 }
430}