1use crate::{
2 Axis, Bounds, Capslock, Context, Empty, IntoElement, IsZero, Keystroke, Modifiers, Pixels,
3 Point, Render, Window, point, px, seal::Sealed,
4};
5use smallvec::SmallVec;
6use std::{
7 any::Any,
8 fmt::Debug,
9 ops::Deref,
10 path::PathBuf,
11 time::{Duration, Instant},
12};
13
14pub trait InputEvent: Sealed + 'static {
16 fn to_platform_input(self) -> PlatformInput;
18}
19
20pub trait KeyEvent: InputEvent {}
22
23pub trait MouseEvent: InputEvent {}
25
26pub trait GestureEvent: InputEvent {}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct KeyDownEvent {
32 pub keystroke: Keystroke,
34
35 pub is_held: bool,
37
38 pub prefer_character_input: bool,
41}
42
43impl Sealed for KeyDownEvent {}
44impl InputEvent for KeyDownEvent {
45 fn to_platform_input(self) -> PlatformInput {
46 PlatformInput::KeyDown(self)
47 }
48}
49impl KeyEvent for KeyDownEvent {}
50
51#[derive(Clone, Debug)]
53pub struct KeyUpEvent {
54 pub keystroke: Keystroke,
56}
57
58impl Sealed for KeyUpEvent {}
59impl InputEvent for KeyUpEvent {
60 fn to_platform_input(self) -> PlatformInput {
61 PlatformInput::KeyUp(self)
62 }
63}
64impl KeyEvent for KeyUpEvent {}
65
66#[derive(Clone, Debug, Default)]
68pub struct ModifiersChangedEvent {
69 pub modifiers: Modifiers,
71 pub capslock: Capslock,
73}
74
75impl Sealed for ModifiersChangedEvent {}
76impl InputEvent for ModifiersChangedEvent {
77 fn to_platform_input(self) -> PlatformInput {
78 PlatformInput::ModifiersChanged(self)
79 }
80}
81impl KeyEvent for ModifiersChangedEvent {}
82
83impl Deref for ModifiersChangedEvent {
84 type Target = Modifiers;
85
86 fn deref(&self) -> &Self::Target {
87 &self.modifiers
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default)]
94pub enum TouchPhase {
95 Started,
97 #[default]
99 Moved,
100 Ended,
102}
103
104#[derive(Clone, Debug, Default)]
106pub struct MouseDownEvent {
107 pub button: MouseButton,
109
110 pub position: Point<Pixels>,
112
113 pub modifiers: Modifiers,
115
116 pub click_count: usize,
118
119 pub first_mouse: bool,
121}
122
123impl Sealed for MouseDownEvent {}
124impl InputEvent for MouseDownEvent {
125 fn to_platform_input(self) -> PlatformInput {
126 PlatformInput::MouseDown(self)
127 }
128}
129impl MouseEvent for MouseDownEvent {}
130
131#[derive(Clone, Debug, Default)]
133pub struct MouseUpEvent {
134 pub button: MouseButton,
136
137 pub position: Point<Pixels>,
139
140 pub modifiers: Modifiers,
142
143 pub click_count: usize,
145}
146
147impl Sealed for MouseUpEvent {}
148impl InputEvent for MouseUpEvent {
149 fn to_platform_input(self) -> PlatformInput {
150 PlatformInput::MouseUp(self)
151 }
152}
153impl MouseEvent for MouseUpEvent {}
154
155#[derive(Clone, Debug, Default)]
157pub struct MouseClickEvent {
158 pub down: MouseDownEvent,
160
161 pub up: MouseUpEvent,
163}
164
165#[derive(Clone, Debug, Default)]
167pub struct KeyboardClickEvent {
168 pub button: KeyboardButton,
170
171 pub bounds: Bounds<Pixels>,
173}
174
175#[derive(Clone, Debug)]
177pub enum ClickEvent {
178 Mouse(MouseClickEvent),
180 Keyboard(KeyboardClickEvent),
182}
183
184impl Default for ClickEvent {
185 fn default() -> Self {
186 ClickEvent::Keyboard(KeyboardClickEvent::default())
187 }
188}
189
190impl ClickEvent {
191 pub fn modifiers(&self) -> Modifiers {
196 match self {
197 ClickEvent::Keyboard(_) => Modifiers::default(),
199 ClickEvent::Mouse(event) => event.up.modifiers,
203 }
204 }
205
206 pub fn position(&self) -> Point<Pixels> {
211 match self {
212 ClickEvent::Keyboard(event) => event.bounds.bottom_left(),
213 ClickEvent::Mouse(event) => event.up.position,
214 }
215 }
216
217 pub fn mouse_position(&self) -> Option<Point<Pixels>> {
222 match self {
223 ClickEvent::Keyboard(_) => None,
224 ClickEvent::Mouse(event) => Some(event.up.position),
225 }
226 }
227
228 pub fn is_right_click(&self) -> bool {
233 match self {
234 ClickEvent::Keyboard(_) => false,
235 ClickEvent::Mouse(event) => {
236 event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
237 }
238 }
239 }
240
241 pub fn standard_click(&self) -> bool {
246 match self {
247 ClickEvent::Keyboard(_) => true,
248 ClickEvent::Mouse(event) => {
249 event.down.button == MouseButton::Left && event.up.button == MouseButton::Left
250 }
251 }
252 }
253
254 pub fn first_focus(&self) -> bool {
259 match self {
260 ClickEvent::Keyboard(_) => false,
261 ClickEvent::Mouse(event) => event.down.first_mouse,
262 }
263 }
264
265 pub fn click_count(&self) -> usize {
270 match self {
271 ClickEvent::Keyboard(_) => 1,
272 ClickEvent::Mouse(event) => event.up.click_count,
273 }
274 }
275
276 pub fn is_keyboard(&self) -> bool {
278 match self {
279 ClickEvent::Mouse(_) => false,
280 ClickEvent::Keyboard(_) => true,
281 }
282 }
283}
284
285#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
287pub enum KeyboardButton {
288 #[default]
290 Enter,
291 Space,
293}
294
295#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
297pub enum MouseButton {
298 #[default]
300 Left,
301
302 Right,
304
305 Middle,
307
308 Navigate(NavigationDirection),
310}
311
312impl MouseButton {
313 pub fn all() -> Vec<Self> {
315 vec![
316 MouseButton::Left,
317 MouseButton::Right,
318 MouseButton::Middle,
319 MouseButton::Navigate(NavigationDirection::Back),
320 MouseButton::Navigate(NavigationDirection::Forward),
321 ]
322 }
323}
324
325#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
327pub enum NavigationDirection {
328 #[default]
330 Back,
331
332 Forward,
334}
335
336#[derive(Clone, Debug, Default)]
338pub struct MouseMoveEvent {
339 pub position: Point<Pixels>,
341
342 pub pressed_button: Option<MouseButton>,
344
345 pub modifiers: Modifiers,
347}
348
349impl Sealed for MouseMoveEvent {}
350impl InputEvent for MouseMoveEvent {
351 fn to_platform_input(self) -> PlatformInput {
352 PlatformInput::MouseMove(self)
353 }
354}
355impl MouseEvent for MouseMoveEvent {}
356
357impl MouseMoveEvent {
358 pub fn dragging(&self) -> bool {
360 self.pressed_button == Some(MouseButton::Left)
361 }
362}
363
364#[derive(Clone, Debug, Default)]
366pub struct ScrollWheelEvent {
367 pub position: Point<Pixels>,
369
370 pub delta: ScrollDelta,
372
373 pub modifiers: Modifiers,
375
376 pub touch_phase: TouchPhase,
378}
379
380impl Sealed for ScrollWheelEvent {}
381impl InputEvent for ScrollWheelEvent {
382 fn to_platform_input(self) -> PlatformInput {
383 PlatformInput::ScrollWheel(self)
384 }
385}
386impl MouseEvent for ScrollWheelEvent {}
387
388impl Deref for ScrollWheelEvent {
389 type Target = Modifiers;
390
391 fn deref(&self) -> &Self::Target {
392 &self.modifiers
393 }
394}
395
396#[derive(Clone, Copy, Debug)]
398pub enum ScrollDelta {
399 Pixels(Point<Pixels>),
401 Lines(Point<f32>),
403}
404
405impl Default for ScrollDelta {
406 fn default() -> Self {
407 Self::Lines(Default::default())
408 }
409}
410
411#[derive(Clone, Debug, Default)]
414pub struct PinchEvent {
415 pub position: Point<Pixels>,
417
418 pub delta: f32,
423
424 pub modifiers: Modifiers,
426
427 pub phase: TouchPhase,
429}
430
431impl Sealed for PinchEvent {}
432impl InputEvent for PinchEvent {
433 fn to_platform_input(self) -> PlatformInput {
434 PlatformInput::Pinch(self)
435 }
436}
437impl GestureEvent for PinchEvent {}
438impl MouseEvent for PinchEvent {}
439
440impl Deref for PinchEvent {
441 type Target = Modifiers;
442
443 fn deref(&self) -> &Self::Target {
444 &self.modifiers
445 }
446}
447
448impl ScrollDelta {
449 pub fn precise(&self) -> bool {
451 match self {
452 ScrollDelta::Pixels(_) => true,
453 ScrollDelta::Lines(_) => false,
454 }
455 }
456
457 pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
459 match self {
460 ScrollDelta::Pixels(delta) => *delta,
461 ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
462 }
463 }
464
465 pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
470 match (self, other) {
471 (ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
472 let x = if a.x.signum() == b.x.signum() {
473 a.x + b.x
474 } else {
475 b.x
476 };
477
478 let y = if a.y.signum() == b.y.signum() {
479 a.y + b.y
480 } else {
481 b.y
482 };
483
484 ScrollDelta::Pixels(point(x, y))
485 }
486
487 (ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
488 let x = if a.x.signum() == b.x.signum() {
489 a.x + b.x
490 } else {
491 b.x
492 };
493
494 let y = if a.y.signum() == b.y.signum() {
495 a.y + b.y
496 } else {
497 b.y
498 };
499
500 ScrollDelta::Lines(point(x, y))
501 }
502
503 _ => other,
504 }
505 }
506}
507
508const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
509
510#[derive(Clone, Copy, Debug, Default)]
512pub struct OngoingScroll {
513 last_event: Option<Instant>,
514 axis: Option<Axis>,
515}
516
517impl OngoingScroll {
518 pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
523 self.filter_at(delta, touch_phase, Instant::now())
524 }
525
526 fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
527 const UNLOCK_PERCENT: f32 = 1.9;
528 const UNLOCK_LOWER_BOUND: Pixels = px(6.);
529
530 if matches!(touch_phase, TouchPhase::Ended) {
531 self.last_event = None;
532 self.axis = None;
533 return;
534 }
535
536 let x = delta.x.abs();
537 let y = delta.y.abs();
538 if x.is_zero() && y.is_zero() {
539 if matches!(touch_phase, TouchPhase::Started) {
540 self.last_event = None;
541 self.axis = None;
542 }
543 return;
544 }
545
546 let starts_new_gesture = matches!(touch_phase, TouchPhase::Started)
547 || self
548 .last_event
549 .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
550 let mut axis = self.axis;
551 if starts_new_gesture {
552 axis = if x <= y {
553 Some(Axis::Vertical)
554 } else {
555 Some(Axis::Horizontal)
556 };
557 } else if x.max(y) >= UNLOCK_LOWER_BOUND {
558 match axis {
559 Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
560 axis = None;
561 }
562 Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
563 axis = None;
564 }
565 _ => {}
566 }
567 }
568
569 self.last_event = Some(now);
570 self.axis = axis;
571 match axis {
572 Some(Axis::Vertical) => delta.x = Pixels::ZERO,
573 Some(Axis::Horizontal) => delta.y = Pixels::ZERO,
574 None => {}
575 }
576 }
577}
578
579#[derive(Clone, Debug, Default)]
581pub struct MouseExitEvent {
582 pub position: Point<Pixels>,
584 pub pressed_button: Option<MouseButton>,
586 pub modifiers: Modifiers,
588}
589
590impl Sealed for MouseExitEvent {}
591impl InputEvent for MouseExitEvent {
592 fn to_platform_input(self) -> PlatformInput {
593 PlatformInput::MouseExited(self)
594 }
595}
596impl MouseEvent for MouseExitEvent {}
597
598impl Deref for MouseExitEvent {
599 type Target = Modifiers;
600
601 fn deref(&self) -> &Self::Target {
602 &self.modifiers
603 }
604}
605
606#[derive(Debug, Clone, Default)]
608#[allow(missing_docs)]
609pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>);
610
611impl ExternalPaths {
612 pub fn paths(&self) -> &[PathBuf] {
614 &self.0
615 }
616}
617
618impl Render for ExternalPaths {
619 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
620 Empty
622 }
623}
624
625#[derive(Debug, Clone)]
627pub enum FileDropEvent {
628 Entered {
630 position: Point<Pixels>,
632 paths: ExternalPaths,
634 },
635 Pending {
637 position: Point<Pixels>,
639 },
640 Submit {
642 position: Point<Pixels>,
644 },
645 Exited,
647}
648
649impl Sealed for FileDropEvent {}
650impl InputEvent for FileDropEvent {
651 fn to_platform_input(self) -> PlatformInput {
652 PlatformInput::FileDrop(self)
653 }
654}
655impl MouseEvent for FileDropEvent {}
656
657#[derive(Clone, Debug)]
659pub enum PlatformInput {
660 KeyDown(KeyDownEvent),
662 KeyUp(KeyUpEvent),
664 ModifiersChanged(ModifiersChangedEvent),
666 MouseDown(MouseDownEvent),
668 MouseUp(MouseUpEvent),
670 MouseMove(MouseMoveEvent),
672 MouseExited(MouseExitEvent),
674 ScrollWheel(ScrollWheelEvent),
676 Pinch(PinchEvent),
678 FileDrop(FileDropEvent),
680}
681
682impl PlatformInput {
683 pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
684 match self {
685 PlatformInput::KeyDown { .. } => None,
686 PlatformInput::KeyUp { .. } => None,
687 PlatformInput::ModifiersChanged { .. } => None,
688 PlatformInput::MouseDown(event) => Some(event),
689 PlatformInput::MouseUp(event) => Some(event),
690 PlatformInput::MouseMove(event) => Some(event),
691 PlatformInput::MouseExited(event) => Some(event),
692 PlatformInput::ScrollWheel(event) => Some(event),
693 PlatformInput::Pinch(event) => Some(event),
694 PlatformInput::FileDrop(event) => Some(event),
695 }
696 }
697
698 pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> {
699 match self {
700 PlatformInput::KeyDown(event) => Some(event),
701 PlatformInput::KeyUp(event) => Some(event),
702 PlatformInput::ModifiersChanged(event) => Some(event),
703 PlatformInput::MouseDown(_) => None,
704 PlatformInput::MouseUp(_) => None,
705 PlatformInput::MouseMove(_) => None,
706 PlatformInput::MouseExited(_) => None,
707 PlatformInput::ScrollWheel(_) => None,
708 PlatformInput::Pinch(_) => None,
709 PlatformInput::FileDrop(_) => None,
710 }
711 }
712}
713
714#[cfg(test)]
715mod test {
716
717 use crate::{
718 self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement,
719 KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div,
720 };
721
722 struct TestView {
723 saw_key_down: bool,
724 saw_action: bool,
725 focus_handle: FocusHandle,
726 }
727
728 actions!(test_only, [TestAction]);
729
730 impl Render for TestView {
731 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
732 div().id("testview").child(
733 div()
734 .key_context("parent")
735 .on_key_down(cx.listener(|this, _, _, cx| {
736 cx.stop_propagation();
737 this.saw_key_down = true
738 }))
739 .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| {
740 this.saw_action = true
741 }))
742 .child(
743 div()
744 .key_context("nested")
745 .track_focus(&self.focus_handle)
746 .into_element(),
747 ),
748 )
749 }
750 }
751
752 #[gpui::test]
753 fn test_on_events(cx: &mut TestAppContext) {
754 let window = cx.update(|cx| {
755 cx.open_window(Default::default(), |_, cx| {
756 cx.new(|cx| TestView {
757 saw_key_down: false,
758 saw_action: false,
759 focus_handle: cx.focus_handle(),
760 })
761 })
762 .unwrap()
763 });
764
765 cx.update(|cx| {
766 cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
767 });
768
769 window
770 .update(cx, |test_view, window, _cx| {
771 window.focus(&test_view.focus_handle)
772 })
773 .unwrap();
774
775 cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
776 cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
777
778 window
779 .update(cx, |test_view, _, _| {
780 assert!(test_view.saw_key_down || test_view.saw_action);
781 assert!(test_view.saw_key_down);
782 assert!(test_view.saw_action);
783 })
784 .unwrap();
785 }
786
787 #[gpui::test]
788 fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding(
789 cx: &mut TestAppContext,
790 ) {
791 let (test_view, cx) = cx.add_window_view(|_, cx| TestView {
792 saw_key_down: false,
793 saw_action: false,
794 focus_handle: cx.focus_handle(),
795 });
796
797 cx.update(|_, cx| {
798 cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]);
799 });
800 test_view.update_in(cx, |test_view, window, _cx| {
801 window.focus(&test_view.focus_handle);
802 });
803
804 cx.simulate_modifiers_change(Modifiers::alt());
805 cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift());
806 cx.simulate_modifiers_change(Modifiers::shift());
807 cx.simulate_modifiers_change(Modifiers::none());
808 assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action));
809
810 cx.simulate_modifiers_change(Modifiers::shift());
811 cx.simulate_modifiers_change(Modifiers::none());
812 assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action));
813 }
814}
815
816#[cfg(test)]
817mod ongoing_scroll_tests {
818 use super::*;
819
820 #[test]
821 fn ongoing_scroll_locks_to_dominant_axis() {
822 let now = Instant::now();
823 let mut ongoing_scroll = OngoingScroll::default();
824 let mut horizontal_delta = point(px(10.), px(2.));
825 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
826 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
827 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
828
829 let mut continued_delta = point(px(3.), px(2.));
830 ongoing_scroll.filter_at(
831 &mut continued_delta,
832 TouchPhase::Moved,
833 now + Duration::from_millis(1),
834 );
835 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
836 assert_eq!(continued_delta, point(px(3.), px(0.)));
837 }
838
839 #[test]
840 fn ongoing_scroll_unlocks_when_direction_changes() {
841 let now = Instant::now();
842 let mut ongoing_scroll = OngoingScroll::default();
843 let mut horizontal_delta = point(px(10.), px(2.));
844 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
845
846 let mut vertical_delta = point(px(2.), px(10.));
847 ongoing_scroll.filter_at(
848 &mut vertical_delta,
849 TouchPhase::Moved,
850 now + Duration::from_millis(1),
851 );
852 assert_eq!(ongoing_scroll.axis, None);
853 assert_eq!(vertical_delta, point(px(2.), px(10.)));
854 }
855
856 #[test]
857 fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() {
858 let now = Instant::now();
859 let mut ongoing_scroll = OngoingScroll::default();
860 let mut horizontal_delta = point(px(10.), px(2.));
861 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
862
863 let mut vertical_delta = point(px(2.), px(10.));
864 ongoing_scroll.filter_at(
865 &mut vertical_delta,
866 TouchPhase::Moved,
867 now + SCROLL_EVENT_SEPARATION,
868 );
869 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
870 assert_eq!(vertical_delta, point(px(0.), px(10.)));
871 }
872
873 #[test]
874 fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() {
875 let now = Instant::now();
876 let mut ongoing_scroll = OngoingScroll::default();
877 let mut horizontal_delta = point(px(10.), px(2.));
878 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
879
880 let mut zero_delta = Point::default();
881 ongoing_scroll.filter_at(
882 &mut zero_delta,
883 TouchPhase::Ended,
884 now + Duration::from_millis(1),
885 );
886 assert_eq!(ongoing_scroll.axis, None);
887
888 let mut vertical_delta = point(px(2.), px(3.));
889 ongoing_scroll.filter_at(
890 &mut vertical_delta,
891 TouchPhase::Moved,
892 now + Duration::from_millis(2),
893 );
894 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
895 assert_eq!(vertical_delta, point(px(0.), px(3.)));
896 }
897
898 #[test]
899 fn ongoing_scroll_ignores_zero_delta_movement() {
900 let now = Instant::now();
901 let mut ongoing_scroll = OngoingScroll::default();
902 let mut horizontal_delta = point(px(10.), px(2.));
903 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
904
905 let mut zero_delta = Point::default();
906 ongoing_scroll.filter_at(
907 &mut zero_delta,
908 TouchPhase::Moved,
909 now + SCROLL_EVENT_SEPARATION,
910 );
911
912 let mut vertical_delta = point(px(2.), px(10.));
913 ongoing_scroll.filter_at(
914 &mut vertical_delta,
915 TouchPhase::Moved,
916 now + SCROLL_EVENT_SEPARATION,
917 );
918 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
919 assert_eq!(vertical_delta, point(px(0.), px(10.)));
920 }
921
922 #[test]
923 fn ongoing_scroll_supports_moved_only_platforms() {
924 let now = Instant::now();
925 let mut ongoing_scroll = OngoingScroll::default();
926 let mut horizontal_delta = point(px(10.), px(2.));
927 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
928 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
929 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
930 }
931}