1use std::cell::{Cell, RefCell};
210use std::rc::Rc;
211
212use frust_core::accesskit::{Action, Role};
213use frust_core::event::PointerSource;
214use frust_core::{
215 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, CursorIcon, EventCtx, EventResult,
216 InputEvent, Key, KeyEvent, LayoutCtx, NamedKey, OverlayBand, OverlayInput, PaintCtx,
217 PaintScene, PointerButton, PointerEvent, PointerPhase, SemanticsCtx, TOUCH_SLOP, View, Widget,
218 any,
219};
220use frust_theme::Theme;
221use kurbo::{Point, Size, Vec2};
222use peniko::Color;
223
224use super::coordinator::{DragCoordinator, DragPhase, DragSourceId, DragState};
225use crate::authoring::presses;
226use crate::gesture::{HoldTracker, LONG_PRESS_MS};
227use crate::overlay::{OverlayAnchor, OverlaySlot};
228use crate::scroll::ambient_scroll_vetoes;
229
230pub const DRAG_THRESHOLD: f64 = 6.0;
240
241pub const GHOST_OPACITY: f32 = 0.85;
245
246const DIM_OPACITY: f32 = 0.38;
250
251const PLACEHOLDER_FILL: Color = Color::from_rgba8(0x00, 0x00, 0x00, 0x14);
254
255const PLACEHOLDER_RADIUS: f64 = 8.0;
258
259#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
262pub enum DragPolicy {
263 Immediate,
266 LongPress,
270 #[default]
274 Auto,
275}
276
277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
279pub enum SourceFeedback {
280 #[default]
282 Dim,
283 Placeholder,
286 None,
288}
289
290type GhostBuilder<State> = Rc<dyn Fn(&State) -> AnyView<State>>;
292
293pub struct DraggableView<State: 'static, T: 'static> {
295 child: AnyView<State>,
296 coordinator: DragCoordinator,
297 payload: Rc<dyn Fn(&State) -> T>,
298 policy: DragPolicy,
299 threshold: f64,
300 hold_threshold_ms: Option<u64>,
301 ghost: Option<GhostBuilder<State>>,
302 ghost_opacity: f32,
303 feedback: SourceFeedback,
304}
305
306pub fn draggable<State, V, T, F>(
313 child: V,
314 coordinator: DragCoordinator,
315 payload: F,
316) -> DraggableView<State, T>
317where
318 State: 'static,
319 V: View<State>,
320 T: 'static,
321 F: Fn(&State) -> T + 'static,
322{
323 DraggableView {
324 child: any(child),
325 coordinator,
326 payload: Rc::new(payload),
327 policy: DragPolicy::Auto,
328 threshold: DRAG_THRESHOLD,
329 hold_threshold_ms: None,
330 ghost: None,
331 ghost_opacity: GHOST_OPACITY,
332 feedback: SourceFeedback::Dim,
333 }
334}
335
336impl<State: 'static, T: 'static> DraggableView<State, T> {
337 pub fn policy(mut self, policy: DragPolicy) -> Self {
339 self.policy = policy;
340 self
341 }
342
343 pub fn threshold(mut self, px: f64) -> Self {
347 self.threshold = px.max(0.0);
348 self
349 }
350
351 pub fn hold_threshold_ms(mut self, ms: u64) -> Self {
355 self.hold_threshold_ms = Some(ms);
356 self
357 }
358
359 pub fn ghost<F, V>(mut self, ghost: F) -> Self
373 where
374 F: Fn(&State) -> V + 'static,
375 V: View<State>,
376 {
377 self.ghost = Some(Rc::new(move |state: &State| any(ghost(state))));
378 self
379 }
380
381 pub fn ghost_opacity(mut self, opacity: f32) -> Self {
384 self.ghost_opacity = opacity.clamp(0.0, 1.0);
385 self
386 }
387
388 pub fn while_dragging(mut self, feedback: SourceFeedback) -> Self {
391 self.feedback = feedback;
392 self
393 }
394
395 fn resolved_hold_ms(&self) -> f64 {
396 self.hold_threshold_ms
397 .map(|ms| ms.max(1) as f64)
398 .unwrap_or(LONG_PRESS_MS)
399 }
400}
401
402#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405enum Initiation {
406 Distance,
407 Hold,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq)]
412enum Gesture {
413 Idle,
415 Pressed {
419 down: Point,
420 last: Point,
421 initiation: Initiation,
422 elapsed: bool,
423 },
424 Declined,
428 Dragging,
430 Ended,
436}
437
438struct Ghost<State: 'static> {
442 pending: Option<AnyView<State>>,
444 mounted: Option<AnyView<State>>,
446 live: bool,
449}
450
451pub struct DraggableWidget<State: 'static, T: 'static> {
453 child: Rc<RefCell<ChildPod>>,
455 coordinator: DragCoordinator,
456 payload: Rc<dyn Fn(&State) -> T>,
457 policy: DragPolicy,
458 threshold: f64,
459 ghost_builder: Option<GhostBuilder<State>>,
460 ghost_opacity: f32,
461 feedback: SourceFeedback,
462 gesture: Gesture,
463 hold: HoldTracker,
465 source: Option<DragSourceId>,
468 slot: OverlaySlot<State>,
469 ghost: Ghost<State>,
470 window_origin: Point,
473 scroll_vetoes: Vec<Rc<Cell<bool>>>,
477 focus_claimed: bool,
480 keyboard_drag: bool,
485}
486
487impl<State: 'static, T: 'static> View<State> for DraggableView<State, T> {
488 type Element = DraggableWidget<State, T>;
489
490 fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
491 let mut slot = OverlaySlot::new();
492 slot.set_band(OverlayBand::Tooltip);
493 slot.set_input(OverlayInput::Transparent);
494 DraggableWidget {
495 child: Rc::new(RefCell::new(crate::authoring::build_child(
496 &self.child,
497 ctx,
498 ))),
499 coordinator: self.coordinator.clone(),
500 payload: Rc::clone(&self.payload),
501 policy: self.policy,
502 threshold: self.threshold,
503 ghost_builder: self.ghost.clone(),
504 ghost_opacity: self.ghost_opacity,
505 feedback: self.feedback,
506 gesture: Gesture::Idle,
507 hold: HoldTracker::new(self.resolved_hold_ms()),
508 source: None,
509 slot,
510 ghost: Ghost {
511 pending: None,
512 mounted: None,
513 live: false,
514 },
515 window_origin: Point::ZERO,
516 scroll_vetoes: Vec::new(),
517 focus_claimed: false,
518 keyboard_drag: false,
519 }
520 }
521
522 fn rebuild(
523 &self,
524 prev: &Self,
525 element: &mut Self::Element,
526 ctx: &mut BuildCtx<'_>,
527 ) -> ChangeFlags {
528 let mut flags = ChangeFlags::NONE;
529 if matches!(element.gesture, Gesture::Idle) {
532 element.coordinator = self.coordinator.clone();
533 }
534 element.payload = Rc::clone(&self.payload);
535 element.policy = self.policy;
536 element.threshold = self.threshold;
537 element.hold.set_threshold_ms(self.resolved_hold_ms());
538 element.ghost_builder = self.ghost.clone();
539 if element.ghost_opacity != self.ghost_opacity || element.feedback != self.feedback {
540 element.ghost_opacity = self.ghost_opacity;
541 element.feedback = self.feedback;
542 flags |= ChangeFlags::PAINT;
543 }
544 flags |= crate::authoring::rebuild_child(
545 &prev.child,
546 &self.child,
547 &mut element.child.borrow_mut(),
548 ctx,
549 );
550 flags | element.sync_ghost(ctx)
551 }
552
553 fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
554 if matches!(element.gesture, Gesture::Pressed { .. } | Gesture::Dragging)
556 && element.owns_session()
557 {
558 element.coordinator.cancel();
559 }
560 element.gesture = Gesture::Idle;
561 element.source = None;
562 element.focus_claimed = false;
564 element.keyboard_drag = false;
565 element.release_scroll_veto();
566 element.ghost.live = false;
567 element.ghost.pending = None;
568 if let Some(mounted) = element.ghost.mounted.take() {
569 element.slot.rebuild(Some(&mounted), None, ctx);
570 }
571 crate::authoring::teardown_child(&self.child, &mut element.child.borrow_mut(), ctx);
572 }
573}
574
575impl<State: 'static, T: 'static> DraggableWidget<State, T> {
576 fn owns_session(&self) -> bool {
579 let Some(source) = self.source else {
580 return false;
581 };
582 match self.coordinator.state() {
583 DragState::Armed { source: armed, .. } => armed == source,
584 DragState::Dragging(session) => session.source == Some(source),
585 DragState::Idle | DragState::Dropping { .. } => false,
586 }
587 }
588
589 fn is_dragging(&self) -> bool {
592 matches!(self.gesture, Gesture::Dragging)
593 && self.coordinator.phase() == DragPhase::Dragging
594 && self.owns_session()
595 }
596
597 fn to_window(&self, local: Point) -> Point {
599 local + self.window_origin.to_vec2()
600 }
601
602 fn set_scroll_veto(&self, raised: bool) {
603 for veto in &self.scroll_vetoes {
604 veto.set(raised);
605 }
606 }
607
608 fn release_scroll_veto(&mut self) {
609 self.set_scroll_veto(false);
610 self.scroll_vetoes.clear();
611 }
612
613 fn end_elsewhere(&mut self) {
624 self.gesture = if self.keyboard_drag {
625 Gesture::Idle
626 } else {
627 Gesture::Ended
628 };
629 self.keyboard_drag = false;
630 self.ghost.live = false;
631 self.release_scroll_veto();
632 }
633
634 fn sync_ghost(&mut self, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
637 if matches!(self.gesture, Gesture::Dragging) && !self.is_dragging() {
640 self.end_elsewhere();
641 }
642 let mut flags = ChangeFlags::NONE;
643 if let Some(next) = self.ghost.pending.take() {
644 if let Some(prev) = self.ghost.mounted.take() {
645 flags |= self.slot.rebuild(Some(&prev), None, ctx);
646 }
647 if self.ghost.live {
648 flags |= self.slot.rebuild(None, Some(&next), ctx);
649 self.ghost.mounted = Some(next);
650 }
651 } else if !self.ghost.live
652 && let Some(prev) = self.ghost.mounted.take()
653 {
654 flags |= self.slot.rebuild(Some(&prev), None, ctx);
655 }
656 flags
657 }
658
659 fn press(&mut self, ctx: &mut EventCtx<'_>, event: &InputEvent, at: Point) -> EventResult {
661 let initiation = match self.policy {
662 DragPolicy::Immediate => Initiation::Distance,
663 DragPolicy::LongPress => Initiation::Hold,
664 DragPolicy::Auto => match ctx.pointer_id().source {
665 PointerSource::Mouse => Initiation::Distance,
666 PointerSource::Touch => Initiation::Hold,
667 },
668 };
669 let source = self.coordinator.new_source_id();
670 if !self.coordinator.arm(source, self.to_window(at)) {
671 self.gesture = Gesture::Declined;
674 return crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event);
675 }
676 self.source = Some(source);
677 self.gesture = Gesture::Pressed {
678 down: at,
679 last: at,
680 initiation,
681 elapsed: false,
682 };
683 self.hold.cancel();
684 self.scroll_vetoes = ambient_scroll_vetoes();
687 ctx.capture_pointer();
688 ctx.request_redraw();
690 let child_took_focus = {
691 let mut child = self.child.borrow_mut();
692 child.event_child(ctx, event);
693 child.holds_live_focus()
694 };
695 if !child_took_focus {
699 ctx.request_focus();
700 self.focus_claimed = true;
701 }
702 EventResult::Handled
703 }
704
705 fn release_focus_claim(&mut self, ctx: &mut EventCtx<'_>) {
707 if std::mem::take(&mut self.focus_claimed) {
708 ctx.release_focus();
709 }
710 }
711
712 fn decline(&mut self) {
715 if self.owns_session() {
716 self.coordinator.cancel();
717 }
718 self.source = None;
719 self.release_scroll_veto();
720 self.gesture = Gesture::Declined;
721 }
722
723 fn begin(&mut self, ctx: &mut EventCtx<'_>, at: Point, down: Point) -> bool {
726 if !self.owns_session() {
727 return false;
728 }
729 let (payload, custom) = {
730 let state: &State = ctx.state_mut::<State>();
731 (
732 (self.payload)(state),
733 self.ghost_builder.as_ref().map(|build| build(state)),
734 )
735 };
736 if !self.coordinator.begin(payload) {
737 return false;
738 }
739 let offset = Point::ZERO - down;
742 let pointer = self.to_window(at);
743 self.coordinator.set_ghost_offset(offset);
744 self.coordinator.update_pointer(pointer);
745 let content = custom.unwrap_or_else(|| {
746 any(SnapshotView {
747 source: Rc::clone(&self.child),
748 })
749 });
750 self.ghost.pending = Some(any(GhostFrame {
751 content,
752 opacity: self.ghost_opacity,
753 }));
754 self.ghost.live = true;
755 self.slot.set_anchor(OverlayAnchor::Window {
756 point: pointer,
757 offset,
758 });
759 {
761 let mut child = self.child.borrow_mut();
762 child.event_child(ctx, &pointer_event(PointerPhase::Cancel, at));
763 ctx.release_captured_child(&mut child);
764 }
765 self.set_scroll_veto(true);
766 self.gesture = Gesture::Dragging;
767 self.keyboard_drag = false;
768 ctx.request_redraw();
769 true
770 }
771
772 fn on_session_end(&mut self, ctx: &mut EventCtx<'_>, release_focus: bool) {
782 self.gesture = Gesture::Idle;
783 self.source = None;
784 self.keyboard_drag = false;
785 self.hold.cancel();
786 self.release_scroll_veto();
787 if release_focus {
788 self.release_focus_claim(ctx);
789 }
790 if self.ghost.live || self.ghost.mounted.is_some() {
791 self.ghost.live = false;
792 frust_core::mark_pending_result_flush();
793 }
794 ctx.request_redraw();
795 }
796
797 fn pointer(
798 &mut self,
799 ctx: &mut EventCtx<'_>,
800 event: &InputEvent,
801 p: &PointerEvent,
802 ) -> EventResult {
803 match (p.phase, self.gesture) {
804 (PointerPhase::Down, _) => {
805 if presses(p) && self.is_keyboard_dragging() {
816 self.keyboard_drop(ctx);
817 self.gesture = Gesture::Ended;
818 return EventResult::Handled;
819 }
820 if !matches!(self.gesture, Gesture::Idle | Gesture::Declined) {
823 if self.owns_session() {
824 self.coordinator.cancel();
825 }
826 self.on_session_end(ctx, true);
827 }
828 self.gesture = Gesture::Idle;
829 if !presses(p) {
830 return crate::authoring::route_event_single(
831 &mut self.child.borrow_mut(),
832 ctx,
833 event,
834 );
835 }
836 self.press(ctx, event, p.position)
837 }
838 (
839 PointerPhase::Move,
840 Gesture::Pressed {
841 down,
842 initiation,
843 elapsed,
844 ..
845 },
846 ) => {
847 let travel = (p.position - down).hypot();
848 let begins = match initiation {
849 Initiation::Distance => travel > self.threshold,
850 Initiation::Hold if travel > TOUCH_SLOP => {
851 self.decline();
852 return self.child.borrow_mut().event_child(ctx, event);
853 }
854 Initiation::Hold => elapsed,
855 };
856 if begins && self.begin(ctx, p.position, down) {
857 return self.drag_move(ctx, p.position);
858 }
859 if begins {
860 self.decline();
862 } else if let Gesture::Pressed { last, .. } = &mut self.gesture {
863 *last = p.position;
864 }
865 self.child.borrow_mut().event_child(ctx, event);
866 EventResult::Handled
867 }
868 (PointerPhase::Move, Gesture::Dragging) => self.drag_move(ctx, p.position),
869 (PointerPhase::Up, Gesture::Pressed { elapsed, .. }) => {
870 if self.owns_session() {
874 self.coordinator.cancel();
875 }
876 let phase = if elapsed {
877 PointerPhase::Cancel
878 } else {
879 PointerPhase::Up
880 };
881 {
882 let mut child = self.child.borrow_mut();
883 child.event_child(ctx, &pointer_event(phase, p.position));
884 child.set_active(false);
885 }
886 self.on_session_end(ctx, elapsed);
890 EventResult::Handled
891 }
892 (PointerPhase::Cancel, Gesture::Pressed { .. }) => {
893 if self.owns_session() {
894 self.coordinator.cancel();
895 }
896 {
897 let mut child = self.child.borrow_mut();
898 child.event_child(ctx, event);
899 child.set_active(false);
900 }
901 self.on_session_end(ctx, true);
902 EventResult::Handled
903 }
904 (PointerPhase::Up, Gesture::Dragging) => {
905 if self.owns_session() {
906 self.coordinator.update_pointer(self.to_window(p.position));
907 self.coordinator.drop();
908 }
909 self.on_session_end(ctx, true);
910 EventResult::Handled
911 }
912 (PointerPhase::Cancel, Gesture::Dragging) => {
913 if self.owns_session() {
914 self.coordinator.cancel();
915 }
916 self.on_session_end(ctx, true);
917 EventResult::Handled
918 }
919 (PointerPhase::Up | PointerPhase::Cancel, Gesture::Ended) => {
920 self.on_session_end(ctx, true);
921 EventResult::Handled
922 }
923 (PointerPhase::Move, Gesture::Ended) => EventResult::Handled,
924 (PointerPhase::Up | PointerPhase::Cancel, Gesture::Declined) => {
925 self.gesture = Gesture::Idle;
926 self.release_focus_claim(ctx);
927 crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
928 }
929 _ => crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event),
931 }
932 }
933
934 fn escape(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
937 if self.owns_session() {
938 self.coordinator.cancel();
939 }
940 self.on_session_end(ctx, true);
941 self.gesture = Gesture::Ended;
944 EventResult::Handled
945 }
946
947 fn is_keyboard_dragging(&self) -> bool {
952 matches!(self.gesture, Gesture::Dragging)
953 && self.owns_session()
954 && matches!(self.coordinator.state(), DragState::Dragging(session) if session.keyboard)
955 }
956
957 fn keyboard_cancel(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
962 if self.owns_session() {
963 self.coordinator.cancel();
964 }
965 self.on_session_end(ctx, true);
966 EventResult::Handled
967 }
968
969 fn keyboard_drop(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
974 if self.owns_session() {
975 self.coordinator.drop();
976 }
977 self.on_session_end(ctx, true);
978 EventResult::Handled
979 }
980
981 fn keyboard_lift(&mut self, ctx: &mut EventCtx<'_>) -> EventResult {
986 let source = self.coordinator.new_source_id();
987 if !self.coordinator.lift(source) {
988 return EventResult::Ignored;
989 }
990 self.source = Some(source);
991 let (payload, custom) = {
992 let state: &State = ctx.state_mut::<State>();
993 (
994 (self.payload)(state),
995 self.ghost_builder.as_ref().map(|build| build(state)),
996 )
997 };
998 if !self.coordinator.begin(payload) {
999 self.source = None;
1000 return EventResult::Ignored;
1001 }
1002 let content = custom.unwrap_or_else(|| {
1003 any(SnapshotView {
1004 source: Rc::clone(&self.child),
1005 })
1006 });
1007 self.ghost.pending = Some(any(GhostFrame {
1008 content,
1009 opacity: self.ghost_opacity,
1010 }));
1011 self.ghost.live = true;
1012 self.slot.set_anchor(OverlayAnchor::Window {
1014 point: self.window_origin,
1015 offset: Vec2::ZERO,
1016 });
1017 self.gesture = Gesture::Dragging;
1018 self.keyboard_drag = true;
1019 ctx.request_focus();
1020 self.focus_claimed = true;
1021 ctx.request_redraw();
1022 EventResult::Handled
1023 }
1024
1025 fn follow_hovered_target(&mut self, ctx: &mut EventCtx<'_>) {
1030 let DragState::Dragging(session) = self.coordinator.state() else {
1031 return;
1032 };
1033 let Some(target) = session.hovered else {
1034 return;
1035 };
1036 let Some(bounds) = self.coordinator.target_bounds(target) else {
1037 return;
1038 };
1039 self.slot.set_window_anchor(ctx, bounds.origin());
1040 }
1041
1042 fn keyboard_chord(&mut self, ctx: &mut EventCtx<'_>, key: &KeyEvent) -> Option<EventResult> {
1048 if !ctx.has_focus() || self.child.borrow().holds_live_focus() {
1049 return None;
1050 }
1051 let activates = key.key == Key::Named(NamedKey::Enter)
1052 || matches!(&key.key, Key::Character(s) if s == " ");
1053 if matches!(self.gesture, Gesture::Idle) {
1054 return activates.then(|| self.keyboard_lift(ctx));
1055 }
1056 if !self.is_keyboard_dragging() {
1057 return None;
1058 }
1059 match &key.key {
1060 Key::Named(NamedKey::ArrowRight) | Key::Named(NamedKey::ArrowDown) => {
1061 self.coordinator.move_to_next_target();
1062 self.follow_hovered_target(ctx);
1063 ctx.request_redraw();
1064 Some(EventResult::Handled)
1065 }
1066 Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowUp) => {
1067 self.coordinator.move_to_previous_target();
1068 self.follow_hovered_target(ctx);
1069 ctx.request_redraw();
1070 Some(EventResult::Handled)
1071 }
1072 _ if activates => Some(self.keyboard_drop(ctx)),
1073 _ => None,
1074 }
1075 }
1076
1077 fn drag_move(&mut self, ctx: &mut EventCtx<'_>, at: Point) -> EventResult {
1079 if !self.is_dragging() {
1080 self.end_elsewhere();
1082 frust_core::mark_pending_result_flush();
1083 ctx.request_redraw();
1084 return EventResult::Handled;
1085 }
1086 let pointer = self.to_window(at);
1087 self.coordinator.update_pointer(pointer);
1088 self.slot.set_window_anchor(ctx, pointer);
1089 ctx.set_cursor(CursorIcon::Grabbing);
1090 EventResult::Handled
1091 }
1092
1093 fn paint_source(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1096 let feedback = if self.is_dragging() {
1097 self.feedback
1098 } else {
1099 SourceFeedback::None
1100 };
1101 let mut child = self.child.borrow_mut();
1102 match feedback {
1103 SourceFeedback::None => child.paint_child(ctx, scene),
1104 SourceFeedback::Dim => {
1105 scene.push_layer(ctx.origin(), ctx.size(), DIM_OPACITY);
1106 child.paint_child(ctx, scene);
1107 scene.pop_layer();
1108 }
1109 SourceFeedback::Placeholder => {
1110 let (fill, radius) = match Theme::from_paint_ctx(ctx) {
1111 Some(theme) => (theme.scheme().surface_container_highest, theme.shape.small),
1112 None => (PLACEHOLDER_FILL, PLACEHOLDER_RADIUS),
1113 };
1114 scene.fill_rounded_rect(ctx.origin(), ctx.size(), radius, fill);
1115 }
1116 }
1117 }
1118}
1119
1120impl<State: 'static, T: 'static> Widget for DraggableWidget<State, T> {
1121 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1122 let size = {
1123 let mut child = self.child.borrow_mut();
1124 let size = child.layout_child(ctx, bc);
1125 child.set_origin(Point::ZERO);
1126 size
1127 };
1128 self.slot.layout(ctx);
1130 bc.constrain(size)
1131 }
1132
1133 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1134 self.window_origin = ctx.origin();
1135 if let Gesture::Pressed {
1140 initiation: Initiation::Hold,
1141 elapsed,
1142 ..
1143 } = &mut self.gesture
1144 && !*elapsed
1145 {
1146 if self.hold.fired(ctx.frame_time()) {
1147 *elapsed = true;
1148 frust_core::mark_pending_result_flush();
1149 }
1150 ctx.request_frame();
1151 }
1152 self.paint_source(ctx, scene);
1153 if self.ghost.live {
1154 let size = ctx.size();
1155 self.slot.paint(ctx, size);
1156 }
1157 }
1158
1159 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1160 if event.is_broadcast() {
1161 if let Gesture::Pressed {
1164 initiation: Initiation::Hold,
1165 elapsed: true,
1166 down,
1167 last,
1168 } = self.gesture
1169 && !self.begin(ctx, last, down)
1170 {
1171 self.decline();
1172 }
1173 self.child.borrow_mut().event_child(ctx, event);
1174 return EventResult::Ignored;
1175 }
1176 if let InputEvent::Pointer(p) = event {
1177 return self.pointer(ctx, event, p);
1178 }
1179 if let InputEvent::Key(key) = event {
1180 if key.key == Key::Named(NamedKey::Escape) && matches!(self.gesture, Gesture::Dragging)
1181 {
1182 return if self.is_keyboard_dragging() {
1183 self.keyboard_cancel(ctx)
1184 } else {
1185 self.escape(ctx)
1186 };
1187 }
1188 if let Some(result) = self.keyboard_chord(ctx, key) {
1189 return result;
1190 }
1191 }
1192 crate::authoring::route_event_single(&mut self.child.borrow_mut(), ctx, event)
1193 }
1194
1195 fn semantics(&self, ctx: &mut SemanticsCtx) {
1196 let keyboard_dragging = self.is_keyboard_dragging();
1202 let dragging = self.is_dragging();
1203 let label = if keyboard_dragging {
1204 "Drop"
1205 } else if dragging {
1206 "Dragging"
1207 } else {
1208 "Draggable"
1209 };
1210 ctx.push_container(
1211 Role::Button,
1212 |node| {
1213 node.add_action(Action::Click);
1214 node.set_label(label);
1215 if !dragging {
1216 node.set_description("Press Enter to lift");
1217 }
1218 },
1219 |ctx| self.child.borrow().semantics_child(ctx),
1220 );
1221 }
1222
1223 fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1224 visitor(&self.child.borrow());
1225 }
1226}
1227
1228fn pointer_event(phase: PointerPhase, at: Point) -> InputEvent {
1230 InputEvent::Pointer(PointerEvent {
1231 phase,
1232 position: at,
1233 button: PointerButton::Primary,
1234 })
1235}
1236
1237struct GhostFrame<State: 'static> {
1243 content: AnyView<State>,
1244 opacity: f32,
1245}
1246
1247struct GhostFrameWidget {
1248 content: ChildPod,
1249 opacity: f32,
1250}
1251
1252impl<State: 'static> View<State> for GhostFrame<State> {
1253 type Element = GhostFrameWidget;
1254
1255 fn build(&self, ctx: &mut BuildCtx<'_>) -> GhostFrameWidget {
1256 GhostFrameWidget {
1257 content: crate::authoring::build_child(&self.content, ctx),
1258 opacity: self.opacity,
1259 }
1260 }
1261
1262 fn rebuild(
1263 &self,
1264 prev: &Self,
1265 element: &mut GhostFrameWidget,
1266 ctx: &mut BuildCtx<'_>,
1267 ) -> ChangeFlags {
1268 element.opacity = self.opacity;
1269 crate::authoring::rebuild_child(&prev.content, &self.content, &mut element.content, ctx)
1270 }
1271
1272 fn teardown(&self, element: &mut GhostFrameWidget, ctx: &mut BuildCtx<'_>) {
1273 crate::authoring::teardown_child(&self.content, &mut element.content, ctx);
1274 }
1275}
1276
1277impl Widget for GhostFrameWidget {
1278 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1279 let size = self.content.layout_child(ctx, bc);
1280 self.content.set_origin(Point::ZERO);
1281 bc.constrain(size)
1282 }
1283
1284 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1285 if self.opacity >= 1.0 {
1286 self.content.paint_child(ctx, scene);
1287 return;
1288 }
1289 scene.push_layer(ctx.origin(), ctx.size(), self.opacity);
1290 self.content.paint_child(ctx, scene);
1291 scene.pop_layer();
1292 }
1293
1294 crate::authoring::visit_children!(content);
1295}
1296
1297struct SnapshotView {
1300 source: Rc<RefCell<ChildPod>>,
1301}
1302
1303struct SnapshotWidget {
1304 source: Rc<RefCell<ChildPod>>,
1305}
1306
1307impl<State: 'static> View<State> for SnapshotView {
1308 type Element = SnapshotWidget;
1309
1310 fn build(&self, _ctx: &mut BuildCtx<'_>) -> SnapshotWidget {
1311 SnapshotWidget {
1312 source: Rc::clone(&self.source),
1313 }
1314 }
1315
1316 fn rebuild(
1317 &self,
1318 _prev: &Self,
1319 element: &mut SnapshotWidget,
1320 _ctx: &mut BuildCtx<'_>,
1321 ) -> ChangeFlags {
1322 element.source = Rc::clone(&self.source);
1323 ChangeFlags::NONE
1324 }
1325}
1326
1327impl Widget for SnapshotWidget {
1328 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1329 bc.constrain(self.source.borrow().size())
1332 }
1333
1334 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1335 if let Ok(mut source) = self.source.try_borrow_mut() {
1339 source.paint_child(ctx, scene);
1340 }
1341 }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346 use super::*;
1347 use crate::test_support::RecordingScene;
1348 use crate::{EdgeInsets, Padding, StackView, stack};
1349 use frust_core::event::PointerId;
1350 use frust_core::{FrameTime, RenderRoot};
1351 use kurbo::Rect;
1352
1353 use crate::drag::{DragStateChange, DragTargetId};
1354
1355 const WINDOW: Size = Size::new(400.0, 600.0);
1356 const AT: Vec2 = Vec2::new(10.0, 100.0);
1358 const CHILD: Size = Size::new(100.0, 40.0);
1359
1360 type Log = Rc<RefCell<Vec<String>>>;
1361
1362 #[derive(Clone, Copy)]
1363 struct Cfg {
1364 policy: DragPolicy,
1365 feedback: SourceFeedback,
1366 custom_ghost: bool,
1367 in_scroll: bool,
1369 in_nested_scroll: bool,
1373 focusable_child: bool,
1375 }
1376
1377 impl Default for Cfg {
1378 fn default() -> Self {
1379 Cfg {
1380 policy: DragPolicy::Auto,
1381 feedback: SourceFeedback::Dim,
1382 custom_ghost: false,
1383 in_scroll: false,
1384 in_nested_scroll: false,
1385 focusable_child: false,
1386 }
1387 }
1388 }
1389
1390 struct App {
1391 cfg: Cfg,
1392 coordinator: DragCoordinator,
1393 log: Log,
1394 taps: u32,
1395 item: u32,
1396 }
1397
1398 struct Probe {
1403 size: Size,
1404 log: Log,
1405 focusable: bool,
1406 }
1407
1408 struct ProbeWidget {
1409 size: Size,
1410 log: Log,
1411 focusable: bool,
1412 }
1413
1414 impl View<App> for Probe {
1415 type Element = ProbeWidget;
1416 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
1417 ProbeWidget {
1418 size: self.size,
1419 log: Rc::clone(&self.log),
1420 focusable: self.focusable,
1421 }
1422 }
1423 fn rebuild(
1424 &self,
1425 _prev: &Self,
1426 element: &mut ProbeWidget,
1427 _ctx: &mut BuildCtx<'_>,
1428 ) -> ChangeFlags {
1429 element.size = self.size;
1430 element.log = Rc::clone(&self.log);
1431 element.focusable = self.focusable;
1432 ChangeFlags::NONE
1433 }
1434 }
1435
1436 impl Widget for ProbeWidget {
1437 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1438 bc.constrain(self.size)
1439 }
1440 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1441 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1442 }
1443 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1444 if let InputEvent::Key(key) = event {
1445 self.log.borrow_mut().push(format!("Key {:?}", key.key));
1446 return EventResult::Handled;
1447 }
1448 let InputEvent::Pointer(p) = event else {
1449 return EventResult::Ignored;
1450 };
1451 self.log
1452 .borrow_mut()
1453 .push(format!("{:?}@{},{}", p.phase, p.position.x, p.position.y));
1454 match p.phase {
1455 PointerPhase::Down => {
1456 ctx.capture_pointer();
1457 if self.focusable && presses(p) {
1458 ctx.request_focus();
1459 }
1460 }
1461 PointerPhase::Up => {
1462 let inside =
1463 Rect::from_origin_size(Point::ZERO, ctx.size()).contains(p.position);
1464 if inside {
1465 ctx.state_mut::<App>().taps += 1;
1466 }
1467 }
1468 PointerPhase::Move | PointerPhase::Cancel => {}
1469 }
1470 EventResult::Handled
1471 }
1472 }
1473
1474 fn logic(state: &mut App) -> StackView<App> {
1475 let cfg = state.cfg;
1476 let mut source = draggable(
1477 Probe {
1478 size: CHILD,
1479 log: Rc::clone(&state.log),
1480 focusable: cfg.focusable_child,
1481 },
1482 state.coordinator.clone(),
1483 |state: &App| state.item,
1484 )
1485 .policy(cfg.policy)
1486 .while_dragging(cfg.feedback);
1487 if cfg.custom_ghost {
1488 source = source.ghost(|_: &App| crate::SizedBox(Some(30.0), Some(20.0)));
1489 }
1490 let placed = any(Padding(
1491 EdgeInsets {
1492 left: AT.x,
1493 top: AT.y,
1494 right: 0.0,
1495 bottom: 0.0,
1496 },
1497 source,
1498 ));
1499 if cfg.in_scroll {
1500 return stack().child(crate::scroll_view(
1501 stack()
1502 .child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
1503 .child(placed),
1504 ));
1505 }
1506 if cfg.in_nested_scroll {
1507 let inner = any(crate::scroll_view(stack().child(placed)));
1514 return stack().child(crate::scroll_view(
1515 stack()
1516 .child(crate::SizedBox(Some(WINDOW.width), Some(2_000.0)))
1517 .child(inner),
1518 ));
1519 }
1520 stack().child(placed)
1521 }
1522
1523 struct Harness {
1524 root: RenderRoot<App, StackView<App>>,
1525 state: App,
1526 clock_ms: f64,
1527 changes: Rc<RefCell<Vec<DragStateChange>>>,
1528 _subscription: crate::drag::DragSubscription,
1529 }
1530
1531 impl Harness {
1532 fn new(cfg: Cfg) -> Self {
1533 let coordinator = DragCoordinator::new();
1534 let changes = Rc::new(RefCell::new(Vec::new()));
1535 let sink = Rc::clone(&changes);
1536 let subscription = coordinator.subscribe(move |change| sink.borrow_mut().push(change));
1537 let mut harness = Harness {
1538 root: RenderRoot::new(),
1539 state: App {
1540 cfg,
1541 coordinator,
1542 log: Rc::new(RefCell::new(Vec::new())),
1543 taps: 0,
1544 item: 7,
1545 },
1546 clock_ms: 0.0,
1547 changes,
1548 _subscription: subscription,
1549 };
1550 harness.frame();
1551 harness
1552 }
1553
1554 fn frame(&mut self) -> RecordingScene {
1556 let mut build: fn(&mut App) -> StackView<App> = logic;
1557 self.root.rebuild(&mut build, &mut self.state);
1558 self.root.layout(WINDOW);
1559 self.clock_ms += 16.0;
1560 let mut scene = RecordingScene::default();
1561 self.root.paint(
1562 &mut scene,
1563 FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
1564 );
1565 scene
1566 }
1567
1568 fn mouse(&mut self, phase: PointerPhase, x: f64, y: f64) {
1569 self.root
1570 .event(&mut self.state, &pointer_event(phase, Point::new(x, y)));
1571 }
1572
1573 fn touch(&mut self, phase: PointerPhase, x: f64, y: f64) {
1574 self.root.event(
1575 &mut self.state,
1576 &InputEvent::PointerContact {
1577 pointer_id: PointerId::touch(0),
1578 event: PointerEvent {
1579 phase,
1580 position: Point::new(x, y),
1581 button: PointerButton::Primary,
1582 },
1583 },
1584 );
1585 }
1586
1587 fn phase(&self) -> DragPhase {
1588 self.state.coordinator.phase()
1589 }
1590
1591 fn key(&mut self, key: Key) {
1592 self.root.event(
1593 &mut self.state,
1594 &InputEvent::Key(frust_core::KeyEvent {
1595 key,
1596 modifiers: frust_core::Modifiers::default(),
1597 repeat: false,
1598 }),
1599 );
1600 }
1601
1602 fn escape(&mut self) {
1603 self.key(Key::Named(NamedKey::Escape));
1604 }
1605
1606 fn target(&mut self, bounds: Rect) -> DragTargetId {
1609 let drag = &self.state.coordinator;
1610 let id = drag.new_target_id();
1611 drag.register_target(id);
1612 drag.set_target_bounds(id, bounds);
1613 id
1614 }
1615
1616 fn log(&self) -> Vec<String> {
1617 self.state.log.borrow().clone()
1618 }
1619
1620 fn mouse_drag(&mut self) {
1623 self.mouse(PointerPhase::Down, 30.0, 120.0);
1624 self.mouse(PointerPhase::Move, 60.0, 150.0);
1625 }
1626 }
1627
1628 fn rects(scene: &RecordingScene) -> Vec<Rect> {
1629 scene
1630 .rects
1631 .iter()
1632 .map(|(origin, size)| Rect::from_origin_size(*origin, *size))
1633 .collect()
1634 }
1635
1636 fn rest() -> Rect {
1637 Rect::from_origin_size(Point::ZERO + AT, CHILD)
1638 }
1639
1640 #[test]
1641 fn a_mouse_drag_begins_past_the_threshold_and_not_before() {
1642 let mut h = Harness::new(Cfg::default());
1643 h.mouse(PointerPhase::Down, 30.0, 120.0);
1644 assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
1645 h.mouse(PointerPhase::Move, 34.0, 122.0);
1646 assert_eq!(
1647 h.phase(),
1648 DragPhase::Armed,
1649 "4.5 px is inside the 6 px threshold"
1650 );
1651 h.mouse(PointerPhase::Move, 37.0, 120.0);
1652 assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
1653 assert_eq!(
1654 h.state.coordinator.with_payload(|item: &u32| *item),
1655 Some(7),
1656 "the payload builder ran against the state at begin"
1657 );
1658 let session = h.state.coordinator.state();
1659 let session = session.session().expect("a session is in flight");
1660 assert_eq!(session.pointer, Point::new(37.0, 120.0));
1661 assert_eq!(session.ghost_offset, Vec2::new(-20.0, -20.0));
1662 }
1663
1664 #[test]
1665 fn a_short_click_passes_through_to_the_child() {
1666 let mut h = Harness::new(Cfg::default());
1667 h.mouse(PointerPhase::Down, 30.0, 120.0);
1668 h.mouse(PointerPhase::Move, 32.0, 121.0);
1669 h.mouse(PointerPhase::Up, 32.0, 121.0);
1670 assert_eq!(h.state.taps, 1, "the child saw the whole click");
1671 assert_eq!(h.log(), vec!["Down@20,20", "Move@22,21", "Up@22,21"]);
1672 assert_eq!(h.phase(), DragPhase::Idle, "the arm was released");
1673 assert!(!h.root.is_pointer_captured());
1674 }
1675
1676 #[test]
1677 fn a_short_tap_on_touch_passes_through_to_the_child() {
1678 let mut h = Harness::new(Cfg::default());
1679 h.touch(PointerPhase::Down, 30.0, 120.0);
1680 h.frame();
1681 h.touch(PointerPhase::Up, 30.0, 120.0);
1682 assert_eq!(h.state.taps, 1);
1683 assert_eq!(h.phase(), DragPhase::Idle);
1684 }
1685
1686 #[test]
1687 fn beginning_a_drag_cancels_the_childs_press() {
1688 let mut h = Harness::new(Cfg::default());
1689 h.mouse_drag();
1690 h.mouse(PointerPhase::Up, 60.0, 150.0);
1691 assert_eq!(h.state.taps, 0, "a drag is never also a tap");
1692 assert_eq!(h.log(), vec!["Down@20,20", "Cancel@50,50"]);
1693 }
1694
1695 #[test]
1696 fn a_touch_long_press_begins_the_drag() {
1697 let mut h = Harness::new(Cfg::default());
1698 h.touch(PointerPhase::Down, 30.0, 120.0);
1699 h.touch(PointerPhase::Move, 40.0, 120.0);
1702 assert_eq!(h.phase(), DragPhase::Armed);
1703 let mut frames = 0;
1704 while h.phase() != DragPhase::Dragging {
1705 h.frame();
1706 frames += 1;
1707 assert!(frames < 60, "the hold never began a drag");
1708 }
1709 assert!(
1712 frames >= 31,
1713 "began after {frames} frames — before the hold elapsed"
1714 );
1715 assert_eq!(
1716 h.state.coordinator.state().session().map(|s| s.pointer),
1717 Some(Point::new(40.0, 120.0)),
1718 "the drag starts where the finger is held"
1719 );
1720 assert_eq!(h.log().last().map(String::as_str), Some("Cancel@30,20"));
1721 }
1722
1723 #[test]
1724 fn an_enclosing_scroll_view_does_not_steal_a_drag_in_flight() {
1725 let mut h = Harness::new(Cfg {
1726 in_scroll: true,
1727 ..Cfg::default()
1728 });
1729 h.touch(PointerPhase::Down, 30.0, 120.0);
1730 let mut frames = 0;
1731 while h.phase() != DragPhase::Dragging {
1732 h.frame();
1733 frames += 1;
1734 assert!(frames < 60, "the hold never began a drag");
1735 }
1736 h.touch(PointerPhase::Move, 30.0, 220.0);
1738 h.touch(PointerPhase::Move, 30.0, 320.0);
1739 assert_eq!(
1740 h.phase(),
1741 DragPhase::Dragging,
1742 "the scroll view took nothing over"
1743 );
1744 assert_eq!(
1745 h.state.coordinator.state().session().map(|s| s.pointer),
1746 Some(Point::new(30.0, 320.0))
1747 );
1748 let scene = h.frame();
1749 assert_eq!(
1750 rects(&scene).first(),
1751 Some(&rest()),
1752 "the content did not scroll under the drag"
1753 );
1754 h.touch(PointerPhase::Up, 30.0, 320.0);
1755 assert_eq!(h.phase(), DragPhase::Idle);
1756 }
1757
1758 #[test]
1759 fn a_drag_vetoes_every_enclosing_scroll_surface_not_only_the_nearest() {
1760 let mut h = Harness::new(Cfg {
1770 in_nested_scroll: true,
1771 ..Cfg::default()
1772 });
1773 let target = h.target(Rect::new(0.0, 300.0, WINDOW.width, WINDOW.height));
1774 h.mouse(PointerPhase::Down, 30.0, 120.0);
1775 assert_eq!(h.phase(), DragPhase::Armed, "a primary press arms");
1776 h.mouse(PointerPhase::Move, 37.0, 120.0);
1779 assert_eq!(h.phase(), DragPhase::Dragging, "7 px crosses it");
1780 h.mouse(PointerPhase::Move, 30.0, 320.0);
1784 assert_eq!(
1785 h.phase(),
1786 DragPhase::Dragging,
1787 "neither enclosing scroll surface took the gesture over at its own slop"
1788 );
1789 assert_eq!(
1790 h.state
1791 .coordinator
1792 .state()
1793 .session()
1794 .and_then(|s| s.hovered),
1795 Some(target),
1796 "the drag reached a target the inner surface doesn't even cover"
1797 );
1798 let scene = h.frame();
1799 assert_eq!(
1800 rects(&scene).first(),
1801 Some(&rest()),
1802 "neither surface scrolled its content under the drag"
1803 );
1804 h.mouse(PointerPhase::Up, 30.0, 320.0);
1805 assert_eq!(
1806 h.phase(),
1807 DragPhase::Dropping,
1808 "the coordinator reached Dropping on the target outside the inner surface"
1809 );
1810 h.state.coordinator.complete_drop();
1811 assert_eq!(h.phase(), DragPhase::Idle, "the drop completed");
1812 }
1813
1814 #[test]
1815 fn a_touch_press_that_moves_before_the_hold_is_the_childs() {
1816 let mut h = Harness::new(Cfg::default());
1817 h.touch(PointerPhase::Down, 30.0, 120.0);
1818 h.frame();
1819 h.touch(PointerPhase::Move, 30.0, 160.0);
1820 assert_eq!(
1821 h.phase(),
1822 DragPhase::Idle,
1823 "travel past the slop gives it up"
1824 );
1825 for _ in 0..40 {
1826 h.frame();
1827 }
1828 assert_eq!(h.phase(), DragPhase::Idle, "no later hold revives it");
1829 h.touch(PointerPhase::Up, 30.0, 160.0);
1830 assert_eq!(h.log(), vec!["Down@20,20", "Move@20,60", "Up@20,60"]);
1831 }
1832
1833 #[test]
1834 fn an_explicit_policy_overrides_the_device() {
1835 let mut h = Harness::new(Cfg {
1836 policy: DragPolicy::LongPress,
1837 ..Cfg::default()
1838 });
1839 h.mouse(PointerPhase::Down, 30.0, 120.0);
1840 h.mouse(PointerPhase::Move, 40.0, 120.0);
1841 assert_eq!(h.phase(), DragPhase::Armed, "a long-press mouse drag waits");
1842
1843 let mut h = Harness::new(Cfg {
1844 policy: DragPolicy::Immediate,
1845 ..Cfg::default()
1846 });
1847 h.touch(PointerPhase::Down, 30.0, 120.0);
1848 h.touch(PointerPhase::Move, 40.0, 120.0);
1849 assert_eq!(
1850 h.phase(),
1851 DragPhase::Dragging,
1852 "an immediate touch drag doesn't"
1853 );
1854 }
1855
1856 #[test]
1857 fn the_ghost_opens_and_closes_with_the_session() {
1858 let mut h = Harness::new(Cfg::default());
1859 let idle = h.frame();
1860 assert_eq!(rects(&idle), vec![rest()], "no ghost before a drag");
1861
1862 h.mouse_drag();
1863 let dragging = h.frame();
1864 let ghost = Rect::from_origin_size(Point::new(40.0, 130.0), CHILD);
1867 assert_eq!(rects(&dragging), vec![rest(), ghost]);
1868 assert!(
1869 dragging
1870 .layers
1871 .contains(&(ghost.origin(), CHILD, GHOST_OPACITY)),
1872 "{:?}",
1873 dragging.layers
1874 );
1875
1876 h.mouse(PointerPhase::Move, 200.0, 400.0);
1877 let moved = h.frame();
1878 assert_eq!(
1879 rects(&moved).last().copied(),
1880 Some(Rect::from_origin_size(Point::new(180.0, 380.0), CHILD)),
1881 "the ghost follows the pointer"
1882 );
1883
1884 h.mouse(PointerPhase::Up, 200.0, 400.0);
1885 let ended = h.frame();
1886 assert_eq!(
1887 rects(&ended),
1888 vec![rest()],
1889 "the ghost closed with the session"
1890 );
1891 assert!(ended.layers.is_empty());
1892 }
1893
1894 #[test]
1895 fn the_pointer_stays_captured_for_the_whole_drag() {
1896 let mut h = Harness::new(Cfg::default());
1897 h.mouse_drag();
1898 h.frame();
1899 assert!(h.root.is_pointer_captured());
1900 h.mouse(PointerPhase::Move, 390.0, 590.0);
1902 assert_eq!(
1903 h.state.coordinator.state().session().map(|s| s.pointer),
1904 Some(Point::new(390.0, 590.0))
1905 );
1906 assert!(h.root.is_pointer_captured());
1907 h.mouse(PointerPhase::Up, 390.0, 590.0);
1908 assert!(!h.root.is_pointer_captured(), "released on the physical Up");
1909 }
1910
1911 #[test]
1912 fn releasing_over_a_target_drops_on_it() {
1913 let mut h = Harness::new(Cfg::default());
1914 let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
1915 h.mouse_drag();
1916 assert_eq!(
1917 h.state
1918 .coordinator
1919 .state()
1920 .session()
1921 .and_then(|s| s.hovered),
1922 None,
1923 "(60, 150) is outside the target"
1924 );
1925 h.mouse(PointerPhase::Move, 75.0, 155.0);
1926 assert_eq!(
1927 h.state
1928 .coordinator
1929 .state()
1930 .session()
1931 .and_then(|s| s.hovered),
1932 Some(target),
1933 "the move resolved the target under the pointer"
1934 );
1935 h.mouse(PointerPhase::Up, 80.0, 160.0);
1936 match h.state.coordinator.state() {
1937 DragState::Dropping { session, target: t } => {
1938 assert_eq!(t, target);
1939 assert_eq!(session.pointer, Point::new(80.0, 160.0));
1940 }
1941 other => panic!("expected a drop, got {other:?}"),
1942 }
1943 }
1944
1945 #[test]
1946 fn releasing_outside_every_target_cancels() {
1947 let mut h = Harness::new(Cfg::default());
1948 let target = h.target(Rect::new(70.0, 150.0, 170.0, 250.0));
1949 h.mouse_drag();
1950 h.mouse(PointerPhase::Move, 75.0, 155.0);
1951 h.mouse(PointerPhase::Up, 200.0, 160.0);
1953 assert_eq!(h.phase(), DragPhase::Idle);
1954 let changes = h.changes.borrow();
1955 let tail: Vec<_> = changes.iter().rev().take(4).rev().copied().collect();
1956 assert_eq!(
1957 tail,
1958 vec![
1959 DragStateChange::Leave { target },
1960 DragStateChange::Move {
1961 pointer: Point::new(200.0, 160.0)
1962 },
1963 DragStateChange::Phase {
1964 previous: DragPhase::Dragging,
1965 next: DragPhase::Cancelled,
1966 },
1967 DragStateChange::Phase {
1968 previous: DragPhase::Cancelled,
1969 next: DragPhase::Idle,
1970 },
1971 ]
1972 );
1973 }
1974
1975 #[test]
1976 fn escape_cancels_the_drag_and_restores_focus() {
1977 let mut h = Harness::new(Cfg::default());
1978 assert!(
1979 !h.root.is_focus_active(),
1980 "nothing focused before the press"
1981 );
1982 let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
1983 h.mouse_drag();
1984 h.frame();
1985 assert!(
1986 h.root.is_focus_active(),
1987 "the press holds focus for the source"
1988 );
1989 assert_eq!(
1990 h.state
1991 .coordinator
1992 .state()
1993 .session()
1994 .and_then(|s| s.hovered),
1995 Some(target)
1996 );
1997 h.escape();
1998 assert_eq!(h.phase(), DragPhase::Idle, "Escape cancelled the session");
1999 assert!(h.changes.borrow().contains(&DragStateChange::Phase {
2000 previous: DragPhase::Dragging,
2001 next: DragPhase::Cancelled,
2002 }));
2003 assert!(
2004 h.changes
2005 .borrow()
2006 .contains(&DragStateChange::Leave { target }),
2007 "the hovered target heard Leave, not a drop"
2008 );
2009 assert!(
2010 !h.root.is_focus_active(),
2011 "focus is back to what it was before the press"
2012 );
2013 let after = h.frame();
2014 assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2015
2016 h.state.log.borrow_mut().clear();
2018 h.mouse(PointerPhase::Move, 32.0, 122.0);
2019 h.mouse(PointerPhase::Up, 32.0, 122.0);
2020 assert!(h.log().is_empty(), "{:?}", h.log());
2021 assert_eq!(h.state.taps, 0);
2022 assert!(!h.root.is_pointer_captured());
2023
2024 h.mouse(PointerPhase::Down, 30.0, 120.0);
2026 h.mouse(PointerPhase::Up, 30.0, 120.0);
2027 assert_eq!(h.state.taps, 1);
2028 }
2029
2030 #[test]
2031 fn escape_cancels_through_a_child_that_holds_focus_and_leaves_it_focused() {
2032 let mut h = Harness::new(Cfg {
2033 focusable_child: true,
2034 ..Cfg::default()
2035 });
2036 h.mouse_drag();
2037 assert!(h.root.is_focus_active());
2038 h.escape();
2039 assert_eq!(h.phase(), DragPhase::Idle);
2040 assert!(
2041 !h.log().iter().any(|line| line.starts_with("Key")),
2042 "the source consumed Escape: {:?}",
2043 h.log()
2044 );
2045 h.mouse(PointerPhase::Up, 60.0, 150.0);
2046 assert!(h.root.is_focus_active(), "the child is still focused");
2048 h.key(Key::Character("a".into()));
2049 assert_eq!(
2050 h.log().last().map(String::as_str),
2051 Some("Key Character(\"a\")"),
2052 "keys still reach the focused child"
2053 );
2054 }
2055
2056 #[test]
2057 fn escape_outside_a_drag_reaches_the_child() {
2058 let mut h = Harness::new(Cfg {
2059 focusable_child: true,
2060 ..Cfg::default()
2061 });
2062 h.mouse(PointerPhase::Down, 30.0, 120.0);
2063 h.mouse(PointerPhase::Up, 30.0, 120.0);
2064 h.escape();
2065 assert_eq!(
2066 h.log().last().map(String::as_str),
2067 Some("Key Named(Escape)")
2068 );
2069 }
2070
2071 #[test]
2072 fn a_drop_releases_the_focus_path_the_press_claimed() {
2073 let mut h = Harness::new(Cfg::default());
2074 h.mouse_drag();
2075 h.mouse(PointerPhase::Up, 60.0, 150.0);
2076 assert_eq!(h.phase(), DragPhase::Idle);
2077 h.escape();
2080 assert!(!h.log().iter().any(|line| line.starts_with("Key")));
2081 h.mouse(PointerPhase::Down, 300.0, 500.0);
2083 h.mouse(PointerPhase::Up, 300.0, 500.0);
2084 assert!(!h.root.is_focus_active());
2085 }
2086
2087 #[test]
2088 fn a_pointer_cancel_abandons_the_session() {
2089 let mut h = Harness::new(Cfg::default());
2090 h.mouse_drag();
2091 h.frame();
2092 h.mouse(PointerPhase::Cancel, 60.0, 150.0);
2093 assert_eq!(h.phase(), DragPhase::Idle);
2094 assert!(h.changes.borrow().contains(&DragStateChange::Phase {
2095 previous: DragPhase::Dragging,
2096 next: DragPhase::Cancelled,
2097 }));
2098 assert!(!h.root.is_pointer_captured());
2099 let after = h.frame();
2100 assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2101 assert_eq!(h.state.taps, 0);
2102 }
2103
2104 #[test]
2105 fn a_session_cancelled_elsewhere_closes_the_ghost_and_swallows_the_rest() {
2106 let mut h = Harness::new(Cfg::default());
2107 h.mouse_drag();
2108 h.frame();
2109 h.state.coordinator.cancel();
2110 let after = h.frame();
2111 assert_eq!(rects(&after), vec![rest()], "closed on the next frame");
2112 h.state.log.borrow_mut().clear();
2113 h.mouse(PointerPhase::Move, 90.0, 160.0);
2114 h.mouse(PointerPhase::Up, 90.0, 160.0);
2115 assert!(
2116 h.log().is_empty(),
2117 "the child hears nothing more: {:?}",
2118 h.log()
2119 );
2120 assert_eq!(h.phase(), DragPhase::Idle);
2121
2122 h.mouse(PointerPhase::Down, 30.0, 120.0);
2124 h.mouse(PointerPhase::Up, 30.0, 120.0);
2125 assert_eq!(h.state.taps, 1);
2126 }
2127
2128 #[test]
2129 fn dim_feedback_paints_the_source_under_a_dim_layer() {
2130 let mut h = Harness::new(Cfg::default());
2131 h.mouse_drag();
2132 let scene = h.frame();
2133 assert_eq!(
2134 scene.layers.first(),
2135 Some(&(rest().origin(), CHILD, DIM_OPACITY))
2136 );
2137 assert_eq!(rects(&scene).first(), Some(&rest()));
2138 }
2139
2140 #[test]
2141 fn placeholder_feedback_leaves_the_rest_position_empty() {
2142 let mut h = Harness::new(Cfg {
2143 feedback: SourceFeedback::Placeholder,
2144 ..Cfg::default()
2145 });
2146 h.mouse_drag();
2147 let scene = h.frame();
2148 assert_eq!(
2149 rects(&scene),
2150 vec![Rect::from_origin_size(Point::new(40.0, 130.0), CHILD)],
2151 "only the ghost paints the child"
2152 );
2153 }
2154
2155 #[test]
2156 fn no_feedback_leaves_the_source_untouched() {
2157 let mut h = Harness::new(Cfg {
2158 feedback: SourceFeedback::None,
2159 ..Cfg::default()
2160 });
2161 h.mouse_drag();
2162 let scene = h.frame();
2163 assert_eq!(
2164 scene.layers.len(),
2165 1,
2166 "only the ghost's layer: {:?}",
2167 scene.layers
2168 );
2169 assert_eq!(rects(&scene).first(), Some(&rest()));
2170 }
2171
2172 #[test]
2173 fn a_ghost_builder_replaces_the_snapshot() {
2174 let mut h = Harness::new(Cfg {
2175 custom_ghost: true,
2176 ..Cfg::default()
2177 });
2178 h.mouse_drag();
2179 let scene = h.frame();
2180 assert_eq!(rects(&scene), vec![rest()], "the child is not snapshotted");
2181 assert!(
2182 scene.layers.contains(&(
2183 Point::new(40.0, 130.0),
2184 Size::new(30.0, 20.0),
2185 GHOST_OPACITY
2186 )),
2187 "the built ghost floats at the pointer, at its own size: {:?}",
2188 scene.layers
2189 );
2190 }
2191
2192 #[test]
2193 fn a_secondary_press_never_arms() {
2194 let mut h = Harness::new(Cfg::default());
2195 h.root.event(
2196 &mut h.state,
2197 &InputEvent::Pointer(PointerEvent {
2198 phase: PointerPhase::Down,
2199 position: Point::new(30.0, 120.0),
2200 button: PointerButton::Secondary,
2201 }),
2202 );
2203 assert_eq!(h.phase(), DragPhase::Idle);
2204 assert_eq!(h.log(), vec!["Down@20,20"], "it still reaches the child");
2205 }
2206
2207 #[test]
2208 fn a_plain_tap_leaves_the_source_focused_for_a_later_keyboard_lift() {
2209 let mut h = Harness::new(Cfg::default());
2210 assert!(!h.root.is_focus_active(), "nothing focused yet");
2211 h.mouse(PointerPhase::Down, 30.0, 120.0);
2212 h.mouse(PointerPhase::Up, 30.0, 120.0);
2213 assert_eq!(h.state.taps, 1, "the tap still reaches the child");
2214 assert!(
2215 h.root.is_focus_active(),
2216 "a plain tap keeps the focus claim, unlike a drag's end"
2217 );
2218 }
2219
2220 #[test]
2221 fn keyboard_lift_cycle_and_drop_deliver_the_payload_to_the_hovered_target() {
2222 let mut h = Harness::new(Cfg::default());
2223 let first = h.target(Rect::new(0.0, 0.0, 100.0, 100.0));
2224 let second = h.target(Rect::new(100.0, 0.0, 200.0, 100.0));
2225
2226 h.mouse(PointerPhase::Down, 30.0, 120.0);
2228 h.mouse(PointerPhase::Up, 30.0, 120.0);
2229 assert!(h.root.is_focus_active());
2230
2231 h.key(Key::Named(NamedKey::Enter));
2232 assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
2233 assert_eq!(
2234 h.state.coordinator.with_payload(|item: &u32| *item),
2235 Some(7),
2236 "the payload builder ran against the state"
2237 );
2238 assert_eq!(
2239 h.state
2240 .coordinator
2241 .state()
2242 .session()
2243 .and_then(|s| s.hovered),
2244 None,
2245 "nothing hovered until a cycle step"
2246 );
2247
2248 h.key(Key::Named(NamedKey::ArrowRight));
2249 assert_eq!(
2250 h.state
2251 .coordinator
2252 .state()
2253 .session()
2254 .and_then(|s| s.hovered),
2255 Some(first),
2256 "the first registered target is hovered after one step"
2257 );
2258
2259 h.key(Key::Named(NamedKey::ArrowRight));
2260 assert_eq!(
2261 h.state
2262 .coordinator
2263 .state()
2264 .session()
2265 .and_then(|s| s.hovered),
2266 Some(second)
2267 );
2268
2269 h.key(Key::Named(NamedKey::Enter));
2270 assert_eq!(
2271 h.phase(),
2272 DragPhase::Dropping,
2273 "Enter drops on the hovered target"
2274 );
2275 match h.state.coordinator.state() {
2276 DragState::Dropping { target, .. } => assert_eq!(target, second),
2277 other => panic!("expected Dropping, got {other:?}"),
2278 }
2279 assert_eq!(
2280 h.state.coordinator.take_payload::<u32>().map(|p| *p),
2281 Some(7),
2282 "the payload the lift carried reaches the dropped-on target"
2283 );
2284 h.state.coordinator.complete_drop();
2285 assert_eq!(h.phase(), DragPhase::Idle);
2286 assert!(
2287 !h.root.is_focus_active(),
2288 "a completed drop releases focus, like a pointer drop"
2289 );
2290 }
2291
2292 #[test]
2293 fn keyboard_drop_with_nothing_hovered_cancels() {
2294 let mut h = Harness::new(Cfg::default());
2295 h.mouse(PointerPhase::Down, 30.0, 120.0);
2296 h.mouse(PointerPhase::Up, 30.0, 120.0);
2297 h.key(Key::Named(NamedKey::Enter));
2298 assert_eq!(h.phase(), DragPhase::Dragging);
2299 h.key(Key::Named(NamedKey::Enter));
2300 assert_eq!(h.phase(), DragPhase::Idle, "dropping on nothing cancels");
2301 }
2302
2303 #[test]
2304 fn escape_cancels_a_keyboard_lift_and_a_later_lift_still_works() {
2305 let mut h = Harness::new(Cfg::default());
2306 h.mouse(PointerPhase::Down, 30.0, 120.0);
2307 h.mouse(PointerPhase::Up, 30.0, 120.0);
2308 h.key(Key::Named(NamedKey::Enter));
2309 assert_eq!(h.phase(), DragPhase::Dragging);
2310 h.escape();
2311 assert_eq!(h.phase(), DragPhase::Idle);
2312 assert!(
2313 !h.root.is_focus_active(),
2314 "Escape ends the focus session outright, even for a keyboard lift"
2315 );
2316
2317 h.mouse(PointerPhase::Down, 30.0, 120.0);
2319 h.mouse(PointerPhase::Up, 30.0, 120.0);
2320 h.key(Key::Named(NamedKey::Enter));
2321 assert_eq!(h.phase(), DragPhase::Dragging, "a later lift still works");
2322 }
2323
2324 #[test]
2325 fn space_also_lifts_and_drops() {
2326 let mut h = Harness::new(Cfg::default());
2327 h.mouse(PointerPhase::Down, 30.0, 120.0);
2328 h.mouse(PointerPhase::Up, 30.0, 120.0);
2329 h.key(Key::Character(" ".into()));
2330 assert_eq!(h.phase(), DragPhase::Dragging, "Space lifts");
2331 h.key(Key::Character(" ".into()));
2332 assert_eq!(
2333 h.phase(),
2334 DragPhase::Idle,
2335 "Space drops (on nothing: a cancel)"
2336 );
2337 }
2338
2339 #[test]
2340 fn a_focused_child_keeps_its_own_keys_the_lift_chord_included() {
2341 let mut h = Harness::new(Cfg {
2342 focusable_child: true,
2343 ..Cfg::default()
2344 });
2345 h.mouse(PointerPhase::Down, 30.0, 120.0);
2346 h.mouse(PointerPhase::Up, 30.0, 120.0);
2347 assert!(h.root.is_focus_active(), "the child holds its own focus");
2348 h.key(Key::Named(NamedKey::Enter));
2349 assert_eq!(
2350 h.phase(),
2351 DragPhase::Idle,
2352 "the child's own focus claim takes the key first — no lift"
2353 );
2354 assert_eq!(h.log().last().map(String::as_str), Some("Key Named(Enter)"));
2355 }
2356
2357 #[test]
2358 fn semantics_reports_a_button_role_with_click_and_a_label_for_each_state() {
2359 let mut h = Harness::new(Cfg::default());
2360 let update = h.root.semantics();
2361 let (_, node) = update
2362 .nodes
2363 .iter()
2364 .find(|(_, n)| n.role() == Role::Button)
2365 .expect("a Role::Button node was pushed");
2366 assert_eq!(
2367 node.label(),
2368 Some("Draggable"),
2369 "idle names no verb Click does not perform"
2370 );
2371 assert_eq!(node.description(), Some("Press Enter to lift"));
2372 assert!(node.supports_action(Action::Click));
2373
2374 h.mouse_drag();
2375 h.frame();
2376 let update = h.root.semantics();
2377 let (_, node) = update
2378 .nodes
2379 .iter()
2380 .find(|(_, n)| n.role() == Role::Button)
2381 .expect("a Role::Button node was pushed");
2382 assert_eq!(
2383 node.label(),
2384 Some("Dragging"),
2385 "a pointer session flips the label, but Click still only cancels it"
2386 );
2387
2388 h.mouse(PointerPhase::Up, 60.0, 150.0);
2389 h.mouse(PointerPhase::Down, 30.0, 120.0);
2390 h.mouse(PointerPhase::Up, 30.0, 120.0);
2391 h.key(Key::Named(NamedKey::Enter));
2392 let update = h.root.semantics();
2393 let (_, node) = update
2394 .nodes
2395 .iter()
2396 .find(|(_, n)| n.role() == Role::Button)
2397 .expect("a Role::Button node was pushed");
2398 assert_eq!(
2399 node.label(),
2400 Some("Drop"),
2401 "a live keyboard session labels Click honestly: it drops"
2402 );
2403 }
2404
2405 fn node_center() -> Point {
2408 Point::new(AT.x + CHILD.width / 2.0, AT.y + CHILD.height / 2.0)
2409 }
2410
2411 #[test]
2412 fn an_accessibility_click_on_an_idle_focused_source_is_a_plain_tap() {
2413 let mut h = Harness::new(Cfg::default());
2414 h.mouse(PointerPhase::Down, 30.0, 120.0);
2416 h.mouse(PointerPhase::Up, 30.0, 120.0);
2417 assert!(h.root.is_focus_active());
2418 h.state.taps = 0;
2419 h.state.log.borrow_mut().clear();
2420
2421 let center = node_center();
2425 h.mouse(PointerPhase::Down, center.x, center.y);
2426 h.mouse(PointerPhase::Up, center.x, center.y);
2427
2428 assert_eq!(h.state.taps, 1, "Click reaches the child as a plain tap");
2429 assert_eq!(h.phase(), DragPhase::Idle, "Click never lifts a drag");
2430 }
2431
2432 #[test]
2433 fn an_accessibility_click_during_a_live_keyboard_session_with_nothing_hovered_cancels_it() {
2434 let mut h = Harness::new(Cfg::default());
2435 h.mouse(PointerPhase::Down, 30.0, 120.0);
2436 h.mouse(PointerPhase::Up, 30.0, 120.0);
2437 h.key(Key::Named(NamedKey::Enter));
2438 assert_eq!(
2439 h.phase(),
2440 DragPhase::Dragging,
2441 "Enter lifts a keyboard session"
2442 );
2443 assert_eq!(
2444 h.state.taps, 1,
2445 "the earlier focusing tap reached the child"
2446 );
2447 h.state.log.borrow_mut().clear();
2448
2449 let center = node_center();
2456 h.mouse(PointerPhase::Down, center.x, center.y);
2457 h.mouse(PointerPhase::Up, center.x, center.y);
2458
2459 assert_eq!(h.phase(), DragPhase::Idle, "the session ended, not dropped");
2460 assert_eq!(h.state.taps, 1, "the matching Up never reached the child");
2461 assert!(h.log().is_empty(), "the Down/Up never reached the child");
2462 assert!(
2463 h.changes.borrow().iter().any(|c| matches!(
2464 c,
2465 DragStateChange::Phase {
2466 next: DragPhase::Cancelled,
2467 ..
2468 }
2469 )),
2470 "cancelled, never a drop: {:?}",
2471 h.changes.borrow()
2472 );
2473 assert!(
2474 !h.changes.borrow().iter().any(|c| matches!(
2475 c,
2476 DragStateChange::Phase {
2477 next: DragPhase::Dropping,
2478 ..
2479 }
2480 )),
2481 "never a drop: {:?}",
2482 h.changes.borrow()
2483 );
2484 }
2485
2486 #[test]
2487 fn an_accessibility_click_during_a_live_keyboard_session_drops_onto_the_hovered_target() {
2488 let mut h = Harness::new(Cfg::default());
2489 let target = h.target(Rect::new(0.0, 0.0, 400.0, 600.0));
2490 h.mouse(PointerPhase::Down, 30.0, 120.0);
2491 h.mouse(PointerPhase::Up, 30.0, 120.0);
2492 h.key(Key::Named(NamedKey::Enter));
2493 h.key(Key::Named(NamedKey::ArrowRight));
2494 assert_eq!(
2495 h.state
2496 .coordinator
2497 .state()
2498 .session()
2499 .and_then(|s| s.hovered),
2500 Some(target),
2501 "cycling landed on the only registered target"
2502 );
2503 h.state.log.borrow_mut().clear();
2504
2505 let center = node_center();
2509 h.mouse(PointerPhase::Down, center.x, center.y);
2510 h.mouse(PointerPhase::Up, center.x, center.y);
2511
2512 assert_eq!(
2513 h.phase(),
2514 DragPhase::Dropping,
2515 "the coordinator reached Dropping on the hovered target"
2516 );
2517 match h.state.coordinator.state() {
2518 DragState::Dropping { target: on, .. } => assert_eq!(on, target),
2519 other => panic!("expected Dropping, got {other:?}"),
2520 }
2521 assert!(h.log().is_empty(), "the Down/Up never reached the child");
2522
2523 h.state.coordinator.complete_drop();
2524 assert_eq!(h.phase(), DragPhase::Idle);
2525 }
2526
2527 #[test]
2528 fn a_keyboard_session_ended_elsewhere_returns_to_idle_ready_for_another_lift() {
2529 let mut h = Harness::new(Cfg::default());
2530 h.mouse(PointerPhase::Down, 30.0, 120.0);
2531 h.mouse(PointerPhase::Up, 30.0, 120.0);
2532 h.key(Key::Named(NamedKey::Enter));
2533 assert_eq!(h.phase(), DragPhase::Dragging, "Enter lifts");
2534
2535 h.state.coordinator.cancel();
2539 let after = h.frame();
2540 assert_eq!(rects(&after), vec![rest()], "the ghost closed");
2541 assert!(
2542 h.root.is_focus_active(),
2543 "the focus claim is left exactly as it was, not released"
2544 );
2545
2546 h.key(Key::Named(NamedKey::Enter));
2549 assert_eq!(
2550 h.phase(),
2551 DragPhase::Dragging,
2552 "the source landed back on Idle, ready for another lift"
2553 );
2554 }
2555}