1#![forbid(unsafe_code)]
2
3use ftui_layout::{
12 PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS, PANE_DRAG_RESIZE_DEFAULT_THRESHOLD, PaneCancelReason,
13 PaneDragResizeMachine, PaneDragResizeMachineError, PaneDragResizeState,
14 PaneDragResizeTransition, PaneModifierSnapshot, PanePointerButton, PanePointerPosition,
15 PaneResizeTarget, PaneSemanticInputEvent, PaneSemanticInputEventKind,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct PanePointerCaptureConfig {
21 pub drag_threshold: u16,
23 pub update_hysteresis: u16,
25 pub activation_button: PanePointerButton,
27 pub cancel_on_leave_without_capture: bool,
29}
30
31impl Default for PanePointerCaptureConfig {
32 fn default() -> Self {
33 Self {
34 drag_threshold: PANE_DRAG_RESIZE_DEFAULT_THRESHOLD,
35 update_hysteresis: PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS,
36 activation_button: PanePointerButton::Primary,
37 cancel_on_leave_without_capture: true,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum CaptureState {
44 Requested,
45 Acquired,
46}
47
48impl CaptureState {
49 const fn is_acquired(self) -> bool {
50 matches!(self, Self::Acquired)
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55struct ActivePointerCapture {
56 pointer_id: u32,
57 target: PaneResizeTarget,
58 button: PanePointerButton,
59 last_position: PanePointerPosition,
60 capture_state: CaptureState,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64struct DispatchContext {
65 phase: PanePointerLifecyclePhase,
66 pointer_id: Option<u32>,
67 target: Option<PaneResizeTarget>,
68 position: Option<PanePointerPosition>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum PanePointerCaptureCommand {
74 Acquire { pointer_id: u32 },
75 Release { pointer_id: u32 },
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum PanePointerLifecyclePhase {
81 PointerDown,
82 PointerMove,
83 PointerUp,
84 PointerCancel,
85 PointerLeave,
86 Blur,
87 VisibilityHidden,
88 LostPointerCapture,
89 CaptureAcquired,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum PanePointerIgnoredReason {
95 InvalidPointerId,
96 ButtonNotAllowed,
97 ButtonMismatch,
98 ActivePointerAlreadyInProgress,
99 NoActivePointer,
100 PointerMismatch,
101 LeaveWhileCaptured,
102 MachineRejectedEvent,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum PanePointerLogOutcome {
108 SemanticForwarded,
109 CaptureStateUpdated,
110 Ignored(PanePointerIgnoredReason),
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct PanePointerLogEntry {
116 pub phase: PanePointerLifecyclePhase,
117 pub sequence: Option<u64>,
118 pub pointer_id: Option<u32>,
119 pub target: Option<PaneResizeTarget>,
120 pub position: Option<PanePointerPosition>,
121 pub capture_command: Option<PanePointerCaptureCommand>,
122 pub outcome: PanePointerLogOutcome,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct PanePointerDispatch {
128 pub semantic_event: Option<PaneSemanticInputEvent>,
129 pub transition: Option<PaneDragResizeTransition>,
130 pub capture_command: Option<PanePointerCaptureCommand>,
131 pub log: PanePointerLogEntry,
132}
133
134impl PanePointerDispatch {
135 fn ignored(
136 phase: PanePointerLifecyclePhase,
137 reason: PanePointerIgnoredReason,
138 pointer_id: Option<u32>,
139 target: Option<PaneResizeTarget>,
140 position: Option<PanePointerPosition>,
141 ) -> Self {
142 Self {
143 semantic_event: None,
144 transition: None,
145 capture_command: None,
146 log: PanePointerLogEntry {
147 phase,
148 sequence: None,
149 pointer_id,
150 target,
151 position,
152 capture_command: None,
153 outcome: PanePointerLogOutcome::Ignored(reason),
154 },
155 }
156 }
157
158 fn capture_state_updated(
159 phase: PanePointerLifecyclePhase,
160 pointer_id: u32,
161 target: PaneResizeTarget,
162 ) -> Self {
163 Self {
164 semantic_event: None,
165 transition: None,
166 capture_command: None,
167 log: PanePointerLogEntry {
168 phase,
169 sequence: None,
170 pointer_id: Some(pointer_id),
171 target: Some(target),
172 position: None,
173 capture_command: None,
174 outcome: PanePointerLogOutcome::CaptureStateUpdated,
175 },
176 }
177 }
178}
179
180#[derive(Debug, Clone)]
186pub struct PanePointerCaptureAdapter {
187 machine: PaneDragResizeMachine,
188 config: PanePointerCaptureConfig,
189 active: Option<ActivePointerCapture>,
190 next_sequence: u64,
191}
192
193impl PanePointerCaptureAdapter {
194 pub fn new(config: PanePointerCaptureConfig) -> Result<Self, PaneDragResizeMachineError> {
196 let machine = PaneDragResizeMachine::new_with_hysteresis(
197 config.drag_threshold,
198 config.update_hysteresis,
199 )?;
200 Ok(Self {
201 machine,
202 config,
203 active: None,
204 next_sequence: 1,
205 })
206 }
207
208 #[must_use]
210 pub const fn config(&self) -> PanePointerCaptureConfig {
211 self.config
212 }
213
214 #[must_use]
216 pub fn active_pointer_id(&self) -> Option<u32> {
217 self.active.map(|active| active.pointer_id)
218 }
219
220 #[must_use]
222 pub const fn machine_state(&self) -> PaneDragResizeState {
223 self.machine.state()
224 }
225
226 pub fn pointer_down(
228 &mut self,
229 target: PaneResizeTarget,
230 pointer_id: u32,
231 button: PanePointerButton,
232 position: PanePointerPosition,
233 modifiers: PaneModifierSnapshot,
234 ) -> PanePointerDispatch {
235 if pointer_id == 0 {
236 return PanePointerDispatch::ignored(
237 PanePointerLifecyclePhase::PointerDown,
238 PanePointerIgnoredReason::InvalidPointerId,
239 Some(pointer_id),
240 Some(target),
241 Some(position),
242 );
243 }
244 if button != self.config.activation_button {
245 return PanePointerDispatch::ignored(
246 PanePointerLifecyclePhase::PointerDown,
247 PanePointerIgnoredReason::ButtonNotAllowed,
248 Some(pointer_id),
249 Some(target),
250 Some(position),
251 );
252 }
253 if self.active.is_some() {
254 return PanePointerDispatch::ignored(
255 PanePointerLifecyclePhase::PointerDown,
256 PanePointerIgnoredReason::ActivePointerAlreadyInProgress,
257 Some(pointer_id),
258 Some(target),
259 Some(position),
260 );
261 }
262
263 let kind = PaneSemanticInputEventKind::PointerDown {
264 target,
265 pointer_id,
266 button,
267 position,
268 };
269 let dispatch = self.forward_semantic(
270 DispatchContext {
271 phase: PanePointerLifecyclePhase::PointerDown,
272 pointer_id: Some(pointer_id),
273 target: Some(target),
274 position: Some(position),
275 },
276 kind,
277 modifiers,
278 Some(PanePointerCaptureCommand::Acquire { pointer_id }),
279 );
280 if dispatch.transition.is_some() {
281 self.active = Some(ActivePointerCapture {
282 pointer_id,
283 target,
284 button,
285 last_position: position,
286 capture_state: CaptureState::Requested,
287 });
288 }
289 dispatch
290 }
291
292 pub fn capture_acquired(&mut self, pointer_id: u32) -> PanePointerDispatch {
294 let Some(mut active) = self.active else {
295 return PanePointerDispatch::ignored(
296 PanePointerLifecyclePhase::CaptureAcquired,
297 PanePointerIgnoredReason::NoActivePointer,
298 Some(pointer_id),
299 None,
300 None,
301 );
302 };
303 if active.pointer_id != pointer_id {
304 return PanePointerDispatch::ignored(
305 PanePointerLifecyclePhase::CaptureAcquired,
306 PanePointerIgnoredReason::PointerMismatch,
307 Some(pointer_id),
308 Some(active.target),
309 None,
310 );
311 }
312 active.capture_state = CaptureState::Acquired;
313 self.active = Some(active);
314 PanePointerDispatch::capture_state_updated(
315 PanePointerLifecyclePhase::CaptureAcquired,
316 pointer_id,
317 active.target,
318 )
319 }
320
321 pub fn pointer_move(
323 &mut self,
324 pointer_id: u32,
325 position: PanePointerPosition,
326 modifiers: PaneModifierSnapshot,
327 ) -> PanePointerDispatch {
328 let Some(mut active) = self.active else {
329 return PanePointerDispatch::ignored(
330 PanePointerLifecyclePhase::PointerMove,
331 PanePointerIgnoredReason::NoActivePointer,
332 Some(pointer_id),
333 None,
334 Some(position),
335 );
336 };
337 if active.pointer_id != pointer_id {
338 return PanePointerDispatch::ignored(
339 PanePointerLifecyclePhase::PointerMove,
340 PanePointerIgnoredReason::PointerMismatch,
341 Some(pointer_id),
342 Some(active.target),
343 Some(position),
344 );
345 }
346
347 let kind = PaneSemanticInputEventKind::PointerMove {
348 target: active.target,
349 pointer_id,
350 position,
351 delta_x: position.x.saturating_sub(active.last_position.x),
352 delta_y: position.y.saturating_sub(active.last_position.y),
353 };
354 let dispatch = self.forward_semantic(
355 DispatchContext {
356 phase: PanePointerLifecyclePhase::PointerMove,
357 pointer_id: Some(pointer_id),
358 target: Some(active.target),
359 position: Some(position),
360 },
361 kind,
362 modifiers,
363 None,
364 );
365 if dispatch.transition.is_some() {
366 active.last_position = position;
367 self.active = Some(active);
368 }
369 dispatch
370 }
371
372 pub fn pointer_up(
374 &mut self,
375 pointer_id: u32,
376 button: PanePointerButton,
377 position: PanePointerPosition,
378 modifiers: PaneModifierSnapshot,
379 ) -> PanePointerDispatch {
380 let Some(active) = self.active else {
381 return PanePointerDispatch::ignored(
382 PanePointerLifecyclePhase::PointerUp,
383 PanePointerIgnoredReason::NoActivePointer,
384 Some(pointer_id),
385 None,
386 Some(position),
387 );
388 };
389 if active.pointer_id != pointer_id {
390 return PanePointerDispatch::ignored(
391 PanePointerLifecyclePhase::PointerUp,
392 PanePointerIgnoredReason::PointerMismatch,
393 Some(pointer_id),
394 Some(active.target),
395 Some(position),
396 );
397 }
398 if active.button != button {
399 return PanePointerDispatch::ignored(
400 PanePointerLifecyclePhase::PointerUp,
401 PanePointerIgnoredReason::ButtonMismatch,
402 Some(pointer_id),
403 Some(active.target),
404 Some(position),
405 );
406 }
407
408 let kind = PaneSemanticInputEventKind::PointerUp {
409 target: active.target,
410 pointer_id,
411 button: active.button,
412 position,
413 };
414 let dispatch = self.forward_semantic(
415 DispatchContext {
416 phase: PanePointerLifecyclePhase::PointerUp,
417 pointer_id: Some(pointer_id),
418 target: Some(active.target),
419 position: Some(position),
420 },
421 kind,
422 modifiers,
423 active
424 .capture_state
425 .is_acquired()
426 .then_some(PanePointerCaptureCommand::Release { pointer_id }),
427 );
428 if dispatch.transition.is_some() {
429 self.active = None;
430 }
431 dispatch
432 }
433
434 pub fn pointer_cancel(&mut self, pointer_id: Option<u32>) -> PanePointerDispatch {
436 self.cancel_active(
437 PanePointerLifecyclePhase::PointerCancel,
438 pointer_id,
439 PaneCancelReason::PointerCancel,
440 true,
441 )
442 }
443
444 pub fn pointer_leave(&mut self, pointer_id: u32) -> PanePointerDispatch {
446 let Some(active) = self.active else {
447 return PanePointerDispatch::ignored(
448 PanePointerLifecyclePhase::PointerLeave,
449 PanePointerIgnoredReason::NoActivePointer,
450 Some(pointer_id),
451 None,
452 None,
453 );
454 };
455 if active.pointer_id != pointer_id {
456 return PanePointerDispatch::ignored(
457 PanePointerLifecyclePhase::PointerLeave,
458 PanePointerIgnoredReason::PointerMismatch,
459 Some(pointer_id),
460 Some(active.target),
461 None,
462 );
463 }
464
465 if matches!(active.capture_state, CaptureState::Requested)
466 && self.config.cancel_on_leave_without_capture
467 {
468 self.cancel_active(
469 PanePointerLifecyclePhase::PointerLeave,
470 Some(pointer_id),
471 PaneCancelReason::PointerCancel,
472 true,
473 )
474 } else {
475 PanePointerDispatch::ignored(
476 PanePointerLifecyclePhase::PointerLeave,
477 PanePointerIgnoredReason::LeaveWhileCaptured,
478 Some(pointer_id),
479 Some(active.target),
480 None,
481 )
482 }
483 }
484
485 pub fn blur(&mut self) -> PanePointerDispatch {
487 let Some(active) = self.active else {
488 return PanePointerDispatch::ignored(
489 PanePointerLifecyclePhase::Blur,
490 PanePointerIgnoredReason::NoActivePointer,
491 None,
492 None,
493 None,
494 );
495 };
496 let kind = PaneSemanticInputEventKind::Blur {
497 target: Some(active.target),
498 };
499 let dispatch = self.forward_semantic(
500 DispatchContext {
501 phase: PanePointerLifecyclePhase::Blur,
502 pointer_id: Some(active.pointer_id),
503 target: Some(active.target),
504 position: None,
505 },
506 kind,
507 PaneModifierSnapshot::default(),
508 active
509 .capture_state
510 .is_acquired()
511 .then_some(PanePointerCaptureCommand::Release {
512 pointer_id: active.pointer_id,
513 }),
514 );
515 if dispatch.transition.is_some() {
516 self.active = None;
517 }
518 dispatch
519 }
520
521 pub fn visibility_hidden(&mut self) -> PanePointerDispatch {
523 self.cancel_active(
524 PanePointerLifecyclePhase::VisibilityHidden,
525 None,
526 PaneCancelReason::FocusLost,
527 true,
528 )
529 }
530
531 pub fn lost_pointer_capture(&mut self, pointer_id: u32) -> PanePointerDispatch {
533 self.cancel_active(
534 PanePointerLifecyclePhase::LostPointerCapture,
535 Some(pointer_id),
536 PaneCancelReason::PointerCancel,
537 false,
538 )
539 }
540
541 fn cancel_active(
542 &mut self,
543 phase: PanePointerLifecyclePhase,
544 pointer_id: Option<u32>,
545 reason: PaneCancelReason,
546 release_capture: bool,
547 ) -> PanePointerDispatch {
548 let Some(active) = self.active else {
549 return PanePointerDispatch::ignored(
550 phase,
551 PanePointerIgnoredReason::NoActivePointer,
552 pointer_id,
553 None,
554 None,
555 );
556 };
557 if let Some(id) = pointer_id
558 && id != active.pointer_id
559 {
560 return PanePointerDispatch::ignored(
561 phase,
562 PanePointerIgnoredReason::PointerMismatch,
563 Some(id),
564 Some(active.target),
565 None,
566 );
567 }
568
569 let kind = PaneSemanticInputEventKind::Cancel {
570 target: Some(active.target),
571 reason,
572 };
573 let command = (release_capture && active.capture_state.is_acquired()).then_some(
574 PanePointerCaptureCommand::Release {
575 pointer_id: active.pointer_id,
576 },
577 );
578 let dispatch = self.forward_semantic(
579 DispatchContext {
580 phase,
581 pointer_id: Some(active.pointer_id),
582 target: Some(active.target),
583 position: None,
584 },
585 kind,
586 PaneModifierSnapshot::default(),
587 command,
588 );
589 if dispatch.transition.is_some() {
590 self.active = None;
591 }
592 dispatch
593 }
594
595 fn forward_semantic(
596 &mut self,
597 context: DispatchContext,
598 kind: PaneSemanticInputEventKind,
599 modifiers: PaneModifierSnapshot,
600 capture_command: Option<PanePointerCaptureCommand>,
601 ) -> PanePointerDispatch {
602 let mut event = PaneSemanticInputEvent::new(self.next_sequence(), kind);
603 event.modifiers = modifiers;
604 match self.machine.apply_event(&event) {
605 Ok(transition) => {
606 let sequence = Some(event.sequence);
607 PanePointerDispatch {
608 semantic_event: Some(event),
609 transition: Some(transition),
610 capture_command,
611 log: PanePointerLogEntry {
612 phase: context.phase,
613 sequence,
614 pointer_id: context.pointer_id,
615 target: context.target,
616 position: context.position,
617 capture_command,
618 outcome: PanePointerLogOutcome::SemanticForwarded,
619 },
620 }
621 }
622 Err(_error) => PanePointerDispatch::ignored(
623 context.phase,
624 PanePointerIgnoredReason::MachineRejectedEvent,
625 context.pointer_id,
626 context.target,
627 context.position,
628 ),
629 }
630 }
631
632 fn next_sequence(&mut self) -> u64 {
633 let sequence = self.next_sequence;
634 self.next_sequence = self.next_sequence.saturating_add(1);
635 sequence
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::{
642 PanePointerCaptureAdapter, PanePointerCaptureCommand, PanePointerCaptureConfig,
643 PanePointerIgnoredReason, PanePointerLifecyclePhase, PanePointerLogOutcome,
644 };
645 use ftui_layout::{
646 PaneCancelReason, PaneDragResizeEffect, PaneDragResizeState, PaneId, PaneModifierSnapshot,
647 PanePointerButton, PanePointerPosition, PaneResizeTarget, PaneSemanticInputEventKind,
648 SplitAxis,
649 };
650
651 fn target() -> PaneResizeTarget {
652 PaneResizeTarget {
653 split_id: PaneId::MIN,
654 axis: SplitAxis::Horizontal,
655 }
656 }
657
658 fn pos(x: i32, y: i32) -> PanePointerPosition {
659 PanePointerPosition::new(x, y)
660 }
661
662 fn adapter() -> PanePointerCaptureAdapter {
663 PanePointerCaptureAdapter::new(PanePointerCaptureConfig::default())
664 .expect("default config should be valid")
665 }
666
667 #[test]
668 fn pointer_down_arms_machine_and_requests_capture() {
669 let mut adapter = adapter();
670 let dispatch = adapter.pointer_down(
671 target(),
672 11,
673 PanePointerButton::Primary,
674 pos(5, 8),
675 PaneModifierSnapshot::default(),
676 );
677 assert_eq!(
678 dispatch.capture_command,
679 Some(PanePointerCaptureCommand::Acquire { pointer_id: 11 })
680 );
681 assert_eq!(adapter.active_pointer_id(), Some(11));
682 assert!(matches!(
683 adapter.machine_state(),
684 PaneDragResizeState::Armed { pointer_id: 11, .. }
685 ));
686 assert!(matches!(
687 dispatch
688 .transition
689 .as_ref()
690 .expect("transition should exist")
691 .effect,
692 PaneDragResizeEffect::Armed { pointer_id: 11, .. }
693 ));
694 }
695
696 #[test]
697 fn non_activation_button_is_ignored_deterministically() {
698 let mut adapter = adapter();
699 let dispatch = adapter.pointer_down(
700 target(),
701 3,
702 PanePointerButton::Secondary,
703 pos(1, 1),
704 PaneModifierSnapshot::default(),
705 );
706 assert_eq!(dispatch.semantic_event, None);
707 assert_eq!(dispatch.transition, None);
708 assert_eq!(dispatch.capture_command, None);
709 assert_eq!(adapter.active_pointer_id(), None);
710 assert_eq!(
711 dispatch.log.outcome,
712 PanePointerLogOutcome::Ignored(PanePointerIgnoredReason::ButtonNotAllowed)
713 );
714 }
715
716 #[test]
717 fn pointer_move_mismatch_is_ignored_without_state_mutation() {
718 let mut adapter = adapter();
719 adapter.pointer_down(
720 target(),
721 9,
722 PanePointerButton::Primary,
723 pos(10, 10),
724 PaneModifierSnapshot::default(),
725 );
726 let before = adapter.machine_state();
727 let dispatch = adapter.pointer_move(77, pos(14, 14), PaneModifierSnapshot::default());
728 assert_eq!(dispatch.semantic_event, None);
729 assert_eq!(
730 dispatch.log.outcome,
731 PanePointerLogOutcome::Ignored(PanePointerIgnoredReason::PointerMismatch)
732 );
733 assert_eq!(before, adapter.machine_state());
734 assert_eq!(adapter.active_pointer_id(), Some(9));
735 }
736
737 #[test]
738 fn pointer_up_releases_capture_and_returns_idle() {
739 let mut adapter = adapter();
740 adapter.pointer_down(
741 target(),
742 9,
743 PanePointerButton::Primary,
744 pos(1, 1),
745 PaneModifierSnapshot::default(),
746 );
747 let ack = adapter.capture_acquired(9);
748 assert_eq!(ack.log.outcome, PanePointerLogOutcome::CaptureStateUpdated);
749 let dispatch = adapter.pointer_up(
750 9,
751 PanePointerButton::Primary,
752 pos(6, 1),
753 PaneModifierSnapshot::default(),
754 );
755 assert_eq!(
756 dispatch.capture_command,
757 Some(PanePointerCaptureCommand::Release { pointer_id: 9 })
758 );
759 assert_eq!(adapter.active_pointer_id(), None);
760 assert_eq!(adapter.machine_state(), PaneDragResizeState::Idle);
761 assert!(matches!(
762 dispatch
763 .semantic_event
764 .as_ref()
765 .expect("semantic event expected")
766 .kind,
767 PaneSemanticInputEventKind::PointerUp { pointer_id: 9, .. }
768 ));
769 }
770
771 #[test]
772 fn pointer_up_with_wrong_button_is_ignored() {
773 let mut adapter = adapter();
774 adapter.pointer_down(
775 target(),
776 4,
777 PanePointerButton::Primary,
778 pos(2, 2),
779 PaneModifierSnapshot::default(),
780 );
781 let dispatch = adapter.pointer_up(
782 4,
783 PanePointerButton::Secondary,
784 pos(3, 2),
785 PaneModifierSnapshot::default(),
786 );
787 assert_eq!(
788 dispatch.log.outcome,
789 PanePointerLogOutcome::Ignored(PanePointerIgnoredReason::ButtonMismatch)
790 );
791 assert_eq!(adapter.active_pointer_id(), Some(4));
792 }
793
794 #[test]
795 fn blur_emits_semantic_blur_and_releases_capture() {
796 let mut adapter = adapter();
797 adapter.pointer_down(
798 target(),
799 6,
800 PanePointerButton::Primary,
801 pos(0, 0),
802 PaneModifierSnapshot::default(),
803 );
804 let ack = adapter.capture_acquired(6);
805 assert_eq!(ack.log.outcome, PanePointerLogOutcome::CaptureStateUpdated);
806 let dispatch = adapter.blur();
807 assert_eq!(dispatch.log.phase, PanePointerLifecyclePhase::Blur);
808 assert!(matches!(
809 dispatch
810 .semantic_event
811 .as_ref()
812 .expect("semantic event expected")
813 .kind,
814 PaneSemanticInputEventKind::Blur { .. }
815 ));
816 assert_eq!(
817 dispatch.capture_command,
818 Some(PanePointerCaptureCommand::Release { pointer_id: 6 })
819 );
820 assert_eq!(adapter.active_pointer_id(), None);
821 assert_eq!(adapter.machine_state(), PaneDragResizeState::Idle);
822 }
823
824 #[test]
825 fn visibility_hidden_emits_focus_lost_cancel() {
826 let mut adapter = adapter();
827 adapter.pointer_down(
828 target(),
829 8,
830 PanePointerButton::Primary,
831 pos(5, 2),
832 PaneModifierSnapshot::default(),
833 );
834 let ack = adapter.capture_acquired(8);
835 assert_eq!(ack.log.outcome, PanePointerLogOutcome::CaptureStateUpdated);
836 let dispatch = adapter.visibility_hidden();
837 assert!(matches!(
838 dispatch
839 .semantic_event
840 .as_ref()
841 .expect("semantic event expected")
842 .kind,
843 PaneSemanticInputEventKind::Cancel {
844 reason: PaneCancelReason::FocusLost,
845 ..
846 }
847 ));
848 assert_eq!(
849 dispatch.capture_command,
850 Some(PanePointerCaptureCommand::Release { pointer_id: 8 })
851 );
852 assert_eq!(adapter.active_pointer_id(), None);
853 }
854
855 #[test]
856 fn lost_pointer_capture_cancels_without_double_release() {
857 let mut adapter = adapter();
858 adapter.pointer_down(
859 target(),
860 42,
861 PanePointerButton::Primary,
862 pos(7, 7),
863 PaneModifierSnapshot::default(),
864 );
865 let dispatch = adapter.lost_pointer_capture(42);
866 assert_eq!(dispatch.capture_command, None);
867 assert!(matches!(
868 dispatch
869 .semantic_event
870 .as_ref()
871 .expect("semantic event expected")
872 .kind,
873 PaneSemanticInputEventKind::Cancel {
874 reason: PaneCancelReason::PointerCancel,
875 ..
876 }
877 ));
878 assert_eq!(adapter.active_pointer_id(), None);
879 }
880
881 #[test]
882 fn pointer_leave_before_capture_ack_cancels() {
883 let mut adapter = adapter();
884 adapter.pointer_down(
885 target(),
886 31,
887 PanePointerButton::Primary,
888 pos(1, 1),
889 PaneModifierSnapshot::default(),
890 );
891 let dispatch = adapter.pointer_leave(31);
892 assert_eq!(dispatch.log.phase, PanePointerLifecyclePhase::PointerLeave);
893 assert!(matches!(
894 dispatch
895 .semantic_event
896 .as_ref()
897 .expect("semantic event expected")
898 .kind,
899 PaneSemanticInputEventKind::Cancel {
900 reason: PaneCancelReason::PointerCancel,
901 ..
902 }
903 ));
904 assert_eq!(dispatch.capture_command, None);
905 assert_eq!(adapter.active_pointer_id(), None);
906 }
907
908 #[test]
909 fn pointer_leave_after_capture_ack_releases_and_cancels() {
910 let mut adapter = adapter();
911 adapter.pointer_down(
912 target(),
913 39,
914 PanePointerButton::Primary,
915 pos(3, 3),
916 PaneModifierSnapshot::default(),
917 );
918 let ack = adapter.capture_acquired(39);
919 assert_eq!(ack.log.outcome, PanePointerLogOutcome::CaptureStateUpdated);
920
921 let dispatch = adapter.pointer_cancel(Some(39));
922 assert!(matches!(
923 dispatch
924 .semantic_event
925 .as_ref()
926 .expect("semantic event expected")
927 .kind,
928 PaneSemanticInputEventKind::Cancel {
929 reason: PaneCancelReason::PointerCancel,
930 ..
931 }
932 ));
933 assert_eq!(
934 dispatch.capture_command,
935 Some(PanePointerCaptureCommand::Release { pointer_id: 39 })
936 );
937 assert_eq!(adapter.active_pointer_id(), None);
938 }
939
940 #[test]
941 fn pointer_leave_after_capture_ack_is_ignored() {
942 let mut adapter = adapter();
943 adapter.pointer_down(
944 target(),
945 55,
946 PanePointerButton::Primary,
947 pos(4, 4),
948 PaneModifierSnapshot::default(),
949 );
950 let ack = adapter.capture_acquired(55);
951 assert_eq!(ack.log.outcome, PanePointerLogOutcome::CaptureStateUpdated);
952
953 let dispatch = adapter.pointer_leave(55);
954 assert_eq!(dispatch.semantic_event, None);
955 assert_eq!(
956 dispatch.log.outcome,
957 PanePointerLogOutcome::Ignored(PanePointerIgnoredReason::LeaveWhileCaptured)
958 );
959 assert_eq!(adapter.active_pointer_id(), Some(55));
960 }
961}