1use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use fret_core::{AppWindowId, Edges, KeyCode, Modifiers, Point, PointerType, Px, Rect, Size};
21use fret_runtime::{Effect, Model, TimerToken};
22use fret_ui::action::{
23 ActionCx, DismissReason, DismissRequestCx, OnDismissRequest, OnPointerUp,
24 OnPressablePointerDown, OnPressablePointerUp, PointerDownCx, PointerMoveCx, PointerUpCx,
25 PressablePointerDownResult, PressablePointerUpResult, UiActionHost, UiPointerActionHost,
26};
27use fret_ui::element::{
28 AnyElement, Elements, LayoutStyle, PointerRegionProps, PressableA11y, PressableProps,
29 PressableState,
30};
31use fret_ui::elements::GlobalElementId;
32use fret_ui::overlay_placement::Side;
33use fret_ui::{ElementContext, UiHost};
34
35use crate::declarative::ModelWatchExt;
36use crate::headless::roving_focus;
37pub use crate::headless::select_item_aligned::{
38 SELECT_ITEM_ALIGNED_CONTENT_MARGIN, SelectItemAlignedInputs, SelectItemAlignedOutputs,
39 select_item_aligned_position,
40};
41use crate::headless::typeahead;
42use crate::overlay;
43use crate::primitives::dialog;
44use crate::primitives::popper;
45use crate::primitives::popper_arrow;
46use crate::primitives::portal_inherited;
47use crate::primitives::trigger_a11y;
48use crate::{IntoUiElement, OverlayController, OverlayPresence, OverlayRequest, collect_children};
49
50pub fn select_root_name(id: GlobalElementId) -> String {
52 OverlayController::modal_root_name(id)
53}
54
55pub fn select_use_open_model<H: UiHost>(
61 cx: &mut ElementContext<'_, H>,
62 controlled_open: Option<Model<bool>>,
63 default_open: impl FnOnce() -> bool,
64) -> crate::primitives::controllable_state::ControllableModel<bool> {
65 crate::primitives::open_state::open_use_model(cx, controlled_open, default_open)
66}
67
68pub fn select_use_value_model<H: UiHost>(
72 cx: &mut ElementContext<'_, H>,
73 controlled_value: Option<Model<Option<Arc<str>>>>,
74 default_value: impl FnOnce() -> Option<Arc<str>>,
75) -> crate::primitives::controllable_state::ControllableModel<Option<Arc<str>>> {
76 crate::primitives::controllable_state::use_controllable_model(
77 cx,
78 controlled_value,
79 default_value,
80 )
81}
82
83#[derive(Debug, Clone, Default)]
89pub struct SelectRoot {
90 open: Option<Model<bool>>,
91 default_open: bool,
92}
93
94impl SelectRoot {
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn open(mut self, open: Option<Model<bool>>) -> Self {
101 self.open = open;
102 self
103 }
104
105 pub fn default_open(mut self, default_open: bool) -> Self {
107 self.default_open = default_open;
108 self
109 }
110
111 pub fn use_open_model<H: UiHost>(
113 &self,
114 cx: &mut ElementContext<'_, H>,
115 ) -> crate::primitives::controllable_state::ControllableModel<bool> {
116 select_use_open_model(cx, self.open.clone(), || self.default_open)
117 }
118
119 pub fn open_model<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> Model<bool> {
120 self.use_open_model(cx).model()
121 }
122
123 pub fn is_open<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> bool {
124 let open_model = self.open_model(cx);
125 cx.watch_model(&open_model)
126 .layout()
127 .copied()
128 .unwrap_or(false)
129 }
130
131 pub fn modal_request<H: UiHost, I, T>(
132 &self,
133 cx: &mut ElementContext<'_, H>,
134 id: GlobalElementId,
135 trigger: GlobalElementId,
136 presence: OverlayPresence,
137 children: I,
138 ) -> OverlayRequest
139 where
140 I: IntoIterator<Item = T>,
141 T: IntoUiElement<H>,
142 {
143 modal_select_request(
144 id,
145 trigger,
146 self.open_model(cx),
147 presence,
148 collect_children(cx, children),
149 )
150 }
151}
152
153pub fn apply_select_trigger_a11y(
158 trigger: AnyElement,
159 expanded: bool,
160 label: Option<Arc<str>>,
161 listbox_element: Option<GlobalElementId>,
162) -> AnyElement {
163 trigger_a11y::apply_trigger_semantics(
164 trigger,
165 Some(fret_core::SemanticsRole::ComboBox),
166 label,
167 Some(expanded),
168 listbox_element,
169 )
170}
171
172pub fn select_trigger_a11y(
174 label: Option<Arc<str>>,
175 expanded: bool,
176 listbox_element: Option<GlobalElementId>,
177) -> PressableA11y {
178 PressableA11y {
179 role: Some(fret_core::SemanticsRole::ComboBox),
180 label,
181 expanded: Some(expanded),
182 controls_element: listbox_element.map(|id| id.0),
183 ..Default::default()
184 }
185}
186
187fn select_listbox_semantics_id_in_scope<H: UiHost>(
188 cx: &mut ElementContext<'_, H>,
189) -> GlobalElementId {
190 select_listbox_pressable_with_id_props::<H, _, _>(cx, |_cx, _st, _id| {
191 (
192 PressableProps {
193 layout: LayoutStyle::default(),
194 enabled: true,
195 focusable: false,
196 ..Default::default()
197 },
198 Vec::<AnyElement>::new(),
199 )
200 })
201 .id
202}
203
204pub fn select_listbox_semantics_id<H: UiHost>(
213 cx: &mut ElementContext<'_, H>,
214 overlay_root_name: &str,
215) -> GlobalElementId {
216 let inherited = portal_inherited::PortalInherited::capture(cx);
217 portal_inherited::with_root_name_inheriting(cx, overlay_root_name, inherited, |cx| {
218 select_listbox_semantics_id_in_scope::<H>(cx)
219 })
220}
221
222#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
228pub struct SelectInitialFocusTargets {
229 pointer_content_focus: Option<GlobalElementId>,
230 keyboard_entry_focus: Option<GlobalElementId>,
231}
232
233impl SelectInitialFocusTargets {
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 pub fn pointer_content_focus(mut self, focus: Option<GlobalElementId>) -> Self {
239 self.pointer_content_focus = focus;
240 self
241 }
242
243 pub fn keyboard_entry_focus(mut self, focus: Option<GlobalElementId>) -> Self {
244 self.keyboard_entry_focus = focus;
245 self
246 }
247
248 pub fn resolve<H: UiHost>(
249 self,
250 cx: &mut ElementContext<'_, H>,
251 window: AppWindowId,
252 ) -> Option<GlobalElementId> {
253 if fret_ui::input_modality::is_keyboard(cx.app, Some(window)) {
254 self.keyboard_entry_focus.or(self.pointer_content_focus)
255 } else {
256 self.pointer_content_focus
257 }
258 }
259}
260
261pub fn select_listbox_pressable_with_id_props<H: UiHost, I, T>(
266 cx: &mut ElementContext<'_, H>,
267 f: impl FnOnce(&mut ElementContext<'_, H>, PressableState, GlobalElementId) -> (PressableProps, I),
268) -> AnyElement
269where
270 I: IntoIterator<Item = T>,
271 T: IntoUiElement<H>,
272{
273 cx.pressable_with_id_props(move |cx, st, id| {
274 let (props, items) = f(cx, st, id);
275 (props, collect_children(cx, items))
276 })
277}
278
279pub fn is_select_open_key(key: KeyCode) -> bool {
281 matches!(
282 key,
283 KeyCode::Space | KeyCode::Enter | KeyCode::ArrowUp | KeyCode::ArrowDown
284 )
285}
286
287pub fn select_open_key_suppresses_activate(key: KeyCode) -> bool {
289 matches!(key, KeyCode::Space | KeyCode::Enter)
290}
291
292pub const SELECT_TRIGGER_CLICK_SLOP_PX: f32 = 10.0;
296
297pub const SELECT_MOUSE_UP_SELECTED_DELAY_MS: u64 = 400;
302
303pub const SELECT_MOUSE_UP_UNSELECTED_DELAY_MS: u64 = 200;
307
308#[derive(Debug, Clone, Copy, PartialEq)]
315pub struct SelectMousePolicies {
316 pub pointer_up_guard: bool,
319
320 pub mouse_up_selection_gate: Option<SelectMouseUpSelectionGatePolicy>,
323}
324
325impl Default for SelectMousePolicies {
326 fn default() -> Self {
327 Self {
328 pointer_up_guard: true,
329 mouse_up_selection_gate: Some(SelectMouseUpSelectionGatePolicy::default()),
330 }
331 }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq)]
335pub struct SelectMouseUpSelectionGatePolicy {
336 pub selected_delay: Duration,
337 pub unselected_delay_when_has_selected: Duration,
338 pub unselected_delay_when_no_selected: Duration,
339}
340
341impl Default for SelectMouseUpSelectionGatePolicy {
342 fn default() -> Self {
343 Self {
344 selected_delay: Duration::from_millis(SELECT_MOUSE_UP_SELECTED_DELAY_MS),
345 unselected_delay_when_has_selected: Duration::from_millis(
346 SELECT_MOUSE_UP_UNSELECTED_DELAY_MS,
347 ),
348 unselected_delay_when_no_selected: Duration::from_millis(
349 SELECT_MOUSE_UP_SELECTED_DELAY_MS,
350 ),
351 }
352 }
353}
354
355#[derive(Debug, Default)]
362pub struct SelectMouseUpSelectionGateState {
363 allow_selected_mouse_up: bool,
364 allow_unselected_mouse_up: bool,
365 selected_delay_token: Option<TimerToken>,
366 unselected_delay_token: Option<TimerToken>,
367}
368
369impl SelectMouseUpSelectionGateState {
370 fn cancel_timer(host: &mut dyn UiActionHost, token: &mut Option<TimerToken>) {
371 if let Some(token) = token.take() {
372 host.push_effect(Effect::CancelTimer { token });
373 }
374 }
375
376 fn arm_timer(
377 host: &mut dyn UiActionHost,
378 window: AppWindowId,
379 after: Duration,
380 slot: &mut Option<TimerToken>,
381 ) {
382 Self::cancel_timer(host, slot);
383 let token = host.next_timer_token();
384 host.push_effect(Effect::SetTimer {
385 window: Some(window),
386 token,
387 after,
388 repeat: None,
389 });
390 *slot = Some(token);
391 }
392
393 pub fn arm_on_open(
394 &mut self,
395 host: &mut dyn UiActionHost,
396 window: AppWindowId,
397 has_selected_item_in_list: bool,
398 ) {
399 self.arm_on_open_with_policy(
400 host,
401 window,
402 has_selected_item_in_list,
403 SelectMouseUpSelectionGatePolicy::default(),
404 );
405 }
406
407 pub fn arm_on_open_with_policy(
408 &mut self,
409 host: &mut dyn UiActionHost,
410 window: AppWindowId,
411 has_selected_item_in_list: bool,
412 policy: SelectMouseUpSelectionGatePolicy,
413 ) {
414 self.allow_selected_mouse_up = false;
415 self.allow_unselected_mouse_up = false;
416
417 let unselected_delay = if has_selected_item_in_list {
418 policy.unselected_delay_when_has_selected
419 } else {
420 policy.unselected_delay_when_no_selected
421 };
422
423 Self::arm_timer(
424 host,
425 window,
426 unselected_delay,
427 &mut self.unselected_delay_token,
428 );
429 Self::arm_timer(
430 host,
431 window,
432 policy.selected_delay,
433 &mut self.selected_delay_token,
434 );
435 }
436
437 pub fn reset_without_cancel(&mut self) {
438 self.allow_selected_mouse_up = false;
439 self.allow_unselected_mouse_up = false;
440 self.selected_delay_token = None;
441 self.unselected_delay_token = None;
442 }
443
444 pub fn clear_and_cancel(&mut self, host: &mut dyn UiActionHost) {
445 Self::cancel_timer(host, &mut self.selected_delay_token);
446 Self::cancel_timer(host, &mut self.unselected_delay_token);
447 self.allow_selected_mouse_up = false;
448 self.allow_unselected_mouse_up = false;
449 }
450
451 pub fn on_timer(&mut self, token: TimerToken) -> bool {
452 let mut handled = false;
453 if self.unselected_delay_token == Some(token) {
454 self.unselected_delay_token = None;
455 self.allow_unselected_mouse_up = true;
456 handled = true;
457 }
458 if self.selected_delay_token == Some(token) {
459 self.selected_delay_token = None;
460 self.allow_selected_mouse_up = true;
461 handled = true;
462 }
463 handled
464 }
465
466 pub fn should_allow_item_mouse_up(&self, item_is_selected: bool) -> bool {
467 if item_is_selected {
468 self.allow_selected_mouse_up
469 } else {
470 self.allow_unselected_mouse_up
471 }
472 }
473}
474
475pub type SelectMouseOpenGuard = Arc<Mutex<SelectMouseOpenGuardState>>;
476
477pub fn select_mouse_open_guard() -> SelectMouseOpenGuard {
478 Arc::new(Mutex::new(SelectMouseOpenGuardState::default()))
479}
480
481pub fn select_mouse_open_guard_clear(guard: &SelectMouseOpenGuard) {
482 let mut guard = guard.lock().unwrap_or_else(|e| e.into_inner());
483 guard.clear();
484}
485
486pub fn select_mouse_open_guard_record_if_opened(
487 guard: &SelectMouseOpenGuard,
488 was_open: bool,
489 now_open: bool,
490 down_pos: Point,
491) {
492 let mut guard = guard.lock().unwrap_or_else(|e| e.into_inner());
493 guard.record_if_opened(was_open, now_open, down_pos);
494}
495
496pub fn select_mouse_open_is_within_click_slop(down: Point, up: Point) -> bool {
501 let dx = (down.x.0 - up.x.0).abs();
502 let dy = (down.y.0 - up.y.0).abs();
503 dx <= SELECT_TRIGGER_CLICK_SLOP_PX && dy <= SELECT_TRIGGER_CLICK_SLOP_PX
504}
505
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub enum SelectMouseOpenGuardPointerUpDecision {
508 NoGuard,
509 Suppress,
510 Allow,
511}
512
513pub fn select_mouse_open_guard_pointer_up_decision(
514 guard: &mut SelectMouseOpenGuardState,
515 up: PointerUpCx,
516) -> SelectMouseOpenGuardPointerUpDecision {
517 if let (Some(last_tick), Some(decision)) = (
518 guard.last_pointer_up_tick_id,
519 guard.last_pointer_up_decision,
520 ) {
521 if up.tick_id == last_tick || up.tick_id.0 == last_tick.0 + 1 {
525 return decision;
526 }
527 }
528
529 if up.button != fret_core::MouseButton::Left {
530 return SelectMouseOpenGuardPointerUpDecision::NoGuard;
531 }
532 if !matches!(up.pointer_type, PointerType::Mouse | PointerType::Unknown) {
533 return SelectMouseOpenGuardPointerUpDecision::NoGuard;
534 }
535
536 let decision = if let Some(down) = guard.take() {
537 let up_pos = up.position_window.unwrap_or(up.position);
538 if select_mouse_open_is_within_click_slop(down, up_pos) {
539 SelectMouseOpenGuardPointerUpDecision::Suppress
540 } else {
541 SelectMouseOpenGuardPointerUpDecision::Allow
542 }
543 } else {
544 SelectMouseOpenGuardPointerUpDecision::NoGuard
545 };
546
547 guard.last_pointer_up_tick_id = Some(up.tick_id);
548 guard.last_pointer_up_decision = Some(decision);
549
550 decision
551}
552
553pub fn select_mouse_open_guard_pointer_up_decision_shared(
554 guard: &SelectMouseOpenGuard,
555 up: PointerUpCx,
556) -> SelectMouseOpenGuardPointerUpDecision {
557 let mut guard = guard.lock().unwrap_or_else(|e| e.into_inner());
558 select_mouse_open_guard_pointer_up_decision(&mut guard, up)
559}
560
561pub fn select_mouse_open_guard_should_suppress_pointer_up(
567 guard: &mut SelectMouseOpenGuardState,
568 up: PointerUpCx,
569) -> bool {
570 select_mouse_open_guard_pointer_up_decision(guard, up)
571 == SelectMouseOpenGuardPointerUpDecision::Suppress
572}
573
574pub fn select_mouse_open_guard_should_suppress_pointer_up_shared(
575 guard: &SelectMouseOpenGuard,
576 up: PointerUpCx,
577) -> bool {
578 select_mouse_open_guard_pointer_up_decision_shared(guard, up)
579 == SelectMouseOpenGuardPointerUpDecision::Suppress
580}
581
582#[derive(Debug, Clone, Copy, PartialEq)]
583pub struct SelectItemAlignedLayout {
584 pub outputs: SelectItemAlignedOutputs,
585 pub rect: Rect,
586 pub side: Side,
587}
588
589pub fn select_item_aligned_layout(inputs: SelectItemAlignedInputs) -> SelectItemAlignedLayout {
595 let outputs = select_item_aligned_position(inputs);
596
597 let margin = SELECT_ITEM_ALIGNED_CONTENT_MARGIN;
598 let window_left = inputs.window.origin.x;
599 let window_top = inputs.window.origin.y;
600 let window_right = Px(window_left.0 + inputs.window.size.width.0);
601 let window_bottom = Px(window_top.0 + inputs.window.size.height.0);
602
603 let clamp_y = |y: Px| {
604 let min_y = Px(window_top.0 + margin.0);
605 let max_y = Px((window_bottom.0 - margin.0 - outputs.height.0).max(min_y.0));
606 Px(y.0.clamp(min_y.0, max_y.0))
607 };
608
609 let trigger_mid_y = Px(inputs.trigger.origin.y.0 + inputs.trigger.size.height.0 / 2.0);
610
611 let x = if let Some(left) = outputs.left {
612 left
613 } else if let Some(right) = outputs.right {
614 Px(window_right.0 - right.0 - outputs.width.0)
615 } else {
616 Px(window_left.0 + margin.0)
617 };
618
619 let y = if outputs.top.is_some() {
620 Px(window_top.0 + margin.0)
621 } else if outputs.bottom.is_some() {
622 let selected_item_half_h = Px(inputs.selected_item.size.height.0 / 2.0);
627 let selected_item_mid_offset = Px((inputs.selected_item.origin.y.0
630 - inputs.viewport.origin.y.0)
631 + selected_item_half_h.0);
632 let content_top_to_item_mid = Px(inputs.content_border_top.0
633 + inputs.content_padding_top.0
634 + selected_item_mid_offset.0);
635 clamp_y(Px(trigger_mid_y.0 - content_top_to_item_mid.0))
636 } else {
637 Px(window_top.0 + margin.0)
638 };
639
640 let side = if outputs.bottom.is_some() {
641 Side::Bottom
642 } else {
643 Side::Top
644 };
645
646 SelectItemAlignedLayout {
647 outputs,
648 rect: Rect::new(Point::new(x, y), Size::new(outputs.width, outputs.height)),
649 side,
650 }
651}
652
653#[derive(Debug, Clone, Copy, PartialEq)]
654pub struct SelectItemAlignedElementInputs {
655 pub direction: popper::LayoutDirection,
656 pub window: Rect,
657 pub trigger: Rect,
658
659 pub content_min_width: Px,
661
662 pub content_border_top: Px,
663 pub content_padding_top: Px,
664 pub content_border_bottom: Px,
665 pub content_padding_bottom: Px,
666
667 pub viewport_padding_top: Px,
668 pub viewport_padding_bottom: Px,
669
670 pub selected_item_is_first: bool,
671 pub selected_item_is_last: bool,
672
673 pub value_node: GlobalElementId,
674 pub viewport: GlobalElementId,
675 pub listbox: GlobalElementId,
676 pub content_panel: GlobalElementId,
677 pub scroll_max_offset_y: Option<Px>,
682 pub content_width_probe: Option<GlobalElementId>,
688 pub selected_item: GlobalElementId,
689 pub selected_item_text: GlobalElementId,
690}
691
692pub fn select_item_aligned_layout_from_elements<H: UiHost>(
693 cx: &ElementContext<'_, H>,
694 inputs: SelectItemAlignedElementInputs,
695) -> Option<SelectItemAlignedLayout> {
696 let value_node = overlay::anchor_bounds_for_element(cx, inputs.value_node)?;
697 let viewport = cx
704 .last_bounds_for_element(inputs.viewport)
705 .or_else(|| overlay::anchor_bounds_for_element(cx, inputs.viewport))?;
706 let listbox = cx
711 .last_bounds_for_element(inputs.listbox)
712 .or_else(|| overlay::anchor_bounds_for_element(cx, inputs.listbox))?;
713 let mut content = cx
714 .last_bounds_for_element(inputs.content_panel)
715 .or_else(|| overlay::anchor_bounds_for_element(cx, inputs.content_panel))?;
716 content.size.width = Px(content.size.width.0.max(inputs.content_min_width.0));
717 let selected_item = cx
718 .last_bounds_for_element(inputs.selected_item)
719 .or_else(|| overlay::anchor_bounds_for_element(cx, inputs.selected_item))?;
720 let selected_item_text = cx
721 .last_bounds_for_element(inputs.selected_item_text)
722 .or_else(|| overlay::anchor_bounds_for_element(cx, inputs.selected_item_text))?;
723 let items_height = if let Some(max_y) = inputs.scroll_max_offset_y {
729 Px((viewport.size.height.0 + max_y.0).max(0.0))
730 } else {
731 listbox.size.height
734 };
735
736 if let Some(probe_id) = inputs.content_width_probe
737 && let Some(probe) = cx.last_bounds_for_element(probe_id)
738 && probe.size.width.0.is_finite()
739 && probe.size.width.0 > 0.0
740 {
741 let border_extra = Px(inputs.content_border_top.0 * 2.0);
747 let probed_width = Px(probe.size.width.0 + border_extra.0);
748 content.size.width = Px(content.size.width.0.max(probed_width.0));
749 }
750
751 Some(select_item_aligned_layout(SelectItemAlignedInputs {
752 direction: inputs.direction,
753 window: inputs.window,
754 trigger: inputs.trigger,
755 content,
756 value_node,
757 selected_item_text,
758 selected_item,
759 viewport,
760 content_border_top: inputs.content_border_top,
761 content_padding_top: inputs.content_padding_top,
762 content_border_bottom: inputs.content_border_bottom,
763 content_padding_bottom: inputs.content_padding_bottom,
764 viewport_padding_top: inputs.viewport_padding_top,
765 viewport_padding_bottom: inputs.viewport_padding_bottom,
766 selected_item_is_first: inputs.selected_item_is_first,
767 selected_item_is_last: inputs.selected_item_is_last,
768 items_height,
769 }))
770}
771
772#[derive(Debug, Clone, Copy, PartialEq)]
773pub struct SelectResolvedContentPlacement {
774 pub placement: SelectContentPlacement,
775 pub item_aligned_layout: Option<SelectItemAlignedLayout>,
776}
777
778pub fn select_resolve_content_placement(
779 anchor: Rect,
780 outer: Rect,
781 desired: Size,
782 popper_placement: popper::PopperContentPlacement,
783 arrow_size: Option<Px>,
784 item_aligned_layout: Option<SelectItemAlignedLayout>,
785) -> SelectResolvedContentPlacement {
786 if let Some(item_aligned_layout) = item_aligned_layout {
787 return SelectResolvedContentPlacement {
788 placement: select_content_placement_item_aligned(anchor, item_aligned_layout),
789 item_aligned_layout: Some(item_aligned_layout),
790 };
791 }
792
793 SelectResolvedContentPlacement {
794 placement: select_content_placement_popper(
795 outer,
796 anchor,
797 desired,
798 popper_placement,
799 arrow_size,
800 ),
801 item_aligned_layout: None,
802 }
803}
804
805pub fn select_resolve_content_placement_from_elements<H: UiHost>(
806 cx: &ElementContext<'_, H>,
807 anchor: Rect,
808 outer: Rect,
809 desired: Size,
810 popper_placement: popper::PopperContentPlacement,
811 arrow_size: Option<Px>,
812 item_aligned: Option<SelectItemAlignedElementInputs>,
813) -> SelectResolvedContentPlacement {
814 let item_aligned_layout =
815 item_aligned.and_then(|inputs| select_item_aligned_layout_from_elements(cx, inputs));
816 select_resolve_content_placement(
817 anchor,
818 outer,
819 desired,
820 popper_placement,
821 arrow_size,
822 item_aligned_layout,
823 )
824}
825
826#[derive(Debug, Clone, Copy, PartialEq)]
827pub struct SelectContentPlacement {
828 pub placed: Rect,
829 pub wrapper_insets: Edges,
830 pub side: Side,
831 pub transform_origin: Point,
832 pub popper_layout: Option<fret_ui::overlay_placement::AnchoredPanelLayout>,
833}
834
835pub fn select_content_placement_item_aligned(
836 anchor: Rect,
837 layout: SelectItemAlignedLayout,
838) -> SelectContentPlacement {
839 let pseudo_layout = fret_ui::overlay_placement::AnchoredPanelLayout {
840 rect: layout.rect,
841 side: layout.side,
842 align: popper::Align::Center,
843 arrow: None,
844 };
845
846 SelectContentPlacement {
847 placed: layout.rect,
848 wrapper_insets: Edges::all(Px(0.0)),
849 side: layout.side,
850 transform_origin: popper::popper_content_transform_origin(&pseudo_layout, anchor, None),
851 popper_layout: None,
852 }
853}
854
855pub fn select_content_placement_popper(
856 outer: Rect,
857 anchor: Rect,
858 desired: Size,
859 placement: popper::PopperContentPlacement,
860 arrow_size: Option<Px>,
861) -> SelectContentPlacement {
862 let layout = popper::popper_content_layout_unclamped(outer, anchor, desired, placement);
869 let wrapper_insets = popper_arrow::wrapper_insets(&layout, placement.arrow_protrusion);
870 let transform_origin = popper::popper_content_transform_origin(&layout, anchor, arrow_size);
871
872 SelectContentPlacement {
873 placed: layout.rect,
874 wrapper_insets,
875 side: layout.side,
876 transform_origin,
877 popper_layout: Some(layout),
878 }
879}
880
881#[derive(Debug, Clone, Copy, PartialEq)]
882pub struct SelectPopperVars {
883 pub available_width: Px,
884 pub available_height: Px,
885 pub trigger_width: Px,
886 pub trigger_height: Px,
887}
888
889pub fn select_popper_desired_width(outer: Rect, anchor: Rect, min_width: Px) -> Px {
890 popper::popper_desired_width(outer, anchor, min_width)
891}
892
893pub fn select_popper_vars(
904 outer: Rect,
905 anchor: Rect,
906 min_width: Px,
907 placement: popper::PopperContentPlacement,
908) -> SelectPopperVars {
909 let metrics =
910 popper::popper_available_metrics_for_placement(outer, anchor, min_width, placement);
911 SelectPopperVars {
912 available_width: metrics.available_width,
913 available_height: metrics.available_height,
914 trigger_width: metrics.anchor_width,
915 trigger_height: metrics.anchor_height,
916 }
917}
918
919pub fn select_popper_available_height(
925 outer: Rect,
926 anchor: Rect,
927 min_width: Px,
928 placement: popper::PopperContentPlacement,
929) -> Px {
930 select_popper_vars(outer, anchor, min_width, placement).available_height
931}
932
933pub const SELECT_TYPEAHEAD_CLEAR_TIMEOUT_MS: u64 = 1000;
937
938#[derive(Debug, Default)]
940pub struct TimedTypeaheadState {
941 query: String,
942 clear_token: Option<TimerToken>,
943}
944
945impl TimedTypeaheadState {
946 pub fn query(&self) -> &str {
947 self.query.as_str()
948 }
949
950 pub fn clear_and_cancel(&mut self, host: &mut dyn UiActionHost) {
951 if let Some(token) = self.clear_token.take() {
952 host.push_effect(Effect::CancelTimer { token });
953 }
954 self.query.clear();
955 }
956
957 pub fn on_timer(&mut self, token: TimerToken) -> bool {
958 if self.clear_token == Some(token) {
959 self.clear_token = None;
960 self.query.clear();
961 return true;
962 }
963 false
964 }
965
966 pub fn push_key_and_arm_timer(
967 &mut self,
968 host: &mut dyn UiActionHost,
969 window: AppWindowId,
970 key: KeyCode,
971 timeout: Duration,
972 ) -> Option<char> {
973 let ch = fret_core::keycode_to_ascii_lowercase(key)?;
974 self.query.push(ch);
975 if let Some(token) = self.clear_token.take() {
976 host.push_effect(Effect::CancelTimer { token });
977 }
978 let token = host.next_timer_token();
979 self.clear_token = Some(token);
980 host.push_effect(Effect::SetTimer {
981 window: Some(window),
982 token,
983 after: timeout,
984 repeat: None,
985 });
986 Some(ch)
987 }
988}
989
990#[derive(Debug, Default)]
996pub struct SelectTriggerKeyState {
997 suppress_next_activate: bool,
998 typeahead: TimedTypeaheadState,
999}
1000
1001impl SelectTriggerKeyState {
1002 pub fn take_suppress_next_activate(&mut self) -> bool {
1003 let v = self.suppress_next_activate;
1004 self.suppress_next_activate = false;
1005 v
1006 }
1007
1008 pub fn clear_typeahead(&mut self, host: &mut dyn UiActionHost) {
1009 self.typeahead.clear_and_cancel(host);
1010 }
1011
1012 pub fn reset_typeahead_buffer(&mut self) {
1013 self.typeahead.query.clear();
1014 self.typeahead.clear_token = None;
1015 }
1016
1017 pub fn typeahead_query(&self) -> &str {
1018 self.typeahead.query()
1019 }
1020
1021 pub fn push_typeahead_key_and_arm_timer(
1022 &mut self,
1023 host: &mut dyn UiActionHost,
1024 window: AppWindowId,
1025 key: KeyCode,
1026 ) -> Option<char> {
1027 let timeout = Duration::from_millis(SELECT_TYPEAHEAD_CLEAR_TIMEOUT_MS);
1028 self.typeahead
1029 .push_key_and_arm_timer(host, window, key, timeout)
1030 }
1031
1032 pub fn on_timer(&mut self, token: TimerToken) -> bool {
1033 self.typeahead.on_timer(token)
1034 }
1035
1036 pub fn handle_key_down_when_closed(
1037 &mut self,
1038 host: &mut dyn UiActionHost,
1039 window: AppWindowId,
1040 open: &Model<bool>,
1041 value: &Model<Option<Arc<str>>>,
1042 values: &[Arc<str>],
1043 labels: &[Arc<str>],
1044 disabled: &[bool],
1045 key: KeyCode,
1046 modifiers: Modifiers,
1047 repeat: bool,
1048 ) -> bool {
1049 if repeat {
1050 return false;
1051 }
1052
1053 let is_open = host.models_mut().get_copied(open).unwrap_or(false);
1054 if is_open {
1055 return false;
1056 }
1057
1058 let is_modifier_key = modifiers.ctrl || modifiers.alt || modifiers.meta || modifiers.alt_gr;
1059 if is_modifier_key {
1060 return false;
1061 }
1062
1063 if key == KeyCode::Space && !self.typeahead.query().is_empty() {
1064 return true;
1065 }
1066
1067 if is_select_open_key(key) {
1068 if select_open_key_suppresses_activate(key) {
1069 self.suppress_next_activate = true;
1070 }
1071 self.typeahead.clear_and_cancel(host);
1072 let _ = host.models_mut().update(open, |v| *v = true);
1073 host.request_redraw(window);
1074 return true;
1075 }
1076
1077 let timeout = Duration::from_millis(SELECT_TYPEAHEAD_CLEAR_TIMEOUT_MS);
1078 let Some(_ch) = self
1079 .typeahead
1080 .push_key_and_arm_timer(host, window, key, timeout)
1081 else {
1082 return false;
1083 };
1084
1085 let current = host.models_mut().read(value, |v| v.clone()).ok().flatten();
1086 let current_idx = current
1087 .as_ref()
1088 .and_then(|v| values.iter().position(|it| it.as_ref() == v.as_ref()));
1089
1090 if let Some(next) = typeahead::match_prefix_arc_str(
1091 labels,
1092 disabled,
1093 self.typeahead.query(),
1094 current_idx,
1095 true,
1096 ) && let Some(next_value) = values.get(next).cloned()
1097 {
1098 let _ = host.models_mut().update(value, |v| *v = Some(next_value));
1099 host.request_redraw(window);
1100 }
1101
1102 true
1103 }
1104}
1105
1106#[derive(Debug, Default)]
1111pub struct SelectMouseOpenGuardState {
1112 mouse_open_down_pos: Option<Point>,
1113 last_pointer_up_tick_id: Option<fret_runtime::TickId>,
1114 last_pointer_up_decision: Option<SelectMouseOpenGuardPointerUpDecision>,
1115}
1116
1117impl SelectMouseOpenGuardState {
1118 pub fn clear(&mut self) {
1119 self.mouse_open_down_pos = None;
1120 self.last_pointer_up_tick_id = None;
1121 self.last_pointer_up_decision = None;
1122 }
1123
1124 pub fn record_if_opened(&mut self, was_open: bool, now_open: bool, down_pos: Point) {
1125 if !was_open && now_open {
1126 self.mouse_open_down_pos = Some(down_pos);
1127 } else {
1128 self.mouse_open_down_pos = None;
1129 }
1130 self.last_pointer_up_tick_id = None;
1131 self.last_pointer_up_decision = None;
1132 }
1133
1134 pub fn take(&mut self) -> Option<Point> {
1135 self.mouse_open_down_pos.take()
1136 }
1137}
1138
1139#[derive(Debug, Default)]
1144pub struct SelectTriggerPointerState {
1145 down_pos: Option<Point>,
1146 moved: bool,
1147 captured: bool,
1148}
1149
1150impl SelectTriggerPointerState {
1151 fn reset(&mut self) {
1152 self.down_pos = None;
1153 self.moved = false;
1154 self.captured = false;
1155 }
1156
1157 fn moved_beyond_slop(&self, current: Point) -> bool {
1158 let Some(down) = self.down_pos else {
1159 return false;
1160 };
1161 (down.x.0 - current.x.0).abs() > SELECT_TRIGGER_CLICK_SLOP_PX
1162 || (down.y.0 - current.y.0).abs() > SELECT_TRIGGER_CLICK_SLOP_PX
1163 }
1164
1165 pub fn handle_pointer_down(
1166 &mut self,
1167 host: &mut dyn UiPointerActionHost,
1168 action_cx: ActionCx,
1169 down: PointerDownCx,
1170 open: &Model<bool>,
1171 enabled: bool,
1172 ) -> bool {
1173 if !enabled {
1174 return false;
1175 }
1176 if down.button != fret_core::MouseButton::Left {
1177 return false;
1178 }
1179
1180 let is_macos_ctrl_click = cfg!(target_os = "macos")
1181 && down.modifiers.ctrl
1182 && down.pointer_type == PointerType::Mouse;
1183 if is_macos_ctrl_click {
1184 return false;
1185 }
1186
1187 match down.pointer_type {
1188 PointerType::Mouse | PointerType::Unknown => {
1189 let was_open = host.models_mut().get_copied(open).unwrap_or(false);
1190 if was_open {
1191 let _ = host.models_mut().update(open, |v| *v = false);
1192 host.request_focus(action_cx.target);
1193 host.request_redraw(action_cx.window);
1194 return true;
1195 }
1196
1197 let _ = host.models_mut().update(open, |v| *v = true);
1198 host.request_redraw(action_cx.window);
1199 host.prevent_default(fret_runtime::DefaultAction::FocusOnPointerDown);
1200 true
1201 }
1202 PointerType::Touch | PointerType::Pen => {
1203 self.down_pos = Some(down.position);
1204 self.moved = false;
1205 self.captured = true;
1206 host.capture_pointer();
1207 true
1208 }
1209 }
1210 }
1211
1212 pub fn handle_pointer_move(
1213 &mut self,
1214 _host: &mut dyn UiPointerActionHost,
1215 _action_cx: ActionCx,
1216 mv: PointerMoveCx,
1217 ) -> bool {
1218 if !self.captured {
1219 return false;
1220 }
1221 if !self.moved && self.moved_beyond_slop(mv.position) {
1222 self.moved = true;
1223 }
1224 true
1225 }
1226
1227 pub fn handle_pointer_up(
1228 &mut self,
1229 host: &mut dyn UiPointerActionHost,
1230 action_cx: ActionCx,
1231 up: PointerUpCx,
1232 open: &Model<bool>,
1233 enabled: bool,
1234 ) -> bool {
1235 if !enabled {
1236 self.reset();
1237 return false;
1238 }
1239 if up.button != fret_core::MouseButton::Left {
1240 self.reset();
1241 return false;
1242 }
1243 if !self.captured {
1244 self.reset();
1245 return false;
1246 }
1247
1248 host.release_pointer_capture();
1249 self.captured = false;
1250
1251 let should_open = !self.moved
1252 && self.down_pos.is_some()
1253 && !self.moved_beyond_slop(up.position)
1254 && host.bounds().contains(up.position);
1255
1256 self.reset();
1257
1258 if should_open {
1259 let _ = host.models_mut().update(open, |v| *v = true);
1260 host.request_redraw(action_cx.window);
1261 }
1262 true
1263 }
1264}
1265
1266#[derive(Debug, Default)]
1275pub struct SelectContentKeyState {
1276 active_row: Option<usize>,
1277 typeahead: TimedTypeaheadState,
1278}
1279
1280impl SelectContentKeyState {
1281 pub fn active_row(&self) -> Option<usize> {
1282 self.active_row
1283 }
1284
1285 pub fn set_active_row(&mut self, row: Option<usize>) {
1286 self.active_row = row;
1287 }
1288
1289 pub fn reset_on_open(&mut self, initial_active_row: Option<usize>) {
1290 self.active_row = initial_active_row;
1291 self.typeahead.query.clear();
1292 self.typeahead.clear_token = None;
1293 }
1294
1295 pub fn clear_typeahead(&mut self, host: &mut dyn UiActionHost) {
1296 self.typeahead.clear_and_cancel(host);
1297 }
1298
1299 pub fn on_timer(&mut self, token: TimerToken) -> bool {
1300 self.typeahead.on_timer(token)
1301 }
1302
1303 pub fn handle_key_down_when_open(
1304 &mut self,
1305 host: &mut dyn UiActionHost,
1306 action_cx: ActionCx,
1307 open: &Model<bool>,
1308 value: &Model<Option<Arc<str>>>,
1309 values_by_row: &[Option<Arc<str>>],
1310 labels_by_row: &[Arc<str>],
1311 disabled_by_row: &[bool],
1312 on_value_change: Option<&Arc<dyn Fn(&mut dyn UiActionHost, ActionCx, Arc<str>) + 'static>>,
1313 key: KeyCode,
1314 repeat: bool,
1315 loop_navigation: bool,
1316 ) -> bool {
1317 if repeat {
1318 return false;
1319 }
1320
1321 let window = action_cx.window;
1322 let is_open = host.models_mut().get_copied(open).unwrap_or(false);
1323 if !is_open {
1324 return false;
1325 }
1326
1327 if key == KeyCode::Space && !self.typeahead.query().is_empty() {
1328 return true;
1329 }
1330
1331 let current = self
1332 .active_row
1333 .or_else(|| roving_focus::first_enabled(disabled_by_row));
1334
1335 match key {
1336 KeyCode::Tab => true,
1337 KeyCode::Escape => {
1338 let _ = host.models_mut().update(open, |v| *v = false);
1339 host.request_redraw(window);
1340 true
1341 }
1342 KeyCode::Home => {
1343 self.active_row = roving_focus::first_enabled(disabled_by_row);
1344 host.request_redraw(window);
1345 true
1346 }
1347 KeyCode::End => {
1348 self.active_row = roving_focus::last_enabled(disabled_by_row);
1349 host.request_redraw(window);
1350 true
1351 }
1352 KeyCode::ArrowDown | KeyCode::ArrowUp => {
1353 let Some(current) = current else {
1354 return true;
1355 };
1356 let forward = key == KeyCode::ArrowDown;
1357 self.active_row =
1358 roving_focus::next_enabled(disabled_by_row, current, forward, loop_navigation)
1359 .or(Some(current));
1360 host.request_redraw(window);
1361 true
1362 }
1363 KeyCode::Enter | KeyCode::Space => {
1364 let Some(active_row) = current else {
1365 return true;
1366 };
1367 let is_disabled = disabled_by_row.get(active_row).copied().unwrap_or(true);
1368 if is_disabled {
1369 return true;
1370 }
1371 if let Some(chosen_value) = values_by_row.get(active_row).cloned().flatten() {
1372 let before = host.models_mut().read(value, |v| v.clone()).ok().flatten();
1373 let did_change = before.as_deref() != Some(chosen_value.as_ref());
1374 if did_change {
1375 let _ = host
1376 .models_mut()
1377 .update(value, |v| *v = Some(chosen_value.clone()));
1378 if let Some(on_value_change) = on_value_change {
1379 on_value_change(host, action_cx, chosen_value.clone());
1380 }
1381 }
1382 let _ = host.models_mut().update(open, |v| *v = false);
1383 host.request_redraw(window);
1384 }
1385 true
1386 }
1387 _ => {
1388 let timeout = Duration::from_millis(SELECT_TYPEAHEAD_CLEAR_TIMEOUT_MS);
1389 let Some(_ch) = self
1390 .typeahead
1391 .push_key_and_arm_timer(host, window, key, timeout)
1392 else {
1393 return false;
1394 };
1395
1396 let next = typeahead::match_prefix_arc_str(
1397 labels_by_row,
1398 disabled_by_row,
1399 self.typeahead.query(),
1400 current,
1401 true,
1402 );
1403 if next != self.active_row {
1404 self.active_row = next;
1405 host.request_redraw(window);
1406 }
1407 true
1408 }
1409 }
1410 }
1411}
1412
1413pub fn select_modal_barrier_layout() -> LayoutStyle {
1417 dialog::modal_barrier_layout()
1418}
1419
1420pub fn select_modal_barrier<H: UiHost, I, T>(
1425 cx: &mut ElementContext<'_, H>,
1426 open: Model<bool>,
1427 dismiss_on_press: bool,
1428 children: I,
1429) -> AnyElement
1430where
1431 I: IntoIterator<Item = T>,
1432 T: IntoUiElement<H>,
1433{
1434 select_modal_barrier_with_dismiss_handler(cx, open, dismiss_on_press, None, children)
1435}
1436
1437pub fn select_modal_barrier_with_dismiss_handler<H: UiHost, I, T>(
1443 cx: &mut ElementContext<'_, H>,
1444 open: Model<bool>,
1445 dismiss_on_press: bool,
1446 on_dismiss_request: Option<OnDismissRequest>,
1447 children: I,
1448) -> AnyElement
1449where
1450 I: IntoIterator<Item = T>,
1451 T: IntoUiElement<H>,
1452{
1453 dialog::modal_barrier_with_dismiss_handler(
1454 cx,
1455 open,
1456 dismiss_on_press,
1457 on_dismiss_request,
1458 children,
1459 )
1460}
1461
1462pub fn select_modal_layer_elements<H: UiHost, I, T>(
1465 cx: &mut ElementContext<'_, H>,
1466 open: Model<bool>,
1467 dismiss_on_press: bool,
1468 barrier_children: I,
1469 content: AnyElement,
1470) -> Elements
1471where
1472 I: IntoIterator<Item = T>,
1473 T: IntoUiElement<H>,
1474{
1475 Elements::from([
1476 select_modal_barrier(cx, open, dismiss_on_press, barrier_children),
1477 content,
1478 ])
1479}
1480
1481pub fn select_modal_layer_elements_with_dismiss_handler<H: UiHost, I, T>(
1484 cx: &mut ElementContext<'_, H>,
1485 open: Model<bool>,
1486 dismiss_on_press: bool,
1487 on_dismiss_request: Option<OnDismissRequest>,
1488 barrier_children: I,
1489 content: AnyElement,
1490) -> Elements
1491where
1492 I: IntoIterator<Item = T>,
1493 T: IntoUiElement<H>,
1494{
1495 Elements::from([
1496 select_modal_barrier_with_dismiss_handler(
1497 cx,
1498 open,
1499 dismiss_on_press,
1500 on_dismiss_request,
1501 barrier_children,
1502 ),
1503 content,
1504 ])
1505}
1506
1507pub fn select_modal_barrier_pointer_up_guard<H: UiHost>(
1513 cx: &mut ElementContext<'_, H>,
1514 _open: Model<bool>,
1515 guard: SelectMouseOpenGuard,
1516) -> AnyElement {
1517 let down = guard
1518 .lock()
1519 .unwrap_or_else(|e| e.into_inner())
1520 .mouse_open_down_pos;
1521 let enabled = down.is_some();
1522 let layout = if let Some(down) = down {
1523 let slop = SELECT_TRIGGER_CLICK_SLOP_PX;
1526 let size = Px(slop * 2.0);
1527 let left = Px((down.x.0 - slop).max(0.0));
1528 let top = Px((down.y.0 - slop).max(0.0));
1529
1530 let mut layout = LayoutStyle::default();
1531 layout.position = fret_ui::element::PositionStyle::Absolute;
1532 layout.inset = fret_ui::element::InsetStyle {
1533 left: Some(left).into(),
1534 right: None.into(),
1535 top: Some(top).into(),
1536 bottom: None.into(),
1537 };
1538 layout.size.width = fret_ui::element::Length::Px(size);
1539 layout.size.height = fret_ui::element::Length::Px(size);
1540 layout
1541 } else {
1542 select_modal_barrier_layout()
1543 };
1544 cx.pointer_region(
1545 PointerRegionProps {
1546 layout,
1547 enabled,
1548 capture_phase_pointer_moves: false,
1549 },
1550 move |cx| {
1551 let guard_for_pointer_up = guard.clone();
1552 cx.pointer_region_on_pointer_up(Arc::new(move |_host, _action_cx, up: PointerUpCx| {
1553 match select_mouse_open_guard_pointer_up_decision_shared(&guard_for_pointer_up, up)
1554 {
1555 SelectMouseOpenGuardPointerUpDecision::NoGuard => false,
1556 SelectMouseOpenGuardPointerUpDecision::Suppress => true,
1557 SelectMouseOpenGuardPointerUpDecision::Allow => false,
1560 }
1561 }));
1562 Vec::new()
1563 },
1564 )
1565}
1566
1567pub fn select_modal_layer_elements_with_pointer_up_guard<H: UiHost, I, T>(
1570 cx: &mut ElementContext<'_, H>,
1571 open: Model<bool>,
1572 dismiss_on_press: bool,
1573 guard: SelectMouseOpenGuard,
1574 barrier_children: I,
1575 content: AnyElement,
1576) -> Elements
1577where
1578 I: IntoIterator<Item = T>,
1579 T: IntoUiElement<H>,
1580{
1581 let guard_el = select_modal_barrier_pointer_up_guard(cx, open.clone(), guard);
1582 Elements::from([
1583 select_modal_barrier(cx, open, dismiss_on_press, barrier_children),
1584 content,
1585 guard_el,
1586 ])
1587}
1588
1589pub fn select_modal_layer_elements_with_pointer_up_guard_and_dismiss_handler<H: UiHost, I, T>(
1593 cx: &mut ElementContext<'_, H>,
1594 open: Model<bool>,
1595 dismiss_on_press: bool,
1596 on_dismiss_request: Option<OnDismissRequest>,
1597 guard: SelectMouseOpenGuard,
1598 barrier_children: I,
1599 content: AnyElement,
1600) -> Elements
1601where
1602 I: IntoIterator<Item = T>,
1603 T: IntoUiElement<H>,
1604{
1605 let guard_el = select_modal_barrier_pointer_up_guard(cx, open.clone(), guard.clone());
1606 let barrier_children = collect_children(cx, barrier_children);
1607 let barrier = if dismiss_on_press {
1608 let open_for_pressable = open.clone();
1609 let guard_for_pressable = guard;
1610 let on_dismiss_request_for_pressable = on_dismiss_request.clone();
1611 cx.pressable(
1612 PressableProps {
1613 layout: select_modal_barrier_layout(),
1614 enabled: true,
1615 focusable: false,
1616 ..Default::default()
1617 },
1618 move |cx, _st| {
1619 let open_for_pointer_up = open_for_pressable.clone();
1620 let guard_for_pointer_up = guard_for_pressable.clone();
1621 let on_dismiss_request_for_pointer_up = on_dismiss_request_for_pressable.clone();
1622 cx.pressable_add_on_pointer_up(Arc::new(move |host, action_cx, up| {
1623 match select_mouse_open_guard_pointer_up_decision_shared(
1624 &guard_for_pointer_up,
1625 up,
1626 ) {
1627 SelectMouseOpenGuardPointerUpDecision::Suppress => {
1628 host.request_redraw(action_cx.window);
1629 return PressablePointerUpResult::SkipActivate;
1630 }
1631 SelectMouseOpenGuardPointerUpDecision::Allow
1632 | SelectMouseOpenGuardPointerUpDecision::NoGuard => {}
1633 }
1634
1635 if let Some(on_dismiss_request) = on_dismiss_request_for_pointer_up.as_ref() {
1636 let mut req = DismissRequestCx::new(DismissReason::OutsidePress {
1637 pointer: Some(fret_ui::action::OutsidePressCx {
1638 pointer_id: up.pointer_id,
1639 pointer_type: up.pointer_type,
1640 button: up.button,
1641 modifiers: up.modifiers,
1642 click_count: up.click_count,
1643 }),
1644 });
1645 on_dismiss_request(host, action_cx, &mut req);
1646 if !req.default_prevented() {
1647 let _ = host
1648 .models_mut()
1649 .update(&open_for_pointer_up, |v| *v = false);
1650 }
1651 } else {
1652 let _ = host
1653 .models_mut()
1654 .update(&open_for_pointer_up, |v| *v = false);
1655 }
1656
1657 PressablePointerUpResult::SkipActivate
1658 }));
1659
1660 barrier_children
1661 },
1662 )
1663 } else {
1664 cx.container(
1665 fret_ui::element::ContainerProps {
1666 layout: select_modal_barrier_layout(),
1667 ..Default::default()
1668 },
1669 move |_cx| barrier_children,
1670 )
1671 };
1672 Elements::from([barrier, content, guard_el])
1673}
1674
1675pub fn select_item_pointer_up_handler(
1681 open: Model<bool>,
1682 value: Model<Option<Arc<str>>>,
1683 item_value: Arc<str>,
1684 item_disabled: bool,
1685 mouse_open_guard: SelectMouseOpenGuard,
1686) -> OnPointerUp {
1687 select_item_pointer_up_handler_with_mouse_up_gate(
1688 open,
1689 value,
1690 item_value,
1691 item_disabled,
1692 mouse_open_guard,
1693 false,
1694 None,
1695 )
1696}
1697
1698pub fn select_item_pointer_up_handler_with_mouse_up_gate(
1704 open: Model<bool>,
1705 value: Model<Option<Arc<str>>>,
1706 item_value: Arc<str>,
1707 item_disabled: bool,
1708 mouse_open_guard: SelectMouseOpenGuard,
1709 item_is_selected: bool,
1710 mouse_up_gate: Option<Arc<Mutex<SelectMouseUpSelectionGateState>>>,
1711) -> OnPointerUp {
1712 Arc::new(move |host, action_cx, up: PointerUpCx| {
1713 if up.button != fret_core::MouseButton::Left {
1714 return false;
1715 }
1716 if !matches!(up.pointer_type, PointerType::Mouse | PointerType::Unknown) {
1717 return false;
1718 }
1719 if item_disabled {
1720 return true;
1721 }
1722 if select_mouse_open_guard_should_suppress_pointer_up_shared(&mouse_open_guard, up) {
1723 return true;
1724 }
1725 if let Some(mouse_up_gate) = mouse_up_gate.as_ref() {
1726 let gate = mouse_up_gate.lock().unwrap_or_else(|e| e.into_inner());
1727 if !gate.should_allow_item_mouse_up(item_is_selected) {
1728 return true;
1729 }
1730 }
1731
1732 let _ = host
1733 .models_mut()
1734 .update(&value, |v| *v = Some(item_value.clone()));
1735 let _ = host.models_mut().update(&open, |v| *v = false);
1736 host.request_redraw(action_cx.window);
1737 true
1738 })
1739}
1740
1741#[derive(Clone)]
1747pub struct SelectItemPressablePointerHandlers {
1748 pub on_pointer_down: OnPressablePointerDown,
1749 pub on_pointer_up: OnPressablePointerUp,
1750}
1751
1752pub fn select_item_pressable_pointer_handlers_with_mouse_up_gate(
1764 open: Model<bool>,
1765 value: Model<Option<Arc<str>>>,
1766 item_value: Arc<str>,
1767 item_disabled: bool,
1768 mouse_open_guard: SelectMouseOpenGuard,
1769 item_is_selected: bool,
1770 mouse_up_gate: Option<Arc<Mutex<SelectMouseUpSelectionGateState>>>,
1771 on_value_change: Option<Arc<dyn Fn(&mut dyn UiActionHost, ActionCx, Arc<str>) + 'static>>,
1772) -> SelectItemPressablePointerHandlers {
1773 let did_pointer_down = Arc::new(Mutex::new(false));
1774
1775 let did_pointer_down_for_down = did_pointer_down.clone();
1776 let mouse_open_guard_for_down = mouse_open_guard.clone();
1777 let on_pointer_down: OnPressablePointerDown = Arc::new(move |_host, _action_cx, down| {
1778 if matches!(down.pointer_type, PointerType::Mouse | PointerType::Unknown) {
1779 select_mouse_open_guard_clear(&mouse_open_guard_for_down);
1780 let mut pressed = did_pointer_down_for_down
1781 .lock()
1782 .unwrap_or_else(|e| e.into_inner());
1783 *pressed = true;
1784 }
1785 PressablePointerDownResult::Continue
1786 });
1787
1788 let did_pointer_down_for_up = did_pointer_down.clone();
1789 let open_for_up = open.clone();
1790 let value_for_up = value.clone();
1791 let item_value_for_up = item_value.clone();
1792 let mouse_open_guard_for_up = mouse_open_guard.clone();
1793 let on_value_change_for_up = on_value_change.clone();
1794 let on_pointer_up: OnPressablePointerUp = Arc::new(move |host, action_cx, up| {
1795 if up.button != fret_core::MouseButton::Left {
1796 return PressablePointerUpResult::Continue;
1797 }
1798 if !matches!(up.pointer_type, PointerType::Mouse | PointerType::Unknown) {
1799 return PressablePointerUpResult::Continue;
1800 }
1801 if item_disabled {
1802 return PressablePointerUpResult::SkipActivate;
1803 }
1804
1805 let had_pointer_down = {
1806 let mut pressed = did_pointer_down_for_up
1807 .lock()
1808 .unwrap_or_else(|e| e.into_inner());
1809 let had = *pressed;
1810 *pressed = false;
1811 had
1812 };
1813
1814 if had_pointer_down && up.is_click {
1815 return PressablePointerUpResult::Continue;
1816 }
1817
1818 if !had_pointer_down {
1819 if select_mouse_open_guard_should_suppress_pointer_up_shared(
1820 &mouse_open_guard_for_up,
1821 up,
1822 ) {
1823 return PressablePointerUpResult::SkipActivate;
1824 }
1825 if let Some(mouse_up_gate) = mouse_up_gate.as_ref() {
1826 let gate = mouse_up_gate.lock().unwrap_or_else(|e| e.into_inner());
1827 if !gate.should_allow_item_mouse_up(item_is_selected) {
1828 return PressablePointerUpResult::SkipActivate;
1829 }
1830 }
1831 }
1832
1833 let before = host
1834 .models_mut()
1835 .read(&value_for_up, |v| v.clone())
1836 .ok()
1837 .flatten();
1838 let did_change = before.as_deref() != Some(item_value_for_up.as_ref());
1839 let _ = host
1840 .models_mut()
1841 .update(&value_for_up, |v| *v = Some(item_value_for_up.clone()));
1842 let _ = host.models_mut().update(&open_for_up, |v| *v = false);
1843 if did_change && let Some(on_value_change) = on_value_change_for_up.as_ref() {
1844 on_value_change(host, action_cx, item_value_for_up.clone());
1845 }
1846 host.request_redraw(action_cx.window);
1847 PressablePointerUpResult::SkipActivate
1848 });
1849
1850 SelectItemPressablePointerHandlers {
1851 on_pointer_down,
1852 on_pointer_up,
1853 }
1854}
1855
1856pub fn modal_select_request(
1860 id: GlobalElementId,
1861 trigger: GlobalElementId,
1862 open: Model<bool>,
1863 presence: OverlayPresence,
1864 children: impl IntoIterator<Item = AnyElement>,
1865) -> OverlayRequest {
1866 let children: Vec<AnyElement> = children.into_iter().collect();
1867 let mut request = OverlayRequest::modal(id, Some(trigger), open, presence, children);
1868 request.close_on_window_focus_lost = true;
1869 request.close_on_window_resize = true;
1870 request.root_name = Some(select_root_name(id));
1871 request
1872}
1873
1874pub fn modal_select_request_with_dismiss_handler(
1877 id: GlobalElementId,
1878 trigger: GlobalElementId,
1879 open: Model<bool>,
1880 presence: OverlayPresence,
1881 on_dismiss_request: Option<OnDismissRequest>,
1882 children: impl IntoIterator<Item = AnyElement>,
1883) -> OverlayRequest {
1884 let mut request = modal_select_request(id, trigger, open, presence, children);
1885 request.dismissible_on_dismiss_request = on_dismiss_request;
1886 request
1887}
1888
1889pub fn request_select<H: UiHost>(cx: &mut ElementContext<'_, H>, request: OverlayRequest) {
1891 OverlayController::request(cx, request);
1892}
1893
1894#[cfg(test)]
1895mod tests {
1896 use super::*;
1897
1898 use std::cell::Cell;
1899
1900 use fret_app::App;
1901 use fret_core::{
1902 AppWindowId, Event, Modifiers, MouseButtons, Point, PointerEvent, PointerId, PointerType,
1903 Px, Rect, Size,
1904 };
1905 use fret_core::{MaterialDescriptor, MaterialId, MaterialRegistrationError, MaterialService};
1906 use fret_core::{PathCommand, SvgId, SvgService};
1907 use fret_core::{PathConstraints, PathId, PathMetrics, PathService, PathStyle};
1908 use fret_core::{Scene, Transform2D};
1909 use fret_core::{TextBlobId, TextConstraints, TextInput, TextMetrics, TextService};
1910 use fret_ui::action::{UiActionHostAdapter, UiFocusActionHost, UiPointerActionHost};
1911 use fret_ui::element::{ContainerProps, ElementKind, LayoutStyle, Length, PressableProps};
1912 use std::time::Duration;
1913
1914 use fret_ui::UiTree;
1915
1916 #[derive(Default)]
1917 struct FakeServices;
1918
1919 impl TextService for FakeServices {
1920 fn prepare(
1921 &mut self,
1922 _input: &TextInput,
1923 _constraints: TextConstraints,
1924 ) -> (TextBlobId, TextMetrics) {
1925 (
1926 TextBlobId::default(),
1927 TextMetrics {
1928 size: fret_core::Size::new(Px(0.0), Px(0.0)),
1929 baseline: Px(0.0),
1930 },
1931 )
1932 }
1933
1934 fn release(&mut self, _blob: TextBlobId) {}
1935 }
1936
1937 impl PathService for FakeServices {
1938 fn prepare(
1939 &mut self,
1940 _commands: &[PathCommand],
1941 _style: PathStyle,
1942 _constraints: PathConstraints,
1943 ) -> (PathId, PathMetrics) {
1944 (PathId::default(), PathMetrics::default())
1945 }
1946
1947 fn release(&mut self, _path: PathId) {}
1948 }
1949
1950 impl SvgService for FakeServices {
1951 fn register_svg(&mut self, _bytes: &[u8]) -> SvgId {
1952 SvgId::default()
1953 }
1954
1955 fn unregister_svg(&mut self, _svg: SvgId) -> bool {
1956 true
1957 }
1958 }
1959
1960 impl MaterialService for FakeServices {
1961 fn register_material(
1962 &mut self,
1963 _desc: MaterialDescriptor,
1964 ) -> Result<MaterialId, MaterialRegistrationError> {
1965 Err(MaterialRegistrationError::Unsupported)
1966 }
1967
1968 fn unregister_material(&mut self, _id: MaterialId) -> bool {
1969 true
1970 }
1971 }
1972
1973 fn bounds() -> Rect {
1974 Rect::new(
1975 Point::new(Px(0.0), Px(0.0)),
1976 Size::new(Px(200.0), Px(120.0)),
1977 )
1978 }
1979
1980 #[test]
1981 fn select_root_open_model_uses_controlled_model() {
1982 let window = AppWindowId::default();
1983 let mut app = App::new();
1984 let b = bounds();
1985
1986 let controlled = app.models_mut().insert(true);
1987
1988 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
1989 let root = SelectRoot::new()
1990 .open(Some(controlled.clone()))
1991 .default_open(false);
1992 assert_eq!(root.open_model(cx), controlled);
1993 });
1994 }
1995
1996 #[test]
1997 fn select_initial_focus_targets_gate_by_input_modality() {
1998 let window = AppWindowId::default();
1999 let mut app = App::new();
2000 let b = bounds();
2001
2002 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
2003 let pointer_focus = GlobalElementId(0x111);
2004 let keyboard_focus = GlobalElementId(0x222);
2005
2006 fret_ui::input_modality::update_for_event(
2008 cx.app,
2009 window,
2010 &Event::Pointer(PointerEvent::Move {
2011 position: Point::new(Px(1.0), Px(2.0)),
2012 buttons: MouseButtons::default(),
2013 modifiers: Modifiers::default(),
2014 pointer_id: PointerId(0),
2015 pointer_type: PointerType::Mouse,
2016 }),
2017 );
2018 assert_eq!(
2019 SelectInitialFocusTargets::new()
2020 .pointer_content_focus(Some(pointer_focus))
2021 .keyboard_entry_focus(Some(keyboard_focus))
2022 .resolve(cx, window),
2023 Some(pointer_focus)
2024 );
2025
2026 fret_ui::input_modality::update_for_event(
2028 cx.app,
2029 window,
2030 &Event::KeyDown {
2031 key: fret_core::KeyCode::KeyA,
2032 modifiers: Modifiers::default(),
2033 repeat: false,
2034 },
2035 );
2036 assert_eq!(
2037 SelectInitialFocusTargets::new()
2038 .pointer_content_focus(Some(pointer_focus))
2039 .keyboard_entry_focus(Some(keyboard_focus))
2040 .resolve(cx, window),
2041 Some(keyboard_focus)
2042 );
2043 });
2044 }
2045
2046 #[test]
2047 fn select_use_value_model_prefers_controlled_and_does_not_call_default() {
2048 let window = AppWindowId::default();
2049 let mut app = App::new();
2050 let b = bounds();
2051
2052 let controlled = app.models_mut().insert(Some(Arc::from("a")));
2053 let called = Cell::new(0);
2054
2055 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
2056 let out = select_use_value_model(cx, Some(controlled.clone()), || {
2057 called.set(called.get() + 1);
2058 None
2059 });
2060 assert!(out.is_controlled());
2061 assert_eq!(out.model(), controlled);
2062 });
2063
2064 assert_eq!(called.get(), 0);
2065 }
2066
2067 #[test]
2068 fn select_item_aligned_layout_from_elements_ignores_visual_bounds_for_viewport() {
2069 let window = AppWindowId::default();
2070 let mut app = App::new();
2071 let mut ui: UiTree<App> = UiTree::new();
2072 ui.set_window(window);
2073
2074 let mut services = FakeServices::default();
2075
2076 let prepare_frame = |app: &mut App, window: AppWindowId| {
2077 let frame_id = app.frame_id();
2078 app.with_global_mut_untracked(
2079 fret_ui::elements::ElementRuntime::default,
2080 |rt, _app| {
2081 rt.prepare_window_for_frame(window, frame_id);
2082 },
2083 );
2084 };
2085
2086 let b = Rect::new(
2087 Point::new(Px(0.0), Px(0.0)),
2088 Size::new(Px(320.0), Px(240.0)),
2089 );
2090
2091 let trigger = Rect::new(
2093 Point::new(Px(120.0), Px(32.0)),
2094 Size::new(Px(120.0), Px(28.0)),
2095 );
2096 let value_node = Rect::new(
2097 Point::new(Px(132.0), Px(40.0)),
2098 Size::new(Px(80.0), Px(16.0)),
2099 );
2100 let content_panel = Rect::new(
2101 Point::new(Px(80.0), Px(72.0)),
2102 Size::new(Px(200.0), Px(140.0)),
2103 );
2104 let viewport = Rect::new(
2105 Point::new(Px(80.0), Px(88.0)),
2106 Size::new(Px(200.0), Px(108.0)),
2107 );
2108 let listbox = Rect::new(
2109 Point::new(Px(80.0), Px(88.0)),
2110 Size::new(Px(200.0), Px(420.0)),
2111 );
2112 let selected_item = Rect::new(
2113 Point::new(Px(80.0), Px(120.0)),
2114 Size::new(Px(200.0), Px(28.0)),
2115 );
2116 let selected_item_text = Rect::new(
2117 Point::new(Px(96.0), Px(126.0)),
2118 Size::new(Px(120.0), Px(16.0)),
2119 );
2120
2121 fn render_frame(
2122 ui: &mut UiTree<App>,
2123 app: &mut App,
2124 services: &mut FakeServices,
2125 window: AppWindowId,
2126 b: Rect,
2127 trigger: Rect,
2128 value_node: Rect,
2129 content_panel: Rect,
2130 viewport: Rect,
2131 listbox: Rect,
2132 selected_item: Rect,
2133 selected_item_text: Rect,
2134 got: Option<&Cell<Option<SelectItemAlignedLayout>>>,
2135 ) -> fret_core::NodeId {
2136 let viewport_id: Cell<Option<GlobalElementId>> = Cell::new(None);
2137 let listbox_id: Cell<Option<GlobalElementId>> = Cell::new(None);
2138 let content_panel_id: Cell<Option<GlobalElementId>> = Cell::new(None);
2139 let selected_item_id: Cell<Option<GlobalElementId>> = Cell::new(None);
2140 let selected_item_text_id: Cell<Option<GlobalElementId>> = Cell::new(None);
2141
2142 fret_ui::declarative::render_root(ui, app, services, window, b, "test", |cx| {
2143 let abs = |rect: Rect| {
2144 let mut layout = LayoutStyle::default();
2145 layout.position = fret_ui::element::PositionStyle::Absolute;
2146 layout.inset.left = Some(rect.origin.x).into();
2147 layout.inset.top = Some(rect.origin.y).into();
2148 layout.size.width = Length::Px(rect.size.width);
2149 layout.size.height = Length::Px(rect.size.height);
2150 ContainerProps {
2151 layout,
2152 ..Default::default()
2153 }
2154 };
2155
2156 let value_node_el = cx.container(abs(value_node), |_cx| Vec::new());
2157
2158 let mut transform_layout = LayoutStyle::default();
2159 transform_layout.size.width = Length::Fill;
2160 transform_layout.size.height = Length::Fill;
2161 let transformed = cx.render_transform_props(
2162 fret_ui::element::RenderTransformProps {
2163 layout: transform_layout,
2164 transform: Transform2D::scale_uniform(0.5),
2165 },
2166 |cx| {
2167 let viewport_el = cx.container(abs(viewport), |_cx| Vec::new());
2168 viewport_id.set(Some(viewport_el.id));
2169 let listbox_el = cx.container(abs(listbox), |_cx| Vec::new());
2170 listbox_id.set(Some(listbox_el.id));
2171 let content_panel_el = cx.container(abs(content_panel), |_cx| Vec::new());
2172 content_panel_id.set(Some(content_panel_el.id));
2173 let selected_item_el = cx.container(abs(selected_item), |_cx| Vec::new());
2174 selected_item_id.set(Some(selected_item_el.id));
2175 let selected_item_text_el =
2176 cx.container(abs(selected_item_text), |_cx| Vec::new());
2177 selected_item_text_id.set(Some(selected_item_text_el.id));
2178
2179 vec![
2180 viewport_el,
2181 listbox_el,
2182 content_panel_el,
2183 selected_item_el,
2184 selected_item_text_el,
2185 ]
2186 },
2187 );
2188
2189 if let Some(got) = got {
2190 let inputs = SelectItemAlignedElementInputs {
2191 direction: popper::LayoutDirection::Ltr,
2192 window: b,
2193 trigger,
2194 content_min_width: Px(80.0),
2195 content_border_top: Px(1.0),
2196 content_padding_top: Px(0.0),
2197 content_border_bottom: Px(1.0),
2198 content_padding_bottom: Px(0.0),
2199 viewport_padding_top: Px(4.0),
2200 viewport_padding_bottom: Px(4.0),
2201 selected_item_is_first: false,
2202 selected_item_is_last: false,
2203 value_node: value_node_el.id,
2204 viewport: viewport_id.get().expect("viewport id"),
2205 listbox: listbox_id.get().expect("listbox id"),
2206 content_panel: content_panel_id.get().expect("content_panel id"),
2207 scroll_max_offset_y: None,
2208 content_width_probe: None,
2209 selected_item: selected_item_id.get().expect("selected_item id"),
2210 selected_item_text: selected_item_text_id
2211 .get()
2212 .expect("selected_item_text id"),
2213 };
2214 got.set(select_item_aligned_layout_from_elements(&*cx, inputs));
2215 }
2216
2217 vec![value_node_el, transformed]
2218 })
2219 }
2220
2221 app.set_frame_id(fret_core::FrameId(app.frame_id().0.saturating_add(1)));
2224 OverlayController::begin_frame(&mut app, window);
2225 prepare_frame(&mut app, window);
2226 let root0 = render_frame(
2227 &mut ui,
2228 &mut app,
2229 &mut services,
2230 window,
2231 b,
2232 trigger,
2233 value_node,
2234 content_panel,
2235 viewport,
2236 listbox,
2237 selected_item,
2238 selected_item_text,
2239 None,
2240 );
2241 ui.set_root(root0);
2242 ui.layout_all(&mut app, &mut services, b, 1.0);
2243 let mut scene = Scene::default();
2244 ui.paint_all(&mut app, &mut services, b, &mut scene, 1.0);
2245
2246 app.set_frame_id(fret_core::FrameId(app.frame_id().0.saturating_add(1)));
2248 OverlayController::begin_frame(&mut app, window);
2249 prepare_frame(&mut app, window);
2250
2251 let expected = select_item_aligned_layout(SelectItemAlignedInputs {
2253 direction: fret_core::LayoutDirection::Ltr,
2254 window: b,
2255 trigger,
2256 content: content_panel,
2257 value_node,
2258 selected_item_text,
2259 selected_item,
2260 viewport,
2261 content_border_top: Px(1.0),
2262 content_padding_top: Px(0.0),
2263 content_border_bottom: Px(1.0),
2264 content_padding_bottom: Px(0.0),
2265 viewport_padding_top: Px(4.0),
2266 viewport_padding_bottom: Px(4.0),
2267 selected_item_is_first: false,
2268 selected_item_is_last: false,
2269 items_height: listbox.size.height,
2270 });
2271
2272 let got: Cell<Option<SelectItemAlignedLayout>> = Cell::new(None);
2273 let root1 = render_frame(
2274 &mut ui,
2275 &mut app,
2276 &mut services,
2277 window,
2278 b,
2279 trigger,
2280 value_node,
2281 content_panel,
2282 viewport,
2283 listbox,
2284 selected_item,
2285 selected_item_text,
2286 Some(&got),
2287 );
2288 ui.set_root(root1);
2289 ui.layout_all(&mut app, &mut services, b, 1.0);
2290
2291 let got = got.get().expect("expected resolved layout");
2292 assert_eq!(got.rect, expected.rect);
2293 assert_eq!(got.side, expected.side);
2294 assert_eq!(got.outputs, expected.outputs);
2295 }
2296
2297 struct PointerHost<'a> {
2298 app: &'a mut App,
2299 bounds: Rect,
2300 prevented_focus_on_pointer_down: bool,
2301 }
2302
2303 impl fret_ui::action::UiActionHost for PointerHost<'_> {
2304 fn models_mut(&mut self) -> &mut fret_runtime::ModelStore {
2305 self.app.models_mut()
2306 }
2307
2308 fn push_effect(&mut self, effect: Effect) {
2309 self.app.push_effect(effect);
2310 }
2311
2312 fn request_redraw(&mut self, window: AppWindowId) {
2313 self.app.request_redraw(window);
2314 }
2315
2316 fn next_timer_token(&mut self) -> TimerToken {
2317 self.app.next_timer_token()
2318 }
2319
2320 fn next_clipboard_token(&mut self) -> fret_runtime::ClipboardToken {
2321 self.app.next_clipboard_token()
2322 }
2323
2324 fn next_share_sheet_token(&mut self) -> fret_runtime::ShareSheetToken {
2325 self.app.next_share_sheet_token()
2326 }
2327 }
2328
2329 impl UiFocusActionHost for PointerHost<'_> {
2330 fn request_focus(&mut self, _target: GlobalElementId) {}
2331 }
2332
2333 impl fret_ui::action::UiDragActionHost for PointerHost<'_> {
2334 fn begin_drag_with_kind(
2335 &mut self,
2336 _pointer_id: fret_core::PointerId,
2337 _kind: fret_runtime::DragKindId,
2338 _source_window: fret_core::AppWindowId,
2339 _start: fret_core::Point,
2340 ) {
2341 }
2342
2343 fn begin_cross_window_drag_with_kind(
2344 &mut self,
2345 _pointer_id: fret_core::PointerId,
2346 _kind: fret_runtime::DragKindId,
2347 _source_window: fret_core::AppWindowId,
2348 _start: fret_core::Point,
2349 ) {
2350 }
2351
2352 fn drag(&self, _pointer_id: fret_core::PointerId) -> Option<&fret_runtime::DragSession> {
2353 None
2354 }
2355
2356 fn drag_mut(
2357 &mut self,
2358 _pointer_id: fret_core::PointerId,
2359 ) -> Option<&mut fret_runtime::DragSession> {
2360 None
2361 }
2362
2363 fn cancel_drag(&mut self, _pointer_id: fret_core::PointerId) {}
2364 }
2365
2366 impl UiPointerActionHost for PointerHost<'_> {
2367 fn bounds(&self) -> Rect {
2368 self.bounds
2369 }
2370
2371 fn capture_pointer(&mut self) {}
2372
2373 fn release_pointer_capture(&mut self) {}
2374
2375 fn prevent_default(&mut self, action: fret_runtime::DefaultAction) {
2376 if action == fret_runtime::DefaultAction::FocusOnPointerDown {
2377 self.prevented_focus_on_pointer_down = true;
2378 }
2379 }
2380
2381 fn set_cursor_icon(&mut self, _icon: fret_core::CursorIcon) {}
2382 }
2383
2384 #[test]
2385 fn apply_select_trigger_a11y_sets_role_expanded_and_controls() {
2386 let window = AppWindowId::default();
2387 let mut app = App::new();
2388 let b = bounds();
2389
2390 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
2391 let trigger = cx.pressable(
2392 PressableProps {
2393 layout: LayoutStyle::default(),
2394 enabled: true,
2395 focusable: true,
2396 ..Default::default()
2397 },
2398 |_cx, _st| Vec::new(),
2399 );
2400
2401 let listbox = GlobalElementId(0xbeef);
2402 let trigger =
2403 apply_select_trigger_a11y(trigger, true, Some(Arc::from("Select")), Some(listbox));
2404
2405 let ElementKind::Pressable(PressableProps { a11y, .. }) = &trigger.kind else {
2406 panic!("expected pressable trigger");
2407 };
2408 assert_eq!(a11y.role, Some(fret_core::SemanticsRole::ComboBox));
2409 assert_eq!(a11y.expanded, Some(true));
2410 assert_eq!(a11y.controls_element, Some(listbox.0));
2411 assert_eq!(a11y.label.as_deref(), Some("Select"));
2412 });
2413 }
2414
2415 #[test]
2416 fn select_listbox_semantics_id_matches_mounted_listbox_id() {
2417 let window = AppWindowId::default();
2418 let mut app = App::new();
2419 let b = bounds();
2420
2421 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
2422 let overlay_root_name = "select-overlay";
2423 let expected = select_listbox_semantics_id::<App>(cx, overlay_root_name);
2424 let inherited = portal_inherited::PortalInherited::capture(cx);
2425 let actual = portal_inherited::with_root_name_inheriting(
2426 cx,
2427 overlay_root_name,
2428 inherited,
2429 |cx| {
2430 select_listbox_pressable_with_id_props::<App, _, _>(cx, |_cx, _st, _id| {
2431 (
2432 PressableProps {
2433 layout: LayoutStyle::default(),
2434 enabled: true,
2435 focusable: false,
2436 ..Default::default()
2437 },
2438 Vec::<AnyElement>::new(),
2439 )
2440 })
2441 .id
2442 },
2443 );
2444 assert_eq!(expected, actual);
2445 });
2446 }
2447
2448 #[test]
2449 fn modal_select_request_sets_default_root_name() {
2450 let mut app = App::new();
2451 let open = app.models_mut().insert(false);
2452 let id = GlobalElementId(0x123);
2453 let trigger = GlobalElementId(0x456);
2454
2455 let req = modal_select_request(
2456 id,
2457 trigger,
2458 open,
2459 OverlayPresence::instant(true),
2460 Vec::new(),
2461 );
2462 let expected = select_root_name(id);
2463 assert_eq!(req.root_name.as_deref(), Some(expected.as_str()));
2464 }
2465
2466 #[test]
2467 fn select_content_placement_item_aligned_has_no_wrapper_insets_and_origin_on_rect_edge() {
2468 let window = Rect::new(
2469 Point::new(Px(0.0), Px(0.0)),
2470 Size::new(Px(300.0), Px(200.0)),
2471 );
2472 let anchor = Rect::new(
2473 Point::new(Px(100.0), Px(80.0)),
2474 Size::new(Px(80.0), Px(24.0)),
2475 );
2476
2477 let item_layout = select_item_aligned_layout(SelectItemAlignedInputs {
2478 direction: popper::LayoutDirection::Ltr,
2479 window,
2480 trigger: anchor,
2481 content: Rect::new(
2482 Point::new(Px(0.0), Px(0.0)),
2483 Size::new(Px(160.0), Px(120.0)),
2484 ),
2485 value_node: Rect::new(
2486 Point::new(Px(110.0), Px(84.0)),
2487 Size::new(Px(60.0), Px(16.0)),
2488 ),
2489 selected_item_text: Rect::new(
2490 Point::new(Px(20.0), Px(40.0)),
2491 Size::new(Px(80.0), Px(16.0)),
2492 ),
2493 selected_item: Rect::new(
2494 Point::new(Px(10.0), Px(36.0)),
2495 Size::new(Px(140.0), Px(24.0)),
2496 ),
2497 viewport: Rect::new(
2498 Point::new(Px(10.0), Px(30.0)),
2499 Size::new(Px(160.0), Px(120.0)),
2500 ),
2501 content_border_top: Px(1.0),
2502 content_padding_top: Px(0.0),
2503 content_border_bottom: Px(1.0),
2504 content_padding_bottom: Px(0.0),
2505 viewport_padding_top: Px(4.0),
2506 viewport_padding_bottom: Px(4.0),
2507 selected_item_is_first: false,
2508 selected_item_is_last: false,
2509 items_height: Px(240.0),
2510 });
2511
2512 let placement = select_content_placement_item_aligned(anchor, item_layout);
2513 assert_eq!(placement.wrapper_insets, Edges::all(Px(0.0)));
2514 assert!(placement.popper_layout.is_none());
2515
2516 match placement.side {
2517 Side::Bottom => assert_eq!(placement.transform_origin.y, placement.placed.origin.y),
2518 Side::Top => {
2519 assert_eq!(
2520 placement.transform_origin.y,
2521 Px(placement.placed.origin.y.0 + placement.placed.size.height.0)
2522 )
2523 }
2524 Side::Left | Side::Right => {}
2525 }
2526 }
2527
2528 #[test]
2529 fn select_content_placement_popper_exposes_layout_and_wrapper_insets_when_arrow_enabled() {
2530 let outer = Rect::new(
2531 Point::new(Px(0.0), Px(0.0)),
2532 Size::new(Px(300.0), Px(200.0)),
2533 );
2534 let anchor = Rect::new(
2535 Point::new(Px(120.0), Px(40.0)),
2536 Size::new(Px(80.0), Px(24.0)),
2537 );
2538 let desired = Size::new(Px(180.0), Px(120.0));
2539
2540 let (arrow_options, arrow_protrusion) =
2541 popper::diamond_arrow_options(true, Px(12.0), Px(4.0));
2542 let placement = popper::PopperContentPlacement::new(
2543 popper::LayoutDirection::Ltr,
2544 Side::Bottom,
2545 popper::Align::Start,
2546 Px(6.0),
2547 )
2548 .with_align_offset(Px(0.0))
2549 .with_arrow(arrow_options, arrow_protrusion);
2550
2551 let out =
2552 select_content_placement_popper(outer, anchor, desired, placement, Some(Px(12.0)));
2553 assert!(out.popper_layout.is_some());
2554 assert_eq!(out.placed, out.popper_layout.unwrap().rect);
2555 assert!(
2556 out.wrapper_insets.top.0 > 0.0
2557 || out.wrapper_insets.bottom.0 > 0.0
2558 || out.wrapper_insets.left.0 > 0.0
2559 || out.wrapper_insets.right.0 > 0.0
2560 );
2561 }
2562
2563 #[test]
2564 fn select_resolve_content_placement_prefers_item_aligned_layout_when_provided() {
2565 let outer = Rect::new(
2566 Point::new(Px(0.0), Px(0.0)),
2567 Size::new(Px(300.0), Px(200.0)),
2568 );
2569 let anchor = Rect::new(
2570 Point::new(Px(120.0), Px(40.0)),
2571 Size::new(Px(80.0), Px(24.0)),
2572 );
2573 let desired = Size::new(Px(180.0), Px(120.0));
2574
2575 let item_layout = select_item_aligned_layout(SelectItemAlignedInputs {
2576 direction: popper::LayoutDirection::Ltr,
2577 window: outer,
2578 trigger: anchor,
2579 content: Rect::new(Point::new(Px(0.0), Px(0.0)), desired),
2580 value_node: Rect::new(
2581 Point::new(Px(130.0), Px(44.0)),
2582 Size::new(Px(60.0), Px(16.0)),
2583 ),
2584 selected_item_text: Rect::new(
2585 Point::new(Px(20.0), Px(40.0)),
2586 Size::new(Px(80.0), Px(16.0)),
2587 ),
2588 selected_item: Rect::new(
2589 Point::new(Px(10.0), Px(36.0)),
2590 Size::new(Px(140.0), Px(24.0)),
2591 ),
2592 viewport: Rect::new(
2593 Point::new(Px(10.0), Px(30.0)),
2594 Size::new(Px(160.0), Px(120.0)),
2595 ),
2596 content_border_top: Px(1.0),
2597 content_padding_top: Px(0.0),
2598 content_border_bottom: Px(1.0),
2599 content_padding_bottom: Px(0.0),
2600 viewport_padding_top: Px(4.0),
2601 viewport_padding_bottom: Px(4.0),
2602 selected_item_is_first: false,
2603 selected_item_is_last: false,
2604 items_height: Px(240.0),
2605 });
2606
2607 let popper_placement = popper::PopperContentPlacement::new(
2608 popper::LayoutDirection::Ltr,
2609 Side::Bottom,
2610 popper::Align::Start,
2611 Px(6.0),
2612 );
2613 let resolved = select_resolve_content_placement(
2614 anchor,
2615 outer,
2616 desired,
2617 popper_placement,
2618 None,
2619 Some(item_layout),
2620 );
2621
2622 assert!(resolved.item_aligned_layout.is_some());
2623 assert!(resolved.placement.popper_layout.is_none());
2624 }
2625
2626 #[test]
2627 fn select_open_keys_match_radix_defaults() {
2628 assert!(is_select_open_key(KeyCode::Enter));
2629 assert!(is_select_open_key(KeyCode::Space));
2630 assert!(is_select_open_key(KeyCode::ArrowDown));
2631 assert!(is_select_open_key(KeyCode::ArrowUp));
2632 assert!(!is_select_open_key(KeyCode::Escape));
2633
2634 assert!(select_open_key_suppresses_activate(KeyCode::Enter));
2635 assert!(select_open_key_suppresses_activate(KeyCode::Space));
2636 assert!(!select_open_key_suppresses_activate(KeyCode::ArrowDown));
2637 assert!(!select_open_key_suppresses_activate(KeyCode::ArrowUp));
2638 }
2639
2640 #[test]
2641 fn select_popper_available_height_tracks_flipped_side_space() {
2642 let outer = Rect::new(
2643 Point::new(Px(0.0), Px(0.0)),
2644 Size::new(Px(100.0), Px(100.0)),
2645 );
2646 let anchor = Rect::new(
2647 Point::new(Px(10.0), Px(70.0)),
2648 Size::new(Px(30.0), Px(10.0)),
2649 );
2650
2651 let placement = popper::PopperContentPlacement::new(
2654 popper::LayoutDirection::Ltr,
2655 Side::Bottom,
2656 popper::Align::Start,
2657 Px(0.0),
2658 );
2659
2660 let vars = select_popper_vars(outer, anchor, Px(0.0), placement);
2661 assert!(vars.available_height.0 > 60.0 && vars.available_height.0 < 80.0);
2662 }
2663
2664 #[test]
2665 fn select_popper_desired_width_respects_min_width_and_outer_bounds() {
2666 let outer = Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(80.0), Px(100.0)));
2667 let anchor = Rect::new(
2668 Point::new(Px(10.0), Px(10.0)),
2669 Size::new(Px(24.0), Px(10.0)),
2670 );
2671
2672 assert_eq!(
2673 select_popper_desired_width(outer, anchor, Px(0.0)),
2674 Px(24.0)
2675 );
2676 assert_eq!(
2677 select_popper_desired_width(outer, anchor, Px(40.0)),
2678 Px(40.0)
2679 );
2680 assert_eq!(
2681 select_popper_desired_width(outer, anchor, Px(100.0)),
2682 Px(80.0)
2683 );
2684 }
2685
2686 #[test]
2687 fn trigger_typeahead_updates_value_without_opening() {
2688 let window = AppWindowId::default();
2689 let mut app = App::new();
2690 let open = app.models_mut().insert(false);
2691 let value = app.models_mut().insert(None::<Arc<str>>);
2692
2693 let values: Vec<Arc<str>> = vec![Arc::from("alpha"), Arc::from("beta")];
2694 let labels: Vec<Arc<str>> = vec![Arc::from("Alpha"), Arc::from("Beta")];
2695 let disabled = vec![false, false];
2696
2697 let mut state = SelectTriggerKeyState::default();
2698 let mut host = UiActionHostAdapter { app: &mut app };
2699 assert!(state.handle_key_down_when_closed(
2700 &mut host,
2701 window,
2702 &open,
2703 &value,
2704 &values,
2705 &labels,
2706 &disabled,
2707 KeyCode::KeyB,
2708 Modifiers::default(),
2709 false,
2710 ));
2711
2712 assert!(!app.models().get_copied(&open).unwrap_or(false));
2713 assert_eq!(
2714 app.models().get_cloned(&value).flatten().as_deref(),
2715 Some("beta")
2716 );
2717
2718 let effects = app.flush_effects();
2719 assert!(
2720 effects.iter().any(|e| matches!(
2721 e,
2722 Effect::SetTimer { after, .. }
2723 if *after == Duration::from_millis(SELECT_TYPEAHEAD_CLEAR_TIMEOUT_MS)
2724 )),
2725 "expected a typeahead clear timer"
2726 );
2727 }
2728
2729 #[test]
2730 fn trigger_open_key_opens_and_suppresses_activate() {
2731 let window = AppWindowId::default();
2732 let mut app = App::new();
2733 let open = app.models_mut().insert(false);
2734 let value = app.models_mut().insert(None::<Arc<str>>);
2735
2736 let values: Vec<Arc<str>> = vec![Arc::from("alpha")];
2737 let labels: Vec<Arc<str>> = vec![Arc::from("Alpha")];
2738 let disabled = vec![false];
2739
2740 let mut state = SelectTriggerKeyState::default();
2741 let mut host = UiActionHostAdapter { app: &mut app };
2742 assert!(state.handle_key_down_when_closed(
2743 &mut host,
2744 window,
2745 &open,
2746 &value,
2747 &values,
2748 &labels,
2749 &disabled,
2750 KeyCode::Enter,
2751 Modifiers::default(),
2752 false,
2753 ));
2754
2755 assert!(app.models().get_copied(&open).unwrap_or(false));
2756 assert!(state.take_suppress_next_activate());
2757 }
2758
2759 #[test]
2760 fn content_arrow_navigation_updates_active_row() {
2761 let window = AppWindowId::default();
2762 let mut app = App::new();
2763 let open = app.models_mut().insert(true);
2764 let value = app.models_mut().insert(None::<Arc<str>>);
2765
2766 let values_by_row: Vec<Option<Arc<str>>> = vec![
2767 Some(Arc::from("alpha")),
2768 Some(Arc::from("beta")),
2769 Some(Arc::from("gamma")),
2770 ];
2771 let labels_by_row: Vec<Arc<str>> =
2772 vec![Arc::from("Alpha"), Arc::from("Beta"), Arc::from("Gamma")];
2773 let disabled_by_row = vec![false, true, false];
2774
2775 let mut state = SelectContentKeyState::default();
2776 let mut host = UiActionHostAdapter { app: &mut app };
2777
2778 assert!(state.handle_key_down_when_open(
2779 &mut host,
2780 ActionCx {
2781 window,
2782 target: GlobalElementId(1),
2783 },
2784 &open,
2785 &value,
2786 &values_by_row,
2787 &labels_by_row,
2788 &disabled_by_row,
2789 None,
2790 KeyCode::ArrowDown,
2791 false,
2792 true,
2793 ));
2794 assert_eq!(state.active_row(), Some(2));
2796 }
2797
2798 #[test]
2799 fn content_tab_is_suppressed() {
2800 let window = AppWindowId::default();
2801 let mut app = App::new();
2802 let open = app.models_mut().insert(true);
2803 let value = app.models_mut().insert(None::<Arc<str>>);
2804
2805 let values_by_row: Vec<Option<Arc<str>>> = vec![Some(Arc::from("beta"))];
2806 let labels_by_row: Vec<Arc<str>> = vec![Arc::from("Beta")];
2807 let disabled_by_row = vec![false];
2808
2809 let mut state = SelectContentKeyState::default();
2810 let mut host = UiActionHostAdapter { app: &mut app };
2811 assert!(state.handle_key_down_when_open(
2812 &mut host,
2813 ActionCx {
2814 window,
2815 target: GlobalElementId(1),
2816 },
2817 &open,
2818 &value,
2819 &values_by_row,
2820 &labels_by_row,
2821 &disabled_by_row,
2822 None,
2823 KeyCode::Tab,
2824 false,
2825 true,
2826 ));
2827
2828 assert!(app.models().get_copied(&open).unwrap_or(false));
2829 assert_eq!(state.active_row(), None);
2830 assert_eq!(app.models().get_cloned(&value).flatten().as_deref(), None);
2831 }
2832
2833 #[test]
2834 fn content_enter_commits_value_and_closes() {
2835 let window = AppWindowId::default();
2836 let mut app = App::new();
2837 let open = app.models_mut().insert(true);
2838 let value = app.models_mut().insert(None::<Arc<str>>);
2839
2840 let values_by_row: Vec<Option<Arc<str>>> = vec![Some(Arc::from("beta"))];
2841 let labels_by_row: Vec<Arc<str>> = vec![Arc::from("Beta")];
2842 let disabled_by_row = vec![false];
2843
2844 let mut state = SelectContentKeyState::default();
2845 state.set_active_row(Some(0));
2846
2847 let mut host = UiActionHostAdapter { app: &mut app };
2848 assert!(state.handle_key_down_when_open(
2849 &mut host,
2850 ActionCx {
2851 window,
2852 target: GlobalElementId(1),
2853 },
2854 &open,
2855 &value,
2856 &values_by_row,
2857 &labels_by_row,
2858 &disabled_by_row,
2859 None,
2860 KeyCode::Enter,
2861 false,
2862 true,
2863 ));
2864
2865 assert!(!app.models().get_copied(&open).unwrap_or(false));
2866 assert_eq!(
2867 app.models().get_cloned(&value).flatten().as_deref(),
2868 Some("beta")
2869 );
2870 }
2871
2872 #[test]
2873 fn trigger_pointer_mouse_down_opens() {
2874 let window = AppWindowId::default();
2875 let mut app = App::new();
2876 let open = app.models_mut().insert(false);
2877
2878 let mut state = SelectTriggerPointerState::default();
2879 let mut host = PointerHost {
2880 app: &mut app,
2881 bounds: bounds(),
2882 prevented_focus_on_pointer_down: false,
2883 };
2884
2885 assert!(state.handle_pointer_down(
2886 &mut host,
2887 ActionCx {
2888 window,
2889 target: GlobalElementId(1),
2890 },
2891 PointerDownCx {
2892 pointer_id: fret_core::PointerId(0),
2893 position: Point::new(Px(10.0), Px(12.0)),
2894 position_local: Point::new(Px(10.0), Px(12.0)),
2895 position_window: Some(Point::new(Px(10.0), Px(12.0))),
2896 tick_id: fret_runtime::TickId(0),
2897 pixels_per_point: 1.0,
2898 button: fret_core::MouseButton::Left,
2899 modifiers: Modifiers::default(),
2900 click_count: 1,
2901 pointer_type: PointerType::Mouse,
2902 hit_is_text_input: false,
2903 hit_is_pressable: false,
2904 hit_pressable_target: None,
2905 hit_pressable_target_in_descendant_subtree: false,
2906 },
2907 &open,
2908 true,
2909 ));
2910 assert!(host.models_mut().get_copied(&open).unwrap_or(false));
2911 assert!(host.prevented_focus_on_pointer_down);
2912 }
2913
2914 #[test]
2915 fn mouse_open_guard_pointer_up_decision_is_reusable_within_tick() {
2916 let guard = select_mouse_open_guard();
2917
2918 select_mouse_open_guard_record_if_opened(
2919 &guard,
2920 false,
2921 true,
2922 Point::new(Px(10.0), Px(12.0)),
2923 );
2924
2925 let up = PointerUpCx {
2926 pointer_id: fret_core::PointerId(0),
2927 position: Point::new(Px(10.0), Px(12.0)),
2928 position_local: Point::new(Px(10.0), Px(12.0)),
2929 position_window: Some(Point::new(Px(10.0), Px(12.0))),
2930 tick_id: fret_runtime::TickId(42),
2931 pixels_per_point: 1.0,
2932 velocity_window: None,
2933 button: fret_core::MouseButton::Left,
2934 modifiers: Modifiers::default(),
2935 is_click: true,
2936 click_count: 1,
2937 pointer_type: PointerType::Mouse,
2938 down_hit_pressable_target: None,
2939 down_hit_pressable_target_in_descendant_subtree: false,
2940 };
2941
2942 assert_eq!(
2943 select_mouse_open_guard_pointer_up_decision_shared(&guard, up),
2944 SelectMouseOpenGuardPointerUpDecision::Suppress
2945 );
2946 assert_eq!(
2947 select_mouse_open_guard_pointer_up_decision_shared(&guard, up),
2948 SelectMouseOpenGuardPointerUpDecision::Suppress
2949 );
2950
2951 let up_next_tick = PointerUpCx {
2952 tick_id: fret_runtime::TickId(43),
2953 ..up
2954 };
2955 assert_eq!(
2956 select_mouse_open_guard_pointer_up_decision_shared(&guard, up_next_tick),
2957 SelectMouseOpenGuardPointerUpDecision::Suppress
2958 );
2959
2960 let up_clear = PointerUpCx {
2961 tick_id: fret_runtime::TickId(44),
2962 ..up
2963 };
2964 assert_eq!(
2965 select_mouse_open_guard_pointer_up_decision_shared(&guard, up_clear),
2966 SelectMouseOpenGuardPointerUpDecision::NoGuard
2967 );
2968 }
2969
2970 #[test]
2971 fn trigger_pointer_touch_opens_on_click_like_up() {
2972 let window = AppWindowId::default();
2973 let mut app = App::new();
2974 let open = app.models_mut().insert(false);
2975
2976 let mut state = SelectTriggerPointerState::default();
2977 let mut host = PointerHost {
2978 app: &mut app,
2979 bounds: bounds(),
2980 prevented_focus_on_pointer_down: false,
2981 };
2982
2983 assert!(state.handle_pointer_down(
2984 &mut host,
2985 ActionCx {
2986 window,
2987 target: GlobalElementId(1),
2988 },
2989 PointerDownCx {
2990 pointer_id: fret_core::PointerId(0),
2991 position: Point::new(Px(10.0), Px(12.0)),
2992 position_local: Point::new(Px(10.0), Px(12.0)),
2993 position_window: Some(Point::new(Px(10.0), Px(12.0))),
2994 tick_id: fret_runtime::TickId(0),
2995 pixels_per_point: 1.0,
2996 button: fret_core::MouseButton::Left,
2997 modifiers: Modifiers::default(),
2998 click_count: 1,
2999 pointer_type: PointerType::Touch,
3000 hit_is_text_input: false,
3001 hit_is_pressable: false,
3002 hit_pressable_target: None,
3003 hit_pressable_target_in_descendant_subtree: false,
3004 },
3005 &open,
3006 true,
3007 ));
3008 assert!(!host.models_mut().get_copied(&open).unwrap_or(false));
3009
3010 assert!(state.handle_pointer_up(
3011 &mut host,
3012 ActionCx {
3013 window,
3014 target: GlobalElementId(1),
3015 },
3016 PointerUpCx {
3017 pointer_id: fret_core::PointerId(0),
3018 position: Point::new(Px(13.0), Px(15.0)),
3019 position_local: Point::new(Px(13.0), Px(15.0)),
3020 position_window: Some(Point::new(Px(13.0), Px(15.0))),
3021 tick_id: fret_runtime::TickId(0),
3022 pixels_per_point: 1.0,
3023 velocity_window: None,
3024 button: fret_core::MouseButton::Left,
3025 modifiers: Modifiers::default(),
3026 is_click: true,
3027 click_count: 1,
3028 pointer_type: PointerType::Touch,
3029 down_hit_pressable_target: None,
3030 down_hit_pressable_target_in_descendant_subtree: false,
3031 },
3032 &open,
3033 true,
3034 ));
3035 assert!(host.models_mut().get_copied(&open).unwrap_or(false));
3036 }
3037
3038 #[test]
3039 fn trigger_pointer_touch_drag_does_not_open() {
3040 let window = AppWindowId::default();
3041 let mut app = App::new();
3042 let open = app.models_mut().insert(false);
3043
3044 let mut state = SelectTriggerPointerState::default();
3045 let mut host = PointerHost {
3046 app: &mut app,
3047 bounds: bounds(),
3048 prevented_focus_on_pointer_down: false,
3049 };
3050
3051 assert!(state.handle_pointer_down(
3052 &mut host,
3053 ActionCx {
3054 window,
3055 target: GlobalElementId(1),
3056 },
3057 PointerDownCx {
3058 pointer_id: fret_core::PointerId(0),
3059 position: Point::new(Px(10.0), Px(12.0)),
3060 position_local: Point::new(Px(10.0), Px(12.0)),
3061 position_window: Some(Point::new(Px(10.0), Px(12.0))),
3062 tick_id: fret_runtime::TickId(0),
3063 pixels_per_point: 1.0,
3064 button: fret_core::MouseButton::Left,
3065 modifiers: Modifiers::default(),
3066 click_count: 1,
3067 pointer_type: PointerType::Touch,
3068 hit_is_text_input: false,
3069 hit_is_pressable: false,
3070 hit_pressable_target: None,
3071 hit_pressable_target_in_descendant_subtree: false,
3072 },
3073 &open,
3074 true,
3075 ));
3076 assert!(state.handle_pointer_move(
3077 &mut host,
3078 ActionCx {
3079 window,
3080 target: GlobalElementId(1),
3081 },
3082 PointerMoveCx {
3083 pointer_id: fret_core::PointerId(0),
3084 position: Point::new(Px(40.0), Px(12.0)),
3085 position_local: Point::new(Px(40.0), Px(12.0)),
3086 position_window: Some(Point::new(Px(40.0), Px(12.0))),
3087 tick_id: fret_runtime::TickId(0),
3088 pixels_per_point: 1.0,
3089 velocity_window: None,
3090 buttons: fret_core::MouseButtons {
3091 left: true,
3092 right: false,
3093 middle: false,
3094 },
3095 modifiers: Modifiers::default(),
3096 pointer_type: PointerType::Touch,
3097 },
3098 ));
3099 assert!(state.handle_pointer_up(
3100 &mut host,
3101 ActionCx {
3102 window,
3103 target: GlobalElementId(1),
3104 },
3105 PointerUpCx {
3106 pointer_id: fret_core::PointerId(0),
3107 position: Point::new(Px(40.0), Px(12.0)),
3108 position_local: Point::new(Px(40.0), Px(12.0)),
3109 position_window: Some(Point::new(Px(40.0), Px(12.0))),
3110 tick_id: fret_runtime::TickId(0),
3111 pixels_per_point: 1.0,
3112 velocity_window: None,
3113 button: fret_core::MouseButton::Left,
3114 modifiers: Modifiers::default(),
3115 is_click: false,
3116 click_count: 1,
3117 pointer_type: PointerType::Touch,
3118 down_hit_pressable_target: None,
3119 down_hit_pressable_target_in_descendant_subtree: false,
3120 },
3121 &open,
3122 true,
3123 ));
3124 assert!(!host.models_mut().get_copied(&open).unwrap_or(false));
3125 }
3126}