1use std::any::{Any, TypeId};
35use std::cell::{Ref, RefCell};
36use std::collections::{HashMap, VecDeque};
37use std::fmt;
38use std::path::PathBuf;
39use std::rc::{Rc, Weak};
40
41use frust_core::FrameTime;
42use kurbo::{Point, Rect, Vec2};
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct DragSourceId(u64);
48
49impl DragSourceId {
50 pub fn get(self) -> u64 {
52 self.0
53 }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct DragTargetId(u64);
60
61impl DragTargetId {
62 pub fn get(self) -> u64 {
64 self.0
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum DragKind {
71 Internal,
75 ExternalFiles(Vec<PathBuf>),
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub enum DragPhase {
85 Idle,
87 Armed,
89 Dragging,
91 Dropping,
94 Cancelled,
97}
98
99#[derive(Clone, Debug, PartialEq)]
102pub struct DragSession {
103 pub kind: DragKind,
105 pub source: Option<DragSourceId>,
107 pub press: Point,
110 pub pointer: Point,
112 pub ghost_offset: Vec2,
115 pub hovered: Option<DragTargetId>,
118 pub payload_type: Option<TypeId>,
121 pub keyboard: bool,
130}
131
132impl DragSession {
133 pub fn ghost_origin(&self) -> Point {
135 self.pointer + self.ghost_offset
136 }
137}
138
139#[derive(Clone, Debug, PartialEq)]
141pub enum DragState {
142 Idle,
144 Armed {
146 source: DragSourceId,
148 press: Point,
150 keyboard: bool,
153 },
154 Dragging(DragSession),
156 Dropping {
158 session: DragSession,
160 target: DragTargetId,
162 },
163}
164
165impl DragState {
166 pub fn phase(&self) -> DragPhase {
168 match self {
169 DragState::Idle => DragPhase::Idle,
170 DragState::Armed { .. } => DragPhase::Armed,
171 DragState::Dragging(_) => DragPhase::Dragging,
172 DragState::Dropping { .. } => DragPhase::Dropping,
173 }
174 }
175
176 pub fn session(&self) -> Option<&DragSession> {
178 match self {
179 DragState::Dragging(session) | DragState::Dropping { session, .. } => Some(session),
180 DragState::Idle | DragState::Armed { .. } => None,
181 }
182 }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq)]
187pub enum DragStateChange {
188 Phase {
190 previous: DragPhase,
192 next: DragPhase,
194 },
195 Enter {
197 target: DragTargetId,
199 },
200 Leave {
203 target: DragTargetId,
205 },
206 Move {
208 pointer: Point,
210 },
211}
212
213type Listener = Rc<dyn Fn(DragStateChange)>;
215
216struct Session {
218 kind: DragKind,
219 source: Option<DragSourceId>,
220 press: Point,
221 pointer: Point,
222 ghost_offset: Vec2,
223 hovered: Option<DragTargetId>,
224 payload: Option<Box<dyn Any>>,
225 payload_type: TypeId,
228 keyboard: bool,
230}
231
232impl Session {
233 fn snapshot(&self) -> DragSession {
234 DragSession {
235 kind: self.kind.clone(),
236 source: self.source,
237 press: self.press,
238 pointer: self.pointer,
239 ghost_offset: self.ghost_offset,
240 hovered: self.hovered,
241 payload_type: self.payload.as_ref().map(|_| self.payload_type),
242 keyboard: self.keyboard,
243 }
244 }
245}
246
247enum Machine {
249 Idle,
250 Armed {
251 source: DragSourceId,
252 press: Point,
253 keyboard: bool,
254 },
255 Dragging(Session),
256 Dropping {
257 session: Session,
258 target: DragTargetId,
259 },
260}
261
262impl Machine {
263 fn phase(&self) -> DragPhase {
264 match self {
265 Machine::Idle => DragPhase::Idle,
266 Machine::Armed { .. } => DragPhase::Armed,
267 Machine::Dragging(_) => DragPhase::Dragging,
268 Machine::Dropping { .. } => DragPhase::Dropping,
269 }
270 }
271
272 fn session(&self) -> Option<&Session> {
273 match self {
274 Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
275 Machine::Idle | Machine::Armed { .. } => None,
276 }
277 }
278
279 fn session_mut(&mut self) -> Option<&mut Session> {
280 match self {
281 Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
282 Machine::Idle | Machine::Armed { .. } => None,
283 }
284 }
285}
286
287struct Shared {
289 machine: Machine,
290 next_id: u64,
292 listeners: Vec<(u64, Listener)>,
295 next_listener: u64,
297 queue: VecDeque<DragStateChange>,
299 dispatching: bool,
303 targets: HashMap<DragTargetId, TargetEntry>,
306 next_registration: u64,
308 pass: u64,
311 pass_frame: Option<FrameTime>,
314}
315
316#[derive(Clone, Copy, Debug)]
318struct TargetEntry {
319 registration: u64,
322 accepts: Option<TypeId>,
325 bounds: Option<Rect>,
329 reported: Option<u64>,
331}
332
333impl Default for Shared {
334 fn default() -> Self {
335 Self {
336 machine: Machine::Idle,
337 next_id: 1,
338 listeners: Vec::new(),
339 next_listener: 0,
340 queue: VecDeque::new(),
341 dispatching: false,
342 targets: HashMap::new(),
343 next_registration: 0,
344 pass: 0,
345 pass_frame: None,
346 }
347 }
348}
349
350impl Shared {
351 fn cancel(&mut self, changes: &mut Vec<DragStateChange>) -> Option<Session> {
356 let previous = self.machine.phase();
357 let (leave, session) = match std::mem::replace(&mut self.machine, Machine::Idle) {
358 Machine::Idle => return None,
359 Machine::Armed { .. } => (None, None),
360 Machine::Dragging(session) => (session.hovered, Some(session)),
361 Machine::Dropping { session, target } => (Some(target), Some(session)),
362 };
363 if let Some(target) = leave {
364 changes.push(DragStateChange::Leave { target });
365 }
366 changes.push(DragStateChange::Phase {
367 previous,
368 next: DragPhase::Cancelled,
369 });
370 changes.push(DragStateChange::Phase {
371 previous: DragPhase::Cancelled,
372 next: DragPhase::Idle,
373 });
374 session
375 }
376
377 fn live_payload_type(&self) -> Option<TypeId> {
380 self.machine.session().map(|session| session.payload_type)
381 }
382
383 fn resolvable(&self, entry: &TargetEntry, payload: Option<TypeId>) -> Option<Rect> {
388 self.resolvable_as_of(entry, payload, self.pass)
389 }
390
391 fn resolvable_as_of(
396 &self,
397 entry: &TargetEntry,
398 payload: Option<TypeId>,
399 pass: u64,
400 ) -> Option<Rect> {
401 if entry.reported != Some(pass) {
402 return None;
403 }
404 if let (Some(accepts), Some(payload)) = (entry.accepts, payload)
405 && accepts != payload
406 {
407 return None;
408 }
409 entry.bounds
410 }
411
412 fn target_at(&self, point: Point) -> Option<DragTargetId> {
415 let payload = self.live_payload_type();
416 self.targets
417 .iter()
418 .filter(|(_, entry)| {
419 self.resolvable(entry, payload)
420 .is_some_and(|rect| rect.contains(point))
421 })
422 .max_by_key(|(_, entry)| entry.registration)
423 .map(|(&id, _)| id)
424 }
425
426 fn target_at_mid_pass(&self, point: Point) -> Option<DragTargetId> {
441 let payload = self.live_payload_type();
442 let previous = self.pass.checked_sub(1);
443 self.targets
444 .iter()
445 .filter(|(_, entry)| {
446 self.resolvable_as_of(entry, payload, self.pass)
447 .or_else(|| {
448 previous.and_then(|pass| self.resolvable_as_of(entry, payload, pass))
449 })
450 .is_some_and(|rect| rect.contains(point))
451 })
452 .max_by_key(|(_, entry)| entry.registration)
453 .map(|(&id, _)| id)
454 }
455
456 fn record_bounds(
463 &mut self,
464 id: DragTargetId,
465 bounds: Option<Rect>,
466 frame: Option<FrameTime>,
467 ) -> bool {
468 let Some(reported) = self.targets.get(&id).map(|entry| entry.reported) else {
469 return false;
470 };
471 let opens = match frame {
472 Some(frame) => self.pass_frame != Some(frame),
473 None => reported == Some(self.pass),
474 };
475 if opens {
476 self.pass += 1;
477 self.pass_frame = frame;
478 }
479 let pass = self.pass;
480 if let Some(entry) = self.targets.get_mut(&id) {
481 entry.bounds = bounds;
482 entry.reported = Some(pass);
483 }
484 true
485 }
486
487 fn set_hovered(&mut self, target: Option<DragTargetId>, changes: &mut Vec<DragStateChange>) {
491 if let Machine::Dragging(session) = &mut self.machine
492 && session.hovered != target
493 {
494 if let Some(old) = session.hovered {
495 changes.push(DragStateChange::Leave { target: old });
496 }
497 session.hovered = target;
498 if let Some(target) = target {
499 changes.push(DragStateChange::Enter { target });
500 }
501 }
502 }
503
504 fn resolve_hover(&mut self, changes: &mut Vec<DragStateChange>) {
510 let Machine::Dragging(session) = &self.machine else {
511 return;
512 };
513 if session.keyboard {
514 return;
515 }
516 let target = self.target_at(session.pointer);
517 self.set_hovered(target, changes);
518 }
519
520 fn resolve_hover_mid_pass(&mut self, changes: &mut Vec<DragStateChange>) {
527 let Machine::Dragging(session) = &self.machine else {
528 return;
529 };
530 if session.keyboard {
531 return;
532 }
533 let target = self.target_at_mid_pass(session.pointer);
534 self.set_hovered(target, changes);
535 }
536
537 fn arm_from_idle(
539 &mut self,
540 source: DragSourceId,
541 press: Point,
542 keyboard: bool,
543 changes: &mut Vec<DragStateChange>,
544 ) {
545 debug_assert!(matches!(self.machine, Machine::Idle));
546 self.machine = Machine::Armed {
547 source,
548 press,
549 keyboard,
550 };
551 changes.push(DragStateChange::Phase {
552 previous: DragPhase::Idle,
553 next: DragPhase::Armed,
554 });
555 }
556
557 fn move_target(&mut self, step: i64, changes: &mut Vec<DragStateChange>) {
574 let Machine::Dragging(session) = &self.machine else {
575 return;
576 };
577 let payload = Some(session.payload_type);
578 let mut ordered: Vec<(u64, DragTargetId)> = self
579 .targets
580 .iter()
581 .filter(|(_, entry)| self.resolvable(entry, payload).is_some())
582 .map(|(&id, entry)| (entry.registration, id))
583 .collect();
584 if ordered.is_empty() {
585 return;
586 }
587 ordered.sort_unstable();
588 let ids: Vec<DragTargetId> = ordered.into_iter().map(|(_, id)| id).collect();
589 let len = ids.len() as i64;
590 let next = match session
591 .hovered
592 .and_then(|current| ids.iter().position(|&id| id == current))
593 {
594 Some(index) => ids[(index as i64 + step).rem_euclid(len) as usize],
595 None if step >= 0 => ids[0],
596 None => ids[ids.len() - 1],
597 };
598 self.set_hovered(Some(next), changes);
599 }
600}
601
602#[derive(Clone, Default)]
634pub struct DragCoordinator {
635 shared: Rc<RefCell<Shared>>,
636}
637
638impl fmt::Debug for DragCoordinator {
639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640 let shared = self.shared.borrow();
641 f.debug_struct("DragCoordinator")
642 .field("phase", &shared.machine.phase())
643 .field("listeners", &shared.listeners.len())
644 .finish()
645 }
646}
647
648impl DragCoordinator {
649 pub fn new() -> Self {
651 Self::default()
652 }
653
654 pub fn new_source_id(&self) -> DragSourceId {
656 DragSourceId(self.next_id())
657 }
658
659 pub fn new_target_id(&self) -> DragTargetId {
661 DragTargetId(self.next_id())
662 }
663
664 fn next_id(&self) -> u64 {
665 let mut shared = self.shared.borrow_mut();
666 let id = shared.next_id;
667 shared.next_id += 1;
668 id
669 }
670
671 pub fn state(&self) -> DragState {
673 match &self.shared.borrow().machine {
674 Machine::Idle => DragState::Idle,
675 Machine::Armed {
676 source,
677 press,
678 keyboard,
679 } => DragState::Armed {
680 source: *source,
681 press: *press,
682 keyboard: *keyboard,
683 },
684 Machine::Dragging(session) => DragState::Dragging(session.snapshot()),
685 Machine::Dropping { session, target } => DragState::Dropping {
686 session: session.snapshot(),
687 target: *target,
688 },
689 }
690 }
691
692 pub fn phase(&self) -> DragPhase {
694 self.shared.borrow().machine.phase()
695 }
696
697 pub fn arm(&self, source: DragSourceId, press: Point) -> bool {
705 self.mutate(|shared, changes| {
706 if matches!(shared.machine, Machine::Dragging(_)) {
707 log_ignored("arm", "a drag is already in flight");
708 return false;
709 }
710 shared.cancel(changes);
711 shared.arm_from_idle(source, press, false, changes);
712 true
713 })
714 }
715
716 pub fn lift(&self, source: DragSourceId) -> bool {
725 self.mutate(|shared, changes| {
726 if matches!(shared.machine, Machine::Dragging(_)) {
727 log_ignored("lift", "a drag is already in flight");
728 return false;
729 }
730 shared.cancel(changes);
731 shared.arm_from_idle(source, Point::ZERO, true, changes);
732 true
733 })
734 }
735
736 pub fn move_to_next_target(&self) {
752 self.mutate(|shared, changes| shared.move_target(1, changes));
753 }
754
755 pub fn move_to_previous_target(&self) {
760 self.mutate(|shared, changes| shared.move_target(-1, changes));
761 }
762
763 pub fn begin<T: Any>(&self, payload: T) -> bool {
777 self.mutate(|shared, changes| {
778 let (source, press, pointer, ghost_offset, keyboard) = match &shared.machine {
779 Machine::Idle => {
780 log_ignored("begin", "nothing is armed");
781 return false;
782 }
783 Machine::Dropping { .. } => {
784 log_ignored("begin", "a drop is still being completed");
785 return false;
786 }
787 Machine::Armed {
788 source,
789 press,
790 keyboard,
791 } => (*source, *press, *press, Vec2::ZERO, *keyboard),
792 Machine::Dragging(_) => {
793 let first = shared
794 .cancel(changes)
795 .expect("a Dragging machine cancels to its session");
796 let Some(source) = first.source else {
797 log_ignored("begin", "the cancelled session had no source to restart");
798 return false;
799 };
800 shared.arm_from_idle(source, first.press, first.keyboard, changes);
801 (
802 source,
803 first.press,
804 first.pointer,
805 first.ghost_offset,
806 first.keyboard,
807 )
808 }
809 };
810 shared.machine = Machine::Dragging(Session {
811 kind: DragKind::Internal,
812 source: Some(source),
813 press,
814 pointer,
815 ghost_offset,
816 hovered: None,
817 payload: Some(Box::new(payload)),
818 payload_type: TypeId::of::<T>(),
819 keyboard,
820 });
821 changes.push(DragStateChange::Phase {
822 previous: DragPhase::Armed,
823 next: DragPhase::Dragging,
824 });
825 true
826 })
827 }
828
829 pub fn begin_external(&self, paths: Vec<PathBuf>, pointer: Point) {
834 self.mutate(|shared, changes| {
835 shared.cancel(changes);
836 shared.machine = Machine::Dragging(Session {
837 kind: DragKind::ExternalFiles(paths.clone()),
838 source: None,
839 press: pointer,
840 pointer,
841 ghost_offset: Vec2::ZERO,
842 hovered: None,
843 payload: Some(Box::new(paths)),
844 payload_type: TypeId::of::<Vec<PathBuf>>(),
845 keyboard: false,
846 });
847 changes.push(DragStateChange::Phase {
848 previous: DragPhase::Idle,
849 next: DragPhase::Dragging,
850 });
851 });
852 }
853
854 pub fn set_external_payload(&self, paths: Vec<PathBuf>) {
870 if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine
871 && matches!(session.kind, DragKind::ExternalFiles(_))
872 {
873 session.kind = DragKind::ExternalFiles(paths.clone());
874 session.payload = Some(Box::new(paths));
875 }
876 }
877
878 pub fn update_pointer(&self, pointer: Point) {
900 self.mutate(|shared, changes| {
901 let Machine::Dragging(session) = &mut shared.machine else {
902 return;
903 };
904 if session.keyboard {
905 return;
906 }
907 let moved = session.pointer != pointer;
908 session.pointer = pointer;
909 shared.resolve_hover(changes);
910 if moved {
911 changes.push(DragStateChange::Move { pointer });
912 }
913 });
914 }
915
916 pub fn resolve_hover(&self) {
938 self.mutate(|shared, changes| shared.resolve_hover_mid_pass(changes));
939 }
940
941 pub fn set_ghost_offset(&self, offset: Vec2) {
946 if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine {
947 session.ghost_offset = offset;
948 }
949 }
950
951 pub fn set_hovered(&self, target: Option<DragTargetId>) {
958 self.mutate(|shared, changes| shared.set_hovered(target, changes));
959 }
960
961 pub fn register_target(&self, id: DragTargetId) {
972 self.register(id, None);
973 }
974
975 pub fn register_target_for<T: Any>(&self, id: DragTargetId) {
984 self.register(id, Some(TypeId::of::<T>()));
985 }
986
987 fn register(&self, id: DragTargetId, accepts: Option<TypeId>) {
988 let mut shared = self.shared.borrow_mut();
989 if shared.targets.contains_key(&id) {
990 return;
991 }
992 let registration = shared.next_registration;
993 shared.next_registration += 1;
994 shared.targets.insert(
995 id,
996 TargetEntry {
997 registration,
998 accepts,
999 bounds: None,
1000 reported: None,
1001 },
1002 );
1003 }
1004
1005 pub fn unregister_target(&self, id: DragTargetId) {
1011 self.mutate(|shared, changes| {
1012 shared.targets.remove(&id);
1013 match &mut shared.machine {
1014 Machine::Dragging(session) if session.hovered == Some(id) => {
1015 session.hovered = None;
1016 changes.push(DragStateChange::Leave { target: id });
1017 }
1018 Machine::Dropping { target, .. } if *target == id => {
1019 shared.cancel(changes);
1020 }
1021 _ => {}
1022 }
1023 });
1024 }
1025
1026 pub fn set_target_bounds(&self, id: DragTargetId, bounds: Rect) {
1033 if !self
1034 .shared
1035 .borrow_mut()
1036 .record_bounds(id, Some(bounds), None)
1037 {
1038 log_ignored("set_target_bounds", "the target is not registered");
1039 }
1040 }
1041
1042 pub fn report_target_bounds(&self, id: DragTargetId, bounds: Option<Rect>, frame: FrameTime) {
1055 let frame = (frame != FrameTime::ZERO).then_some(frame);
1056 if !self.shared.borrow_mut().record_bounds(id, bounds, frame) {
1057 log_ignored("report_target_bounds", "the target is not registered");
1058 }
1059 }
1060
1061 pub fn target_bounds(&self, id: DragTargetId) -> Option<Rect> {
1066 self.shared
1067 .borrow()
1068 .targets
1069 .get(&id)
1070 .and_then(|entry| entry.bounds)
1071 }
1072
1073 pub fn target_at(&self, point: Point) -> Option<DragTargetId> {
1081 self.shared.borrow().target_at(point)
1082 }
1083
1084 pub fn is_external(&self) -> bool {
1087 self.shared
1088 .borrow()
1089 .machine
1090 .session()
1091 .is_some_and(|session| matches!(session.kind, DragKind::ExternalFiles(_)))
1092 }
1093
1094 pub fn registered_targets(&self) -> Vec<DragTargetId> {
1096 let shared = self.shared.borrow();
1097 let mut targets: Vec<(u64, DragTargetId)> = shared
1098 .targets
1099 .iter()
1100 .map(|(&id, entry)| (entry.registration, id))
1101 .collect();
1102 targets.sort_unstable();
1103 targets.into_iter().map(|(_, id)| id).collect()
1104 }
1105
1106 pub fn drop(&self) -> Option<DragTargetId> {
1112 self.mutate(|shared, changes| match &shared.machine {
1113 Machine::Idle | Machine::Dropping { .. } => None,
1114 Machine::Armed { .. } => {
1115 shared.cancel(changes);
1116 None
1117 }
1118 Machine::Dragging(session) => {
1119 let Some(target) = session.hovered else {
1120 shared.cancel(changes);
1121 return None;
1122 };
1123 let Machine::Dragging(session) =
1124 std::mem::replace(&mut shared.machine, Machine::Idle)
1125 else {
1126 unreachable!("matched Dragging above");
1127 };
1128 shared.machine = Machine::Dropping { session, target };
1129 changes.push(DragStateChange::Phase {
1130 previous: DragPhase::Dragging,
1131 next: DragPhase::Dropping,
1132 });
1133 Some(target)
1134 }
1135 })
1136 }
1137
1138 pub fn complete_drop(&self) {
1141 self.mutate(|shared, changes| {
1142 if matches!(shared.machine, Machine::Dropping { .. }) {
1143 shared.machine = Machine::Idle;
1144 changes.push(DragStateChange::Phase {
1145 previous: DragPhase::Dropping,
1146 next: DragPhase::Idle,
1147 });
1148 }
1149 });
1150 }
1151
1152 pub fn cancel(&self) {
1156 self.mutate(|shared, changes| {
1157 shared.cancel(changes);
1158 });
1159 }
1160
1161 pub fn end_external(&self) -> bool {
1173 self.mutate(|shared, changes| {
1174 let ends = matches!(
1175 &shared.machine,
1176 Machine::Dragging(session) if matches!(session.kind, DragKind::ExternalFiles(_))
1177 );
1178 if ends {
1179 shared.cancel(changes);
1180 }
1181 ends
1182 })
1183 }
1184
1185 pub fn payload_is<T: Any>(&self) -> bool {
1188 self.shared
1189 .borrow()
1190 .machine
1191 .session()
1192 .is_some_and(|session| {
1193 session.payload.is_some() && session.payload_type == TypeId::of::<T>()
1194 })
1195 }
1196
1197 pub fn payload_ref<T: Any>(&self) -> Option<Ref<'_, T>> {
1205 Ref::filter_map(self.shared.borrow(), |shared| {
1206 shared
1207 .machine
1208 .session()?
1209 .payload
1210 .as_ref()?
1211 .downcast_ref::<T>()
1212 })
1213 .ok()
1214 }
1215
1216 pub fn with_payload<T: Any, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R> {
1221 self.payload_ref::<T>().map(|payload| f(&payload))
1222 }
1223
1224 pub fn take_payload<T: Any>(&self) -> Option<Box<T>> {
1228 let mut shared = self.shared.borrow_mut();
1229 let session = shared.machine.session_mut()?;
1230 if session.payload_type != TypeId::of::<T>() {
1231 return None;
1232 }
1233 session.payload.take()?.downcast::<T>().ok()
1234 }
1235
1236 pub fn subscribe<F: Fn(DragStateChange) + 'static>(&self, listener: F) -> DragSubscription {
1241 let mut shared = self.shared.borrow_mut();
1242 let id = shared.next_listener;
1243 shared.next_listener += 1;
1244 shared.listeners.push((id, Rc::new(listener)));
1245 DragSubscription {
1246 shared: Rc::downgrade(&self.shared),
1247 id,
1248 }
1249 }
1250
1251 fn mutate<R>(&self, f: impl FnOnce(&mut Shared, &mut Vec<DragStateChange>) -> R) -> R {
1254 let mut changes = Vec::new();
1255 let result = {
1256 let mut shared = self.shared.borrow_mut();
1257 let result = f(&mut shared, &mut changes);
1258 shared.queue.extend(changes.iter().copied());
1259 result
1260 };
1261 if !changes.is_empty() {
1262 frust_core::mark_pending_result_flush();
1263 self.dispatch();
1264 }
1265 result
1266 }
1267
1268 fn dispatch(&self) {
1273 {
1274 let mut shared = self.shared.borrow_mut();
1275 if shared.dispatching {
1276 return;
1277 }
1278 shared.dispatching = true;
1279 }
1280 let _guard = DispatchGuard(&self.shared);
1281 loop {
1282 let (change, listeners) = {
1283 let mut shared = self.shared.borrow_mut();
1284 let Some(change) = shared.queue.pop_front() else {
1285 break;
1286 };
1287 (change, shared.listeners.clone())
1288 };
1289 for (id, listener) in listeners {
1290 let live = self
1292 .shared
1293 .borrow()
1294 .listeners
1295 .iter()
1296 .any(|(other, _)| *other == id);
1297 if live {
1298 listener(change);
1299 }
1300 }
1301 }
1302 }
1303}
1304
1305struct DispatchGuard<'a>(&'a RefCell<Shared>);
1309
1310impl Drop for DispatchGuard<'_> {
1311 fn drop(&mut self) {
1312 if let Ok(mut shared) = self.0.try_borrow_mut() {
1313 shared.dispatching = false;
1314 if std::thread::panicking() {
1315 shared.queue.clear();
1316 }
1317 }
1318 }
1319}
1320
1321#[must_use = "dropping a DragSubscription unsubscribes its listener at once"]
1324pub struct DragSubscription {
1325 shared: Weak<RefCell<Shared>>,
1326 id: u64,
1327}
1328
1329impl fmt::Debug for DragSubscription {
1330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1331 f.debug_struct("DragSubscription")
1332 .field("id", &self.id)
1333 .finish()
1334 }
1335}
1336
1337impl Drop for DragSubscription {
1338 fn drop(&mut self) {
1339 if let Some(shared) = self.shared.upgrade() {
1340 shared
1341 .borrow_mut()
1342 .listeners
1343 .retain(|(id, _)| *id != self.id);
1344 }
1345 }
1346}
1347
1348fn log_ignored(_call: &str, _why: &str) {
1350 #[cfg(debug_assertions)]
1351 eprintln!("frust-widgets: DragCoordinator::{_call} ignored: {_why}");
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use std::cell::RefCell;
1357
1358 use super::*;
1359
1360 const PRESS: Point = Point::new(10.0, 10.0);
1361 const MOVED: Point = Point::new(20.0, 30.0);
1362 const OTHER_PRESS: Point = Point::new(5.0, 5.0);
1363 const FILE_ENTRY: Point = Point::new(1.0, 2.0);
1364
1365 struct Fixture {
1367 drag: DragCoordinator,
1368 source: DragSourceId,
1369 other_source: DragSourceId,
1370 first: DragTargetId,
1371 second: DragTargetId,
1372 log: Rc<RefCell<Vec<DragStateChange>>>,
1373 _subscription: DragSubscription,
1374 }
1375
1376 impl Fixture {
1377 fn new() -> Self {
1378 let drag = DragCoordinator::new();
1379 let log = Rc::new(RefCell::new(Vec::new()));
1380 let sink = Rc::clone(&log);
1381 let subscription = drag.subscribe(move |change| sink.borrow_mut().push(change));
1382 Self {
1383 source: drag.new_source_id(),
1384 other_source: drag.new_source_id(),
1385 first: drag.new_target_id(),
1386 second: drag.new_target_id(),
1387 drag,
1388 log,
1389 _subscription: subscription,
1390 }
1391 }
1392
1393 fn take_log(&self) -> Vec<DragStateChange> {
1394 std::mem::take(&mut *self.log.borrow_mut())
1395 }
1396 }
1397
1398 #[derive(Clone, Copy, Debug)]
1400 enum Start {
1401 Idle,
1402 Armed,
1403 Dragging,
1405 Hovering,
1407 Dropping,
1409 External,
1411 }
1412
1413 impl Start {
1414 fn reach(self, f: &Fixture) {
1415 match self {
1416 Start::Idle => {}
1417 Start::Armed => {
1418 f.drag.arm(f.source, PRESS);
1419 }
1420 Start::Dragging => {
1421 f.drag.arm(f.source, PRESS);
1422 f.drag.begin(7_i32);
1423 }
1424 Start::Hovering => {
1425 Start::Dragging.reach(f);
1426 f.drag.set_hovered(Some(f.first));
1427 }
1428 Start::Dropping => {
1429 Start::Hovering.reach(f);
1430 f.drag.drop();
1431 }
1432 Start::External => {
1433 f.drag
1434 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
1435 }
1436 }
1437 }
1438 }
1439
1440 #[derive(Clone, Copy, Debug)]
1442 enum Op {
1443 Arm,
1444 Begin,
1445 BeginExternal,
1446 Move,
1447 HoverSecond,
1448 HoverNone,
1449 Drop,
1450 Complete,
1451 Cancel,
1452 }
1453
1454 impl Op {
1455 fn apply(self, f: &Fixture) {
1456 match self {
1457 Op::Arm => {
1458 f.drag.arm(f.other_source, OTHER_PRESS);
1459 }
1460 Op::Begin => {
1461 f.drag.begin("second");
1462 }
1463 Op::BeginExternal => {
1464 f.drag
1465 .begin_external(vec![PathBuf::from("/tmp/b.txt")], FILE_ENTRY);
1466 }
1467 Op::Move => f.drag.update_pointer(MOVED),
1468 Op::HoverSecond => f.drag.set_hovered(Some(f.second)),
1469 Op::HoverNone => f.drag.set_hovered(None),
1470 Op::Drop => {
1471 f.drag.drop();
1472 }
1473 Op::Complete => f.drag.complete_drop(),
1474 Op::Cancel => f.drag.cancel(),
1475 }
1476 }
1477 }
1478
1479 #[derive(Clone, Copy, Debug)]
1481 enum Expect {
1482 Phase(DragPhase, DragPhase),
1483 EnterSecond,
1484 LeaveFirst,
1485 Moved,
1486 }
1487
1488 impl Expect {
1489 fn resolve(self, f: &Fixture) -> DragStateChange {
1490 match self {
1491 Expect::Phase(previous, next) => DragStateChange::Phase { previous, next },
1492 Expect::EnterSecond => DragStateChange::Enter { target: f.second },
1493 Expect::LeaveFirst => DragStateChange::Leave { target: f.first },
1494 Expect::Moved => DragStateChange::Move { pointer: MOVED },
1495 }
1496 }
1497 }
1498
1499 use DragPhase::{Armed, Cancelled, Dragging, Dropping, Idle};
1500
1501 const CANCEL_FROM_ARMED: &[Expect] = &[
1502 Expect::Phase(Armed, Cancelled),
1503 Expect::Phase(Cancelled, Idle),
1504 ];
1505 const CANCEL_FROM_DRAGGING: &[Expect] = &[
1506 Expect::Phase(Dragging, Cancelled),
1507 Expect::Phase(Cancelled, Idle),
1508 ];
1509
1510 fn table() -> Vec<(Start, Op, DragPhase, Vec<Expect>)> {
1513 use Expect::{EnterSecond, LeaveFirst, Moved, Phase};
1514 let cat = |parts: &[&[Expect]]| parts.concat();
1515 vec![
1516 (Start::Idle, Op::Arm, Armed, vec![Phase(Idle, Armed)]),
1518 (Start::Idle, Op::Begin, Idle, vec![]),
1519 (
1520 Start::Idle,
1521 Op::BeginExternal,
1522 Dragging,
1523 vec![Phase(Idle, Dragging)],
1524 ),
1525 (Start::Idle, Op::Move, Idle, vec![]),
1526 (Start::Idle, Op::HoverSecond, Idle, vec![]),
1527 (Start::Idle, Op::HoverNone, Idle, vec![]),
1528 (Start::Idle, Op::Drop, Idle, vec![]),
1529 (Start::Idle, Op::Complete, Idle, vec![]),
1530 (Start::Idle, Op::Cancel, Idle, vec![]),
1531 (
1533 Start::Armed,
1534 Op::Arm,
1535 Armed,
1536 cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Armed)]]),
1537 ),
1538 (
1539 Start::Armed,
1540 Op::Begin,
1541 Dragging,
1542 vec![Phase(Armed, Dragging)],
1543 ),
1544 (
1545 Start::Armed,
1546 Op::BeginExternal,
1547 Dragging,
1548 cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Dragging)]]),
1549 ),
1550 (Start::Armed, Op::Move, Armed, vec![]),
1551 (Start::Armed, Op::HoverSecond, Armed, vec![]),
1552 (Start::Armed, Op::HoverNone, Armed, vec![]),
1553 (Start::Armed, Op::Drop, Idle, CANCEL_FROM_ARMED.to_vec()),
1554 (Start::Armed, Op::Complete, Armed, vec![]),
1555 (Start::Armed, Op::Cancel, Idle, CANCEL_FROM_ARMED.to_vec()),
1556 (Start::Dragging, Op::Arm, Dragging, vec![]),
1558 (
1559 Start::Dragging,
1560 Op::Begin,
1561 Dragging,
1562 cat(&[
1563 CANCEL_FROM_DRAGGING,
1564 &[Phase(Idle, Armed), Phase(Armed, Dragging)],
1565 ]),
1566 ),
1567 (
1568 Start::Dragging,
1569 Op::BeginExternal,
1570 Dragging,
1571 cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
1572 ),
1573 (Start::Dragging, Op::Move, Dragging, vec![Moved]),
1574 (
1575 Start::Dragging,
1576 Op::HoverSecond,
1577 Dragging,
1578 vec![EnterSecond],
1579 ),
1580 (Start::Dragging, Op::HoverNone, Dragging, vec![]),
1581 (
1582 Start::Dragging,
1583 Op::Drop,
1584 Idle,
1585 CANCEL_FROM_DRAGGING.to_vec(),
1586 ),
1587 (Start::Dragging, Op::Complete, Dragging, vec![]),
1588 (
1589 Start::Dragging,
1590 Op::Cancel,
1591 Idle,
1592 CANCEL_FROM_DRAGGING.to_vec(),
1593 ),
1594 (Start::Hovering, Op::Arm, Dragging, vec![]),
1596 (
1597 Start::Hovering,
1598 Op::Begin,
1599 Dragging,
1600 cat(&[
1601 &[LeaveFirst],
1602 CANCEL_FROM_DRAGGING,
1603 &[Phase(Idle, Armed), Phase(Armed, Dragging)],
1604 ]),
1605 ),
1606 (
1607 Start::Hovering,
1608 Op::BeginExternal,
1609 Dragging,
1610 cat(&[
1611 &[LeaveFirst],
1612 CANCEL_FROM_DRAGGING,
1613 &[Phase(Idle, Dragging)],
1614 ]),
1615 ),
1616 (Start::Hovering, Op::Move, Dragging, vec![LeaveFirst, Moved]),
1619 (
1620 Start::Hovering,
1621 Op::HoverSecond,
1622 Dragging,
1623 vec![LeaveFirst, EnterSecond],
1624 ),
1625 (Start::Hovering, Op::HoverNone, Dragging, vec![LeaveFirst]),
1626 (
1627 Start::Hovering,
1628 Op::Drop,
1629 Dropping,
1630 vec![Phase(Dragging, Dropping)],
1631 ),
1632 (Start::Hovering, Op::Complete, Dragging, vec![]),
1633 (
1634 Start::Hovering,
1635 Op::Cancel,
1636 Idle,
1637 cat(&[&[LeaveFirst], CANCEL_FROM_DRAGGING]),
1638 ),
1639 (
1641 Start::Dropping,
1642 Op::Arm,
1643 Armed,
1644 vec![
1645 LeaveFirst,
1646 Phase(Dropping, Cancelled),
1647 Phase(Cancelled, Idle),
1648 Phase(Idle, Armed),
1649 ],
1650 ),
1651 (Start::Dropping, Op::Begin, Dropping, vec![]),
1652 (
1653 Start::Dropping,
1654 Op::BeginExternal,
1655 Dragging,
1656 vec![
1657 LeaveFirst,
1658 Phase(Dropping, Cancelled),
1659 Phase(Cancelled, Idle),
1660 Phase(Idle, Dragging),
1661 ],
1662 ),
1663 (Start::Dropping, Op::Move, Dropping, vec![]),
1664 (Start::Dropping, Op::HoverSecond, Dropping, vec![]),
1665 (Start::Dropping, Op::HoverNone, Dropping, vec![]),
1666 (Start::Dropping, Op::Drop, Dropping, vec![]),
1667 (
1668 Start::Dropping,
1669 Op::Complete,
1670 Idle,
1671 vec![Phase(Dropping, Idle)],
1672 ),
1673 (
1674 Start::Dropping,
1675 Op::Cancel,
1676 Idle,
1677 vec![
1678 LeaveFirst,
1679 Phase(Dropping, Cancelled),
1680 Phase(Cancelled, Idle),
1681 ],
1682 ),
1683 (Start::External, Op::Arm, Dragging, vec![]),
1685 (
1688 Start::External,
1689 Op::Begin,
1690 Idle,
1691 CANCEL_FROM_DRAGGING.to_vec(),
1692 ),
1693 (
1694 Start::External,
1695 Op::BeginExternal,
1696 Dragging,
1697 cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
1698 ),
1699 (Start::External, Op::Move, Dragging, vec![Moved]),
1700 (
1701 Start::External,
1702 Op::HoverSecond,
1703 Dragging,
1704 vec![EnterSecond],
1705 ),
1706 (Start::External, Op::HoverNone, Dragging, vec![]),
1707 (
1708 Start::External,
1709 Op::Drop,
1710 Idle,
1711 CANCEL_FROM_DRAGGING.to_vec(),
1712 ),
1713 (Start::External, Op::Complete, Dragging, vec![]),
1714 (
1715 Start::External,
1716 Op::Cancel,
1717 Idle,
1718 CANCEL_FROM_DRAGGING.to_vec(),
1719 ),
1720 ]
1721 }
1722
1723 #[test]
1724 fn every_transition_lands_on_the_tabled_phase_with_the_tabled_changes() {
1725 let rows = table();
1726 assert_eq!(rows.len(), 6 * 9, "every start phase x every operation");
1727 for (start, op, phase, expect) in rows {
1728 let f = Fixture::new();
1729 start.reach(&f);
1730 f.take_log();
1731 op.apply(&f);
1732 let expected: Vec<DragStateChange> = expect.iter().map(|e| e.resolve(&f)).collect();
1733 assert_eq!(f.take_log(), expected, "{start:?} + {op:?}: changes");
1734 assert_eq!(f.drag.phase(), phase, "{start:?} + {op:?}: phase");
1735 assert_eq!(
1736 f.drag.state().phase(),
1737 phase,
1738 "{start:?} + {op:?}: state snapshot agrees with phase"
1739 );
1740 assert!(
1741 f.drag.phase() != Cancelled,
1742 "{start:?} + {op:?}: Cancelled is never a resting phase"
1743 );
1744 }
1745 }
1746
1747 #[test]
1748 fn setup_paths_report_their_own_changes() {
1749 let f = Fixture::new();
1750 Start::Dropping.reach(&f);
1751 assert_eq!(
1752 f.take_log(),
1753 vec![
1754 DragStateChange::Phase {
1755 previous: Idle,
1756 next: Armed
1757 },
1758 DragStateChange::Phase {
1759 previous: Armed,
1760 next: Dragging
1761 },
1762 DragStateChange::Enter { target: f.first },
1763 DragStateChange::Phase {
1764 previous: Dragging,
1765 next: Dropping
1766 },
1767 ]
1768 );
1769 }
1770
1771 #[test]
1772 fn set_external_payload_replaces_the_held_paths_and_notifies_nothing() {
1773 let f = Fixture::new();
1774 f.drag.begin_external(Vec::new(), FILE_ENTRY);
1775 f.take_log();
1776
1777 let real_paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")];
1778 f.drag.set_external_payload(real_paths.clone());
1779 assert_eq!(
1780 f.take_log(),
1781 Vec::<DragStateChange>::new(),
1782 "replacing the payload notifies no subscriber"
1783 );
1784 assert_eq!(
1785 f.drag.with_payload::<Vec<PathBuf>, _>(Clone::clone),
1786 Some(real_paths.clone())
1787 );
1788
1789 f.drag.set_hovered(Some(f.first));
1790 f.take_log();
1791 assert_eq!(f.drag.drop(), Some(f.first));
1792 assert_eq!(
1793 f.drag.take_payload::<Vec<PathBuf>>().map(|b| *b),
1794 Some(real_paths),
1795 "the drop target receives the replaced paths, not the empty hover list"
1796 );
1797 }
1798
1799 #[test]
1800 fn set_external_payload_is_a_no_op_outside_an_external_dragging_session() {
1801 let f = Fixture::new();
1802 f.drag
1804 .set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
1805 assert_eq!(f.drag.phase(), DragPhase::Idle);
1806
1807 f.drag.arm(f.source, PRESS);
1809 f.drag.begin(7_i32);
1810 f.drag
1811 .set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
1812 assert_eq!(f.drag.with_payload::<i32, _>(|v| *v), Some(7));
1813 }
1814
1815 #[test]
1816 fn a_session_snapshot_tracks_source_pointer_hover_and_drop_target() {
1817 let f = Fixture::new();
1818 f.drag.arm(f.source, PRESS);
1819 assert_eq!(
1820 f.drag.state(),
1821 DragState::Armed {
1822 source: f.source,
1823 press: PRESS,
1824 keyboard: false,
1825 }
1826 );
1827 f.drag.begin(7_i32);
1828 f.drag.set_ghost_offset(Vec2::new(-4.0, -6.0));
1829 f.drag.update_pointer(MOVED);
1830 f.drag.set_hovered(Some(f.first));
1831 let DragState::Dragging(session) = f.drag.state() else {
1832 panic!("expected Dragging");
1833 };
1834 assert_eq!(session.kind, DragKind::Internal);
1835 assert_eq!(session.source, Some(f.source));
1836 assert_eq!(session.press, PRESS);
1837 assert_eq!(session.pointer, MOVED);
1838 assert_eq!(session.ghost_origin(), Point::new(16.0, 24.0));
1839 assert_eq!(session.hovered, Some(f.first));
1840 assert_eq!(session.payload_type, Some(TypeId::of::<i32>()));
1841
1842 assert_eq!(f.drag.drop(), Some(f.first));
1843 let DragState::Dropping { session, target } = f.drag.state() else {
1844 panic!("expected Dropping");
1845 };
1846 assert_eq!(target, f.first);
1847 assert_eq!(session.pointer, MOVED);
1848 }
1849
1850 #[test]
1851 fn a_second_begin_restarts_from_the_same_source_at_the_current_pointer() {
1852 let f = Fixture::new();
1853 f.drag.arm(f.source, PRESS);
1854 assert!(f.drag.begin(1_u8));
1855 f.drag.set_ghost_offset(Vec2::new(-2.0, -2.0));
1856 f.drag.update_pointer(MOVED);
1857 assert!(f.drag.begin("replacement"));
1858 let DragState::Dragging(session) = f.drag.state() else {
1859 panic!("expected Dragging");
1860 };
1861 assert_eq!(session.source, Some(f.source));
1862 assert_eq!(session.press, PRESS);
1863 assert_eq!(session.pointer, MOVED);
1864 assert_eq!(session.ghost_offset, Vec2::new(-2.0, -2.0));
1865 assert_eq!(session.hovered, None);
1866 assert!(f.drag.payload_is::<&str>());
1867 assert!(
1868 !f.drag.payload_is::<u8>(),
1869 "the first payload was discarded"
1870 );
1871 }
1872
1873 #[test]
1874 fn begin_without_arm_and_arm_while_dragging_report_failure() {
1875 let f = Fixture::new();
1876 assert!(!f.drag.begin(1_u8), "begin from Idle is ignored");
1877 assert!(f.drag.arm(f.source, PRESS));
1878 assert!(f.drag.begin(1_u8));
1879 assert!(
1880 !f.drag.arm(f.other_source, OTHER_PRESS),
1881 "arm while dragging is ignored"
1882 );
1883 assert_eq!(
1884 f.drag.state().session().and_then(|s| s.source),
1885 Some(f.source)
1886 );
1887 }
1888
1889 #[test]
1890 fn a_typed_payload_round_trips_and_moves_out_once() {
1891 #[derive(Debug, PartialEq)]
1892 struct Card {
1893 id: u32,
1894 title: String,
1895 }
1896 let f = Fixture::new();
1897 assert!(!f.drag.payload_is::<Card>(), "no payload while Idle");
1898 assert!(f.drag.take_payload::<Card>().is_none());
1899 f.drag.arm(f.source, PRESS);
1900 assert!(
1901 f.drag.payload_ref::<Card>().is_none(),
1902 "no payload while Armed"
1903 );
1904 f.drag.begin(Card {
1905 id: 3,
1906 title: "three".into(),
1907 });
1908
1909 assert!(f.drag.payload_is::<Card>());
1910 assert!(!f.drag.payload_is::<u32>());
1911 assert_eq!(f.drag.payload_ref::<Card>().map(|card| card.id), Some(3));
1912 assert!(
1913 f.drag.payload_ref::<String>().is_none(),
1914 "wrong type reads None"
1915 );
1916 assert_eq!(
1917 f.drag
1918 .with_payload(|card: &Card| card.title.clone())
1919 .as_deref(),
1920 Some("three")
1921 );
1922 assert_eq!(f.drag.with_payload(|n: &u32| *n), None);
1923 assert!(
1924 f.drag.take_payload::<u32>().is_none(),
1925 "a wrong-type take leaves the payload in place"
1926 );
1927 assert!(f.drag.payload_is::<Card>());
1928
1929 f.drag.set_hovered(Some(f.first));
1931 f.drag.drop();
1932 let taken = f
1933 .drag
1934 .take_payload::<Card>()
1935 .expect("the drop target claims it");
1936 assert_eq!(
1937 *taken,
1938 Card {
1939 id: 3,
1940 title: "three".into()
1941 }
1942 );
1943 assert!(
1944 f.drag.take_payload::<Card>().is_none(),
1945 "a second take is None"
1946 );
1947 assert!(!f.drag.payload_is::<Card>());
1948 assert_eq!(
1949 f.drag.state().session().and_then(|s| s.payload_type),
1950 None,
1951 "the snapshot reports the payload gone"
1952 );
1953 f.drag.complete_drop();
1954 assert_eq!(f.drag.phase(), Idle);
1955 }
1956
1957 #[test]
1958 fn a_payload_is_discarded_by_cancel_and_by_an_unclaimed_completion() {
1959 let f = Fixture::new();
1960 let probe = Rc::new(());
1961 f.drag.arm(f.source, PRESS);
1962 f.drag.begin(Rc::clone(&probe));
1963 assert_eq!(Rc::strong_count(&probe), 2);
1964 f.drag.cancel();
1965 assert_eq!(Rc::strong_count(&probe), 1, "cancel drops the payload");
1966
1967 f.drag.arm(f.source, PRESS);
1968 f.drag.begin(Rc::clone(&probe));
1969 f.drag.set_hovered(Some(f.first));
1970 f.drag.drop();
1971 assert_eq!(Rc::strong_count(&probe), 2, "still held while Dropping");
1972 f.drag.complete_drop();
1973 assert_eq!(
1974 Rc::strong_count(&probe),
1975 1,
1976 "an unclaimed payload is dropped on completion"
1977 );
1978 }
1979
1980 #[test]
1981 fn subscribers_hear_every_change_in_subscription_order() {
1982 let drag = DragCoordinator::new();
1983 let source = drag.new_source_id();
1984 let log = Rc::new(RefCell::new(Vec::new()));
1985 let subscriptions: Vec<DragSubscription> = (0..3)
1986 .map(|index| {
1987 let sink = Rc::clone(&log);
1988 drag.subscribe(move |change| sink.borrow_mut().push((index, change)))
1989 })
1990 .collect();
1991 drag.arm(source, PRESS);
1992 let armed = DragStateChange::Phase {
1993 previous: Idle,
1994 next: Armed,
1995 };
1996 assert_eq!(*log.borrow(), vec![(0, armed), (1, armed), (2, armed)]);
1997
1998 log.borrow_mut().clear();
2000 let mut subscriptions = subscriptions;
2001 drop(subscriptions.remove(1));
2002 drag.cancel();
2003 let to_cancelled = DragStateChange::Phase {
2004 previous: Armed,
2005 next: Cancelled,
2006 };
2007 let to_idle = DragStateChange::Phase {
2008 previous: Cancelled,
2009 next: Idle,
2010 };
2011 assert_eq!(
2012 *log.borrow(),
2013 vec![
2014 (0, to_cancelled),
2015 (2, to_cancelled),
2016 (0, to_idle),
2017 (2, to_idle),
2018 ]
2019 );
2020 }
2021
2022 #[test]
2023 fn a_reentrant_listener_keeps_the_global_order_for_every_subscriber() {
2024 let drag = DragCoordinator::new();
2025 let source = drag.new_source_id();
2026 let target = drag.new_target_id();
2027 let log = Rc::new(RefCell::new(Vec::new()));
2028
2029 let handle = drag.clone();
2032 let sink = Rc::clone(&log);
2033 let _first = drag.subscribe(move |change| {
2034 sink.borrow_mut().push(("first", change));
2035 if change
2036 == (DragStateChange::Phase {
2037 previous: Dragging,
2038 next: Dropping,
2039 })
2040 {
2041 assert_eq!(handle.phase(), Dropping);
2042 handle.cancel();
2043 assert_eq!(
2044 handle.phase(),
2045 Idle,
2046 "the re-entrant cancel applied at once"
2047 );
2048 }
2049 });
2050 let sink = Rc::clone(&log);
2051 let _second = drag.subscribe(move |change| sink.borrow_mut().push(("second", change)));
2052
2053 drag.arm(source, PRESS);
2054 drag.begin(());
2055 drag.set_hovered(Some(target));
2056 log.borrow_mut().clear();
2057 drag.drop();
2058
2059 let dropping = DragStateChange::Phase {
2060 previous: Dragging,
2061 next: Dropping,
2062 };
2063 let leave = DragStateChange::Leave { target };
2064 let to_cancelled = DragStateChange::Phase {
2065 previous: Dropping,
2066 next: Cancelled,
2067 };
2068 let to_idle = DragStateChange::Phase {
2069 previous: Cancelled,
2070 next: Idle,
2071 };
2072 assert_eq!(
2073 *log.borrow(),
2074 vec![
2075 ("first", dropping),
2076 ("second", dropping),
2077 ("first", leave),
2078 ("second", leave),
2079 ("first", to_cancelled),
2080 ("second", to_cancelled),
2081 ("first", to_idle),
2082 ("second", to_idle),
2083 ],
2084 "the second subscriber hears the drop before the cancel it triggered"
2085 );
2086 }
2087
2088 #[test]
2089 fn a_listener_unsubscribed_mid_dispatch_hears_nothing_more() {
2090 let drag = DragCoordinator::new();
2091 let source = drag.new_source_id();
2092 let heard = Rc::new(RefCell::new(0_u32));
2093 let victim: Rc<RefCell<Option<DragSubscription>>> = Rc::new(RefCell::new(None));
2094
2095 let slot = Rc::clone(&victim);
2096 let _killer = drag.subscribe(move |_| {
2097 slot.borrow_mut().take();
2098 });
2099 let count = Rc::clone(&heard);
2100 *victim.borrow_mut() = Some(drag.subscribe(move |_| *count.borrow_mut() += 1));
2101
2102 drag.arm(source, PRESS);
2103 assert_eq!(*heard.borrow(), 0);
2104 }
2105
2106 #[test]
2107 fn a_subscription_outliving_its_coordinator_drops_cleanly() {
2108 let drag = DragCoordinator::new();
2109 let subscription = drag.subscribe(|_| {});
2110 drop(drag);
2111 drop(subscription);
2112 }
2113
2114 #[test]
2115 fn a_panicking_listener_does_not_wedge_dispatch() {
2116 let drag = DragCoordinator::new();
2117 let source = drag.new_source_id();
2118 let heard = Rc::new(RefCell::new(Vec::new()));
2119 let sink = Rc::clone(&heard);
2120 let armed = Rc::new(std::cell::Cell::new(true));
2121 let trip = Rc::clone(&armed);
2122 let _panicky = drag.subscribe(move |_| {
2123 if trip.replace(false) {
2124 panic!("listener failure");
2125 }
2126 });
2127 let _recorder = drag.subscribe(move |change| sink.borrow_mut().push(change));
2128
2129 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2130 drag.arm(source, PRESS);
2131 }));
2132 assert!(outcome.is_err());
2133 assert_eq!(drag.phase(), Armed, "the transition itself applied");
2134 heard.borrow_mut().clear();
2135 drag.cancel();
2136 assert_eq!(
2137 *heard.borrow(),
2138 vec![
2139 DragStateChange::Phase {
2140 previous: Armed,
2141 next: Cancelled
2142 },
2143 DragStateChange::Phase {
2144 previous: Cancelled,
2145 next: Idle
2146 },
2147 ],
2148 "later changes are delivered, the undelivered one is not replayed"
2149 );
2150 }
2151
2152 #[test]
2153 fn an_external_file_session_runs_its_whole_lifecycle() {
2154 let f = Fixture::new();
2155 let paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.png")];
2156 f.drag.begin_external(paths.clone(), FILE_ENTRY);
2157 let DragState::Dragging(session) = f.drag.state() else {
2158 panic!("expected Dragging");
2159 };
2160 assert_eq!(session.kind, DragKind::ExternalFiles(paths.clone()));
2161 assert_eq!(session.source, None, "an OS drag has no source");
2162 assert_eq!(session.press, FILE_ENTRY);
2163 assert_eq!(session.pointer, FILE_ENTRY);
2164 assert!(f.drag.payload_is::<Vec<PathBuf>>());
2165
2166 f.drag.update_pointer(MOVED);
2167 f.drag.set_hovered(Some(f.first));
2168 f.drag.set_hovered(Some(f.second));
2169 assert_eq!(f.drag.drop(), Some(f.second));
2170 assert_eq!(
2171 f.drag.take_payload::<Vec<PathBuf>>().map(|paths| *paths),
2172 Some(paths)
2173 );
2174 f.drag.complete_drop();
2175 assert_eq!(
2176 f.take_log(),
2177 vec![
2178 DragStateChange::Phase {
2179 previous: Idle,
2180 next: Dragging
2181 },
2182 DragStateChange::Move { pointer: MOVED },
2183 DragStateChange::Enter { target: f.first },
2184 DragStateChange::Leave { target: f.first },
2185 DragStateChange::Enter { target: f.second },
2186 DragStateChange::Phase {
2187 previous: Dragging,
2188 next: Dropping
2189 },
2190 DragStateChange::Phase {
2191 previous: Dropping,
2192 next: Idle
2193 },
2194 ]
2195 );
2196 assert_eq!(f.drag.state(), DragState::Idle);
2197 }
2198
2199 #[test]
2200 fn an_external_session_cancelled_by_the_os_leaves_its_hovered_target() {
2201 let f = Fixture::new();
2202 f.drag
2203 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
2204 f.drag.set_hovered(Some(f.first));
2205 f.take_log();
2206 f.drag.cancel();
2207 assert_eq!(
2208 f.take_log(),
2209 vec![
2210 DragStateChange::Leave { target: f.first },
2211 DragStateChange::Phase {
2212 previous: Dragging,
2213 next: Cancelled
2214 },
2215 DragStateChange::Phase {
2216 previous: Cancelled,
2217 next: Idle
2218 },
2219 ]
2220 );
2221 assert!(!f.drag.payload_is::<Vec<PathBuf>>());
2222 }
2223
2224 #[test]
2225 fn unchanged_pointer_and_hover_notify_nothing() {
2226 let f = Fixture::new();
2227 f.drag.register_target(f.first);
2228 f.drag
2229 .set_target_bounds(f.first, Rect::from_center_size(PRESS, (20.0, 20.0)));
2230 Start::Hovering.reach(&f);
2231 f.take_log();
2232 f.drag.update_pointer(PRESS);
2233 f.drag.set_hovered(Some(f.first));
2234 assert!(f.take_log().is_empty());
2235 }
2236
2237 #[test]
2238 fn ids_are_unique_and_every_change_raises_a_flush() {
2239 let f = Fixture::new();
2240 let mut ids = vec![
2241 f.source.get(),
2242 f.other_source.get(),
2243 f.first.get(),
2244 f.second.get(),
2245 ];
2246 ids.sort_unstable();
2247 ids.dedup();
2248 assert_eq!(ids.len(), 4);
2249
2250 frust_core::take_pending_result_flush();
2251 f.drag.update_pointer(MOVED); assert!(!frust_core::take_pending_result_flush());
2253 f.drag.arm(f.source, PRESS);
2254 assert!(frust_core::take_pending_result_flush());
2255 }
2256
2257 #[test]
2258 fn clones_share_one_session() {
2259 let f = Fixture::new();
2260 let other = f.drag.clone();
2261 other.arm(f.source, PRESS);
2262 assert_eq!(f.drag.phase(), Armed);
2263 assert_eq!(
2264 f.take_log().len(),
2265 1,
2266 "a clone's change reaches the original's subscriber"
2267 );
2268 }
2269
2270 #[test]
2271 fn register_and_unregister_targets() {
2272 let f = Fixture::new();
2273 assert_eq!(f.drag.registered_targets().len(), 0);
2274 f.drag.register_target(f.first);
2275 assert_eq!(f.drag.registered_targets().len(), 1);
2276 assert!(f.drag.registered_targets().contains(&f.first));
2277 f.drag.register_target(f.first);
2279 assert_eq!(f.drag.registered_targets().len(), 1);
2280 f.drag.register_target(f.second);
2281 assert_eq!(f.drag.registered_targets().len(), 2);
2282 f.drag.unregister_target(f.first);
2283 assert_eq!(f.drag.registered_targets().len(), 1);
2284 assert!(!f.drag.registered_targets().contains(&f.first));
2285 assert!(f.drag.registered_targets().contains(&f.second));
2286 }
2287
2288 #[test]
2289 fn unregister_while_hovered_emits_leave_and_clears_hover() {
2290 let f = Fixture::new();
2291 f.drag.register_target(f.first);
2292 f.drag.register_target(f.second);
2293 f.drag.arm(f.source, PRESS);
2294 f.drag.begin(7_i32);
2295 f.drag.set_hovered(Some(f.first));
2296 f.take_log();
2297 f.drag.unregister_target(f.first);
2299 let changes = f.take_log();
2300 assert_eq!(
2301 changes,
2302 vec![DragStateChange::Leave { target: f.first }],
2303 "unregister while hovered emits Leave"
2304 );
2305 assert_eq!(
2306 f.drag.state().session().and_then(|s| s.hovered),
2307 None,
2308 "hover is cleared after unregister"
2309 );
2310 }
2311
2312 #[test]
2313 fn unregistering_the_pending_drop_target_ends_the_session() {
2314 let f = Fixture::new();
2315 f.drag.register_target(f.first);
2316 f.drag.arm(f.source, PRESS);
2317 f.drag.begin(7_i32);
2318 f.drag.set_hovered(Some(f.first));
2319 f.drag.drop();
2320 f.take_log();
2321 f.drag.unregister_target(f.first);
2322 assert_eq!(
2323 f.take_log(),
2324 vec![
2325 DragStateChange::Leave { target: f.first },
2326 DragStateChange::Phase {
2327 previous: DragPhase::Dropping,
2328 next: DragPhase::Cancelled,
2329 },
2330 DragStateChange::Phase {
2331 previous: DragPhase::Cancelled,
2332 next: DragPhase::Idle,
2333 },
2334 ],
2335 "one Leave, then the session is cancelled"
2336 );
2337 assert_eq!(f.drag.state(), DragState::Idle);
2338 assert!(
2339 !f.drag.payload_is::<i32>(),
2340 "the unclaimed payload is discarded"
2341 );
2342 f.drag.complete_drop();
2344 f.drag.cancel();
2345 assert!(f.take_log().is_empty());
2346 }
2347
2348 #[test]
2349 fn unregistering_another_target_while_dropping_changes_nothing() {
2350 let f = Fixture::new();
2351 f.drag.register_target(f.first);
2352 f.drag.register_target(f.second);
2353 f.drag.arm(f.source, PRESS);
2354 f.drag.begin(7_i32);
2355 f.drag.set_hovered(Some(f.first));
2356 f.drag.drop();
2357 f.take_log();
2358 f.drag.unregister_target(f.second);
2359 assert!(f.take_log().is_empty());
2360 assert_eq!(f.drag.phase(), DragPhase::Dropping);
2361 }
2362
2363 #[test]
2364 fn set_and_query_target_bounds() {
2365 let f = Fixture::new();
2366 f.drag.register_target(f.first);
2367 assert_eq!(f.drag.target_bounds(f.first), None, "unset bounds are None");
2368 let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
2369 f.drag.set_target_bounds(f.first, rect);
2370 assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2371 let new_rect = Rect::from_origin_size(Point::new(5.0, 10.0), (200.0, 100.0));
2372 f.drag.set_target_bounds(f.first, new_rect);
2373 assert_eq!(f.drag.target_bounds(f.first), Some(new_rect));
2374 }
2375
2376 #[test]
2377 fn set_target_bounds_on_unknown_id_is_ignored() {
2378 let f = Fixture::new();
2379 let unregistered = f.drag.new_target_id();
2380 let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
2381 f.drag.set_target_bounds(unregistered, rect);
2383 assert_eq!(f.drag.target_bounds(unregistered), None);
2384 }
2385
2386 #[test]
2387 fn target_at_finds_single_containing_target() {
2388 let f = Fixture::new();
2389 f.drag.register_target(f.first);
2390 let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
2391 f.drag.set_target_bounds(f.first, rect);
2392 let point = Point::new(50.0, 50.0);
2393 assert_eq!(
2394 f.drag.target_at(point),
2395 Some(f.first),
2396 "point inside bounds returns the target"
2397 );
2398 let outside = Point::new(200.0, 200.0);
2399 assert_eq!(
2400 f.drag.target_at(outside),
2401 None,
2402 "point outside all bounds returns None"
2403 );
2404 }
2405
2406 #[test]
2407 fn a_nested_target_registered_after_its_container_wins() {
2408 let f = Fixture::new();
2409 f.drag.register_target(f.first);
2410 f.drag.register_target(f.second);
2411 let large = Rect::from_origin_size(Point::new(0.0, 0.0), (200.0, 200.0));
2413 f.drag.set_target_bounds(f.first, large);
2414 let small = Rect::from_origin_size(Point::new(50.0, 50.0), (100.0, 100.0));
2416 f.drag.set_target_bounds(f.second, small);
2417 assert_eq!(f.drag.target_at(Point::new(100.0, 100.0)), Some(f.second));
2418 assert_eq!(
2419 f.drag.target_at(Point::new(10.0, 10.0)),
2420 Some(f.first),
2421 "outside the nested target the container still resolves"
2422 );
2423 }
2424
2425 #[test]
2426 fn two_overlapping_targets_resolve_to_the_later_registration() {
2427 let f = Fixture::new();
2428 f.drag.register_target(f.second);
2431 f.drag.register_target(f.first);
2432 f.drag
2433 .set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
2434 f.drag
2435 .set_target_bounds(f.first, Rect::new(50.0, 50.0, 300.0, 300.0));
2436 let overlap = Point::new(75.0, 75.0);
2437 assert_eq!(f.drag.target_at(overlap), Some(f.first));
2438 assert_eq!(f.drag.registered_targets(), vec![f.second, f.first]);
2439
2440 f.drag.arm(f.source, PRESS);
2442 f.drag.begin(1_u8);
2443 f.take_log();
2444 f.drag.update_pointer(overlap);
2445 assert_eq!(
2446 f.take_log(),
2447 vec![
2448 DragStateChange::Enter { target: f.first },
2449 DragStateChange::Move { pointer: overlap },
2450 ]
2451 );
2452
2453 f.drag.register_target(f.second);
2456 assert_eq!(f.drag.target_at(overlap), Some(f.first));
2457 f.drag.unregister_target(f.second);
2458 f.drag.register_target(f.second);
2459 f.drag
2460 .set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
2461 assert_eq!(f.drag.target_at(overlap), Some(f.second));
2462 }
2463
2464 #[test]
2465 fn moving_across_three_targets_enters_and_leaves_each_in_turn() {
2466 let f = Fixture::new();
2467 let third = f.drag.new_target_id();
2468 let columns = [
2470 (f.first, Rect::new(0.0, 0.0, 100.0, 300.0)),
2471 (f.second, Rect::new(100.0, 0.0, 200.0, 300.0)),
2472 (third, Rect::new(220.0, 0.0, 320.0, 300.0)),
2473 ];
2474 for (id, rect) in columns {
2475 f.drag.register_target(id);
2476 f.drag.set_target_bounds(id, rect);
2477 }
2478 f.drag.arm(f.source, Point::new(50.0, 10.0));
2479 f.drag.begin(1_u8);
2480 f.take_log();
2481
2482 let path = [
2483 Point::new(50.0, 50.0), Point::new(60.0, 60.0), Point::new(150.0, 60.0), Point::new(210.0, 60.0), Point::new(250.0, 60.0), Point::new(250.0, 400.0), ];
2490 for point in path {
2491 f.drag.update_pointer(point);
2492 }
2493 let mv = |pointer: Point| DragStateChange::Move { pointer };
2494 assert_eq!(
2495 f.take_log(),
2496 vec![
2497 DragStateChange::Enter { target: f.first },
2498 mv(path[0]),
2499 mv(path[1]),
2500 DragStateChange::Leave { target: f.first },
2501 DragStateChange::Enter { target: f.second },
2502 mv(path[2]),
2503 DragStateChange::Leave { target: f.second },
2504 mv(path[3]),
2505 DragStateChange::Enter { target: third },
2506 mv(path[4]),
2507 DragStateChange::Leave { target: third },
2508 mv(path[5]),
2509 ]
2510 );
2511 assert_eq!(
2512 f.drag.drop(),
2513 None,
2514 "released outside every target: a cancel"
2515 );
2516 assert_eq!(f.drag.phase(), Idle);
2517 }
2518
2519 #[test]
2520 fn an_explicit_hover_stands_until_the_next_resolution() {
2521 let f = Fixture::new();
2522 f.drag.register_target(f.first);
2523 f.drag
2524 .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2525 Start::Dragging.reach(&f);
2526 f.drag.set_hovered(Some(f.second));
2527 assert_eq!(
2528 f.drag.state().session().and_then(|s| s.hovered),
2529 Some(f.second),
2530 "the override applies at once"
2531 );
2532 f.take_log();
2533 f.drag.update_pointer(Point::new(40.0, 40.0));
2534 assert_eq!(
2535 f.take_log(),
2536 vec![
2537 DragStateChange::Leave { target: f.second },
2538 DragStateChange::Enter { target: f.first },
2539 DragStateChange::Move {
2540 pointer: Point::new(40.0, 40.0)
2541 },
2542 ]
2543 );
2544 }
2545
2546 #[test]
2547 fn resolve_hover_follows_bounds_that_move_under_a_still_pointer() {
2548 let f = Fixture::new();
2549 f.drag.register_target(f.first);
2550 f.drag.register_target(f.second);
2551 f.drag
2552 .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 50.0));
2553 f.drag
2554 .set_target_bounds(f.second, Rect::new(0.0, 50.0, 100.0, 100.0));
2555 f.drag.arm(f.source, PRESS);
2556 f.drag.begin(1_u8);
2557 f.drag.update_pointer(Point::new(10.0, 40.0));
2558 f.take_log();
2559
2560 f.drag
2563 .set_target_bounds(f.first, Rect::new(0.0, -20.0, 100.0, 30.0));
2564 f.drag
2565 .set_target_bounds(f.second, Rect::new(0.0, 30.0, 100.0, 80.0));
2566 assert!(f.take_log().is_empty(), "reporting bounds notifies nothing");
2567 f.drag.resolve_hover();
2568 assert_eq!(
2569 f.take_log(),
2570 vec![
2571 DragStateChange::Leave { target: f.first },
2572 DragStateChange::Enter { target: f.second },
2573 ]
2574 );
2575 f.drag.resolve_hover();
2576 assert!(f.take_log().is_empty(), "an unchanged hover is silent");
2577
2578 f.drag.drop();
2580 f.take_log();
2581 f.drag.resolve_hover();
2582 assert!(f.take_log().is_empty());
2583 }
2584
2585 #[test]
2586 fn target_at_ignores_targets_without_bounds() {
2587 let f = Fixture::new();
2588 f.drag.register_target(f.first);
2589 f.drag.register_target(f.second);
2590 let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (100.0, 100.0));
2592 f.drag.set_target_bounds(f.second, rect);
2593 let point = Point::new(50.0, 50.0);
2594 assert_eq!(
2595 f.drag.target_at(point),
2596 Some(f.second),
2597 "ignores registered targets without bounds set"
2598 );
2599 }
2600
2601 #[test]
2602 fn target_bounds_persist_across_drag_sessions() {
2603 let f = Fixture::new();
2604 f.drag.register_target(f.first);
2605 let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
2606 f.drag.set_target_bounds(f.first, rect);
2607 f.drag.arm(f.source, PRESS);
2609 f.drag.begin(7_i32);
2610 f.drag.update_pointer(MOVED);
2611 f.drag.drop();
2612 f.drag.complete_drop();
2613 assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2615 }
2616
2617 #[test]
2618 fn lift_begins_a_keyboard_session_with_no_pointer_resolution() {
2619 let f = Fixture::new();
2620 f.drag.register_target(f.first);
2621 f.drag
2622 .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2623 assert!(f.drag.lift(f.source));
2624 assert_eq!(
2625 f.drag.state(),
2626 DragState::Armed {
2627 source: f.source,
2628 press: Point::ZERO,
2629 keyboard: true,
2630 }
2631 );
2632 assert!(f.drag.begin(9_u8));
2633 let DragState::Dragging(session) = f.drag.state() else {
2634 panic!("expected Dragging");
2635 };
2636 assert!(session.keyboard, "begin carries the keyboard flag over");
2637 assert_eq!(session.hovered, None);
2638
2639 f.take_log();
2643 f.drag.update_pointer(Point::new(50.0, 50.0));
2644 assert!(f.take_log().is_empty());
2645 assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2646 f.drag.resolve_hover();
2647 assert!(f.take_log().is_empty());
2648 }
2649
2650 #[test]
2651 fn move_to_next_and_previous_target_cycle_in_registration_order_and_wrap() {
2652 let f = Fixture::new();
2653 let third = f.drag.new_target_id();
2654 f.drag.register_target(f.first);
2655 f.drag.register_target(f.second);
2656 f.drag.register_target(third);
2657 for id in [f.first, f.second, third] {
2658 f.drag
2659 .set_target_bounds(id, Rect::new(0.0, 0.0, 10.0, 10.0));
2660 }
2661 f.drag.lift(f.source);
2662 f.drag.begin(1_u8);
2663 f.take_log();
2664
2665 f.drag.move_to_next_target();
2666 assert_eq!(
2667 f.drag.state().session().and_then(|s| s.hovered),
2668 Some(f.first),
2669 "starts at the first registered target"
2670 );
2671 f.drag.move_to_next_target();
2672 assert_eq!(
2673 f.drag.state().session().and_then(|s| s.hovered),
2674 Some(f.second)
2675 );
2676 f.drag.move_to_next_target();
2677 assert_eq!(
2678 f.drag.state().session().and_then(|s| s.hovered),
2679 Some(third)
2680 );
2681 f.drag.move_to_next_target();
2682 assert_eq!(
2683 f.drag.state().session().and_then(|s| s.hovered),
2684 Some(f.first),
2685 "wraps back to the first"
2686 );
2687 assert_eq!(
2688 f.take_log(),
2689 vec![
2690 DragStateChange::Enter { target: f.first },
2691 DragStateChange::Leave { target: f.first },
2692 DragStateChange::Enter { target: f.second },
2693 DragStateChange::Leave { target: f.second },
2694 DragStateChange::Enter { target: third },
2695 DragStateChange::Leave { target: third },
2696 DragStateChange::Enter { target: f.first },
2697 ]
2698 );
2699
2700 f.drag.move_to_previous_target();
2701 assert_eq!(
2702 f.drag.state().session().and_then(|s| s.hovered),
2703 Some(third),
2704 "wraps back to the last going the other way"
2705 );
2706 }
2707
2708 #[test]
2709 fn move_to_previous_target_starts_at_the_last_when_nothing_is_hovered() {
2710 let f = Fixture::new();
2711 f.drag.register_target(f.first);
2712 f.drag.register_target(f.second);
2713 f.drag
2714 .set_target_bounds(f.first, Rect::new(0.0, 0.0, 10.0, 10.0));
2715 f.drag
2716 .set_target_bounds(f.second, Rect::new(0.0, 0.0, 10.0, 10.0));
2717 f.drag.lift(f.source);
2718 f.drag.begin(1_u8);
2719 f.drag.move_to_previous_target();
2720 assert_eq!(
2721 f.drag.state().session().and_then(|s| s.hovered),
2722 Some(f.second)
2723 );
2724 }
2725
2726 #[test]
2727 fn move_target_is_a_no_op_outside_dragging_or_with_no_targets() {
2728 let f = Fixture::new();
2729 f.drag.move_to_next_target();
2730 assert_eq!(f.drag.phase(), DragPhase::Idle);
2731 f.drag.lift(f.source);
2732 f.take_log();
2733 f.drag.move_to_next_target();
2734 assert!(
2735 f.take_log().is_empty(),
2736 "Armed is not Dragging yet — still a no-op"
2737 );
2738 f.drag.begin(1_u8);
2739 f.take_log();
2740 f.drag.move_to_next_target();
2741 assert!(f.take_log().is_empty(), "no registered targets to cycle");
2742 }
2743
2744 #[test]
2745 fn lift_while_dragging_is_ignored() {
2746 let f = Fixture::new();
2747 f.drag.arm(f.source, PRESS);
2748 f.drag.begin(1_u8);
2749 assert!(!f.drag.lift(f.other_source));
2750 assert_eq!(
2751 f.drag.state().session().and_then(|s| s.source),
2752 Some(f.source)
2753 );
2754 }
2755
2756 fn frame(n: u64) -> FrameTime {
2758 FrameTime::from_nanos(n * 16_000_000)
2759 }
2760
2761 #[test]
2762 fn resolution_skips_a_later_registered_target_of_another_payload_type() {
2763 let f = Fixture::new();
2764 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2765 f.drag.register_target_for::<u8>(f.first);
2766 f.drag.register_target_for::<u32>(f.second);
2767 f.drag.set_target_bounds(f.first, rect);
2768 f.drag.set_target_bounds(f.second, rect);
2769 let inside = Point::new(50.0, 50.0);
2770 assert_eq!(
2771 f.drag.target_at(inside),
2772 Some(f.second),
2773 "with no session, the later registration wins as before"
2774 );
2775
2776 f.drag.arm(f.source, PRESS);
2777 f.drag.begin(3_u8);
2778 assert_eq!(f.drag.target_at(inside), Some(f.first));
2779 f.take_log();
2780 f.drag.update_pointer(inside);
2781 assert_eq!(
2782 f.take_log(),
2783 vec![
2784 DragStateChange::Enter { target: f.first },
2785 DragStateChange::Move { pointer: inside },
2786 ],
2787 "the u8 session resolves onto the u8 target under the u32 one"
2788 );
2789 f.drag.resolve_hover();
2790 assert_eq!(
2791 f.drag.state().session().and_then(|s| s.hovered),
2792 Some(f.first)
2793 );
2794 }
2795
2796 #[test]
2797 fn resolution_finds_nothing_when_only_other_types_are_under_the_pointer() {
2798 let f = Fixture::new();
2799 f.drag.register_target_for::<u32>(f.first);
2800 f.drag
2801 .set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
2802 f.drag.arm(f.source, PRESS);
2803 f.drag.begin("text");
2804 f.drag.update_pointer(Point::new(50.0, 50.0));
2805 assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2806 assert_eq!(f.drag.drop(), None, "released over nothing: a cancel");
2807 assert_eq!(f.drag.phase(), DragPhase::Idle);
2808 }
2809
2810 #[test]
2811 fn an_external_session_resolves_only_path_list_targets() {
2812 let f = Fixture::new();
2813 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2814 f.drag.register_target_for::<Vec<PathBuf>>(f.first);
2815 f.drag.register_target_for::<u32>(f.second);
2816 f.drag.set_target_bounds(f.first, rect);
2817 f.drag.set_target_bounds(f.second, rect);
2818 f.drag
2819 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
2820 f.drag.update_pointer(Point::new(50.0, 50.0));
2821 assert_eq!(
2822 f.drag.state().session().and_then(|s| s.hovered),
2823 Some(f.first)
2824 );
2825 }
2826
2827 #[test]
2828 fn keyboard_cycling_skips_targets_of_another_payload_type() {
2829 let f = Fixture::new();
2830 let third = f.drag.new_target_id();
2831 let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2832 f.drag.register_target_for::<u32>(f.first);
2833 f.drag.register_target_for::<u8>(f.second);
2834 f.drag.register_target(third);
2835 for id in [f.first, f.second, third] {
2836 f.drag.set_target_bounds(id, rect);
2837 }
2838 f.drag.lift(f.source);
2839 f.drag.begin(1_u8);
2840 let hovered = || f.drag.state().session().and_then(|s| s.hovered);
2841 f.drag.move_to_next_target();
2842 assert_eq!(hovered(), Some(f.second), "the u32 target is skipped");
2843 f.drag.move_to_next_target();
2844 assert_eq!(hovered(), Some(third), "an untyped target accepts any");
2845 f.drag.move_to_next_target();
2846 assert_eq!(hovered(), Some(f.second), "wraps past the u32 target again");
2847 f.drag.move_to_previous_target();
2848 assert_eq!(hovered(), Some(third));
2849 }
2850
2851 #[test]
2852 fn a_target_clipped_to_nothing_is_not_resolved_or_cycled() {
2853 let f = Fixture::new();
2854 f.drag.register_target(f.first);
2855 f.drag.register_target(f.second);
2856 f.drag
2857 .report_target_bounds(f.first, Some(Rect::new(0.0, 0.0, 100.0, 100.0)), frame(1));
2858 f.drag.report_target_bounds(f.second, None, frame(1));
2859 assert_eq!(f.drag.target_bounds(f.second), None);
2860 assert_eq!(f.drag.target_at(Point::new(50.0, 50.0)), Some(f.first));
2861
2862 f.drag.lift(f.source);
2863 f.drag.begin(1_u8);
2864 f.drag.move_to_next_target();
2865 f.drag.move_to_next_target();
2866 assert_eq!(
2867 f.drag.state().session().and_then(|s| s.hovered),
2868 Some(f.first),
2869 "the clipped target is never cycled to"
2870 );
2871 }
2872
2873 #[test]
2874 fn a_target_that_stopped_reporting_is_not_resolved() {
2875 let f = Fixture::new();
2876 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2877 let inside = Point::new(50.0, 50.0);
2878 f.drag.register_target(f.first);
2879 f.drag.register_target(f.second);
2880 f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2881 f.drag.report_target_bounds(
2882 f.second,
2883 Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
2884 frame(1),
2885 );
2886 assert_eq!(f.drag.target_at(inside), Some(f.first));
2887
2888 f.drag.report_target_bounds(
2891 f.second,
2892 Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
2893 frame(2),
2894 );
2895 assert_eq!(f.drag.target_bounds(f.first), Some(rect));
2896 assert_eq!(f.drag.target_at(inside), None);
2897
2898 f.drag.arm(f.source, PRESS);
2899 f.drag.begin(1_u8);
2900 f.drag.update_pointer(inside);
2901 assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
2902
2903 f.drag.report_target_bounds(f.first, Some(rect), frame(3));
2905 f.drag.resolve_hover();
2906 assert_eq!(
2907 f.drag.state().session().and_then(|s| s.hovered),
2908 Some(f.first)
2909 );
2910 }
2911
2912 #[test]
2913 fn resolve_hover_tolerates_a_target_one_pass_behind_but_not_two() {
2914 let f = Fixture::new();
2915 let hovered_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
2916 let elsewhere = Rect::new(200.0, 0.0, 300.0, 100.0);
2917 let point = Point::new(50.0, 50.0);
2918 f.drag.register_target(f.first);
2919 f.drag.register_target(f.second);
2920 f.drag
2921 .report_target_bounds(f.first, Some(elsewhere), frame(1));
2922 f.drag
2923 .report_target_bounds(f.second, Some(hovered_rect), frame(1));
2924 f.drag.arm(f.source, PRESS);
2925 f.drag.begin(1_u8);
2926 f.drag.update_pointer(point);
2927 assert_eq!(
2928 f.drag.state().session().and_then(|s| s.hovered),
2929 Some(f.second)
2930 );
2931 f.take_log();
2932
2933 f.drag
2939 .report_target_bounds(f.first, Some(elsewhere), frame(2));
2940 f.drag.resolve_hover();
2941 assert!(
2942 f.take_log().is_empty(),
2943 "a target one pass behind is tolerated, not treated as culled"
2944 );
2945 assert_eq!(
2946 f.drag.state().session().and_then(|s| s.hovered),
2947 Some(f.second)
2948 );
2949
2950 f.drag
2955 .report_target_bounds(f.first, Some(elsewhere), frame(3));
2956 f.drag.resolve_hover();
2957 assert_eq!(
2958 f.take_log(),
2959 vec![DragStateChange::Leave { target: f.second }],
2960 "two full passes without repainting is culled, not tolerated"
2961 );
2962 }
2963
2964 #[test]
2965 fn a_stale_target_is_not_cycled_to() {
2966 let f = Fixture::new();
2967 let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2968 f.drag.register_target(f.first);
2969 f.drag.register_target(f.second);
2970 f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2971 f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2972 f.drag.report_target_bounds(f.second, Some(rect), frame(2));
2973 f.drag.lift(f.source);
2974 f.drag.begin(1_u8);
2975 f.drag.move_to_next_target();
2976 assert_eq!(
2977 f.drag.state().session().and_then(|s| s.hovered),
2978 Some(f.second)
2979 );
2980 f.drag.move_to_next_target();
2981 assert_eq!(
2982 f.drag.state().session().and_then(|s| s.hovered),
2983 Some(f.second),
2984 "only the target painted in the latest pass is cycled"
2985 );
2986 }
2987
2988 #[test]
2989 fn painting_twice_in_one_frame_keeps_the_pass_open() {
2990 let f = Fixture::new();
2991 let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
2992 f.drag.register_target(f.first);
2993 f.drag.register_target(f.second);
2994 f.drag.report_target_bounds(f.first, Some(rect), frame(1));
2995 f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2996 f.drag.report_target_bounds(f.second, Some(rect), frame(1));
2997 assert_eq!(
2998 f.drag.target_at(Point::new(5.0, 5.0)),
2999 Some(f.second),
3000 "same frame time, same pass: both still resolve"
3001 );
3002 f.drag.unregister_target(f.second);
3003 assert_eq!(f.drag.target_at(Point::new(5.0, 5.0)), Some(f.first));
3004 }
3005
3006 #[test]
3007 fn an_unclocked_repeat_report_opens_the_next_pass() {
3008 let f = Fixture::new();
3009 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
3010 let inside = Point::new(50.0, 50.0);
3011 f.drag.register_target(f.first);
3012 f.drag.register_target(f.second);
3013 f.drag.set_target_bounds(f.first, rect);
3014 f.drag
3015 .set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
3016 assert_eq!(f.drag.target_at(inside), Some(f.first));
3017 f.drag
3019 .set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
3020 assert_eq!(f.drag.target_at(inside), None);
3021 f.drag
3023 .report_target_bounds(f.first, Some(rect), FrameTime::ZERO);
3024 assert_eq!(f.drag.target_at(inside), Some(f.first));
3025 }
3026
3027 #[test]
3028 fn later_registration_still_wins_among_visible_type_matching_overlaps() {
3029 let f = Fixture::new();
3030 let third = f.drag.new_target_id();
3031 let fourth = f.drag.new_target_id();
3032 let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
3033 let inside = Point::new(50.0, 50.0);
3034 f.drag.register_target_for::<u8>(f.first);
3035 f.drag.register_target_for::<u8>(f.second);
3036 f.drag.register_target_for::<u8>(third);
3038 f.drag.register_target_for::<u32>(fourth);
3039 f.drag.report_target_bounds(third, Some(rect), frame(1));
3040 for id in [f.first, f.second, fourth] {
3041 f.drag.report_target_bounds(id, Some(rect), frame(2));
3042 }
3043 f.drag.arm(f.source, PRESS);
3044 f.drag.begin(1_u8);
3045 f.drag.update_pointer(inside);
3046 assert_eq!(
3047 f.drag.state().session().and_then(|s| s.hovered),
3048 Some(f.second)
3049 );
3050 }
3051
3052 #[test]
3053 fn is_external_reports_the_kind_of_the_session_in_flight() {
3054 let f = Fixture::new();
3055 assert!(!f.drag.is_external());
3056 f.drag.arm(f.source, PRESS);
3057 assert!(!f.drag.is_external());
3058 f.drag.begin(1_u8);
3059 assert!(!f.drag.is_external());
3060 f.drag.cancel();
3061 f.drag.register_target(f.first);
3062 f.drag
3063 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3064 assert!(f.drag.is_external());
3065 f.drag.set_hovered(Some(f.first));
3066 f.drag.drop();
3067 assert!(f.drag.is_external(), "still external while Dropping");
3068 f.drag.complete_drop();
3069 assert!(!f.drag.is_external());
3070 }
3071
3072 #[test]
3073 fn end_external_ends_a_hovering_external_session_once() {
3074 let f = Fixture::new();
3075 f.drag
3076 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3077 f.drag.set_hovered(Some(f.first));
3078 f.take_log();
3079
3080 assert!(f.drag.end_external(), "the first call ends it");
3081 assert_eq!(
3082 f.take_log(),
3083 vec![
3084 DragStateChange::Leave { target: f.first },
3085 DragStateChange::Phase {
3086 previous: Dragging,
3087 next: Cancelled
3088 },
3089 DragStateChange::Phase {
3090 previous: Cancelled,
3091 next: Idle
3092 },
3093 ]
3094 );
3095 assert_eq!(f.drag.state(), DragState::Idle);
3096
3097 assert!(!f.drag.end_external(), "every later call is a no-op");
3098 assert!(f.take_log().is_empty());
3099 }
3100
3101 #[test]
3102 fn end_external_leaves_internal_armed_and_dropping_sessions_alone() {
3103 let f = Fixture::new();
3104 assert!(!f.drag.end_external(), "nothing in flight");
3105
3106 f.drag.arm(f.source, PRESS);
3107 assert!(!f.drag.end_external());
3108 assert_eq!(f.drag.phase(), DragPhase::Armed);
3109
3110 f.drag.begin(1_u8);
3111 assert!(!f.drag.end_external());
3112 assert_eq!(f.drag.phase(), DragPhase::Dragging);
3113 f.drag.cancel();
3114
3115 assert!(f.drag.lift(f.source), "a keyboard session");
3116 f.drag.begin(2_u8);
3117 assert!(!f.drag.end_external());
3118 assert!(
3119 f.drag.payload_is::<u8>(),
3120 "the keyboard session is untouched"
3121 );
3122 f.drag.cancel();
3123
3124 f.drag
3125 .begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
3126 f.drag.set_hovered(Some(f.first));
3127 assert_eq!(f.drag.drop(), Some(f.first));
3128 f.take_log();
3129 assert!(
3130 !f.drag.end_external(),
3131 "a released drop is its target's to claim"
3132 );
3133 assert!(f.take_log().is_empty());
3134 assert_eq!(f.drag.phase(), DragPhase::Dropping);
3135 }
3136}