Polished scrolling significantly

Mikayla Maki created

Change summary

crates/gpui/src/platform/event.rs     | 11 +++
crates/gpui/src/platform/mac/event.rs | 13 +++
crates/terminal/src/terminal.rs       | 94 ++++++++++++++++++----------
3 files changed, 81 insertions(+), 37 deletions(-)

Detailed changes

crates/gpui/src/platform/event.rs 🔗

@@ -19,6 +19,15 @@ pub struct ModifiersChangedEvent {
     pub cmd: bool,
 }
 
+/// The phase of a touch motion event.
+/// Based on the winit enum of the same name,
+#[derive(Clone, Copy, Debug)]
+pub enum TouchPhase {
+    Started,
+    Moved,
+    Ended,
+}
+
 #[derive(Clone, Copy, Debug, Default)]
 pub struct ScrollWheelEvent {
     pub position: Vector2F,
@@ -28,6 +37,8 @@ pub struct ScrollWheelEvent {
     pub alt: bool,
     pub shift: bool,
     pub cmd: bool,
+    /// If the platform supports returning the phase of a scroll wheel event, it will be stored here
+    pub phase: Option<TouchPhase>,
 }
 
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]

crates/gpui/src/platform/mac/event.rs 🔗

@@ -3,10 +3,10 @@ use crate::{
     keymap::Keystroke,
     platform::{Event, NavigationDirection},
     KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
-    MouseMovedEvent, ScrollWheelEvent,
+    MouseMovedEvent, ScrollWheelEvent, TouchPhase,
 };
 use cocoa::{
-    appkit::{NSEvent, NSEventModifierFlags, NSEventType},
+    appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
     base::{id, YES},
     foundation::NSString as _,
 };
@@ -150,6 +150,14 @@ impl Event {
             NSEventType::NSScrollWheel => window_height.map(|window_height| {
                 let modifiers = native_event.modifierFlags();
 
+                let phase = match native_event.phase() {
+                    NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
+                        Some(TouchPhase::Started)
+                    }
+                    NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended),
+                    _ => Some(TouchPhase::Moved),
+                };
+
                 Self::ScrollWheel(ScrollWheelEvent {
                     position: vec2f(
                         native_event.locationInWindow().x as f32,
@@ -159,6 +167,7 @@ impl Event {
                         native_event.scrollingDeltaX() as f32,
                         native_event.scrollingDeltaY() as f32,
                     ),
+                    phase,
                     precise: native_event.hasPreciseScrollingDeltas() == YES,
                     ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
                     alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),

crates/terminal/src/terminal.rs 🔗

@@ -72,7 +72,7 @@ pub fn init(cx: &mut MutableAppContext) {
 ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
 ///Scroll multiplier that is set to 3 by default. This will be removed when I
 ///Implement scroll bars.
-const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+const SCROLL_MULTIPLIER: f32 = 4.;
 const MAX_SEARCH_LINES: usize = 100;
 const DEBUG_TERMINAL_WIDTH: f32 = 500.;
 const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
@@ -381,6 +381,7 @@ impl TerminalBuilder {
             shell_pid,
             foreground_process_info: None,
             breadcrumb_text: String::new(),
+            scroll_px: 0.,
         };
 
         Ok(TerminalBuilder {
@@ -500,6 +501,7 @@ pub struct Terminal {
     shell_pid: u32,
     shell_fd: u32,
     foreground_process_info: Option<LocalProcessInfo>,
+    scroll_px: f32,
 }
 
 impl Terminal {
@@ -893,44 +895,66 @@ impl Terminal {
 
     ///Scroll the terminal
     pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
-        if self.mouse_mode(e.shift) {
-            //TODO: Currently this only sends the current scroll reports as they come in. Alacritty
-            //Sends the *entire* scroll delta on *every* scroll event, only resetting it when
-            //The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
-            //This would be consistent with a scroll model based on 'distance from origin'...
-            let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32;
-            let point = mouse_point(
-                e.position.sub(origin),
-                self.cur_size,
-                self.last_content.display_offset,
-            );
-
-            if let Some(scrolls) =
-                scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
+        let mouse_mode = self.mouse_mode(e.shift);
+
+        if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
+            if mouse_mode {
+                let point = mouse_point(
+                    e.position.sub(origin),
+                    self.cur_size,
+                    self.last_content.display_offset,
+                );
+
+                if let Some(scrolls) =
+                    scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
+                {
+                    for scroll in scrolls {
+                        self.pty_tx.notify(scroll);
+                    }
+                };
+            } else if self
+                .last_content
+                .mode
+                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
+                && !e.shift
             {
-                for scroll in scrolls {
-                    self.pty_tx.notify(scroll);
+                self.pty_tx.notify(alt_scroll(scroll_lines))
+            } else {
+                if scroll_lines != 0 {
+                    let scroll = AlacScroll::Delta(scroll_lines);
+
+                    self.events.push_back(InternalEvent::Scroll(scroll));
                 }
-            };
-        } else if self
-            .last_content
-            .mode
-            .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
-            && !e.shift
-        {
-            //TODO: See above TODO, also applies here.
-            let scroll_lines =
-                ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
-
-            self.pty_tx.notify(alt_scroll(scroll_lines))
-        } else {
-            let scroll_lines =
-                ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
-            if scroll_lines != 0 {
-                let scroll = AlacScroll::Delta(scroll_lines);
+            }
+        }
+    }
+
+    fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
+        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
+
+        match e.phase {
+            /* Reset scroll state on started */
+            Some(gpui::TouchPhase::Started) => {
+                self.scroll_px = 0.;
+                None
+            }
+            /* Calculate the appropriate scroll lines */
+            Some(gpui::TouchPhase::Moved) => {
+                let old_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+
+                self.scroll_px += e.delta.y() * scroll_multiplier;
+
+                let new_offset = (self.scroll_px / self.cur_size.line_height) as i32;
+
+                // Whenever we hit the edges, reset our stored scroll to 0
+                // so we can respond to changes in direction quickly
+                self.scroll_px %= self.cur_size.height;
 
-                self.events.push_back(InternalEvent::Scroll(scroll));
+                Some(new_offset - old_offset)
             }
+            /* Fall back to delta / line_height */
+            None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32),
+            _ => None,
         }
     }