From 27b551d34593cd94a3fba016c3a7f2042f9d9d38 Mon Sep 17 00:00:00 2001
From: Dmitry Sharshakov <d3dx12.xx@gmail.com>
Date: Tue, 10 Aug 2021 08:35:20 +0300
Subject: [PATCH] helix-term: handle scrolling when mouse is enabled (#554)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* helix-term: handle scrolling when mouse is enabled

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: configure scrolling speed

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: use new config for scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: defaults for edtior config

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: add scroll-lines property

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: scroll hovered view

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: support inverted scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: remove duplicating code

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: do not focus view while scrolled

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: refactor mouse events and scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* simplify

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
---
 helix-term/src/commands.rs  |   2 +-
 helix-term/src/ui/editor.rs | 176 +++++++++++++++++++++++-------------
 helix-view/src/editor.rs    |   5 +-
 3 files changed, 116 insertions(+), 67 deletions(-)

diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index f8c5d480..b918256e 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -871,7 +871,7 @@ fn switch_to_lowercase(cx: &mut Context) {
     doc.append_changes_to_history(view.id);
 }
 
-fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
+pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
     use Direction::*;
     let (view, doc) = current!(cx.editor);
     let cursor = coords_at_pos(
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 487a1ce3..aa21a389 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -9,6 +9,7 @@ use crate::{
 use helix_core::{
     coords_at_pos,
     graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
+    movement::Direction,
     syntax::{self, HighlightEvent},
     unicode::segmentation::UnicodeSegmentation,
     unicode::width::UnicodeWidthStr,
@@ -694,8 +695,112 @@ impl EditorView {
     }
 }
 
+impl EditorView {
+    fn handle_mouse_event(
+        &mut self,
+        event: MouseEvent,
+        cxt: &mut commands::Context,
+    ) -> EventResult {
+        match event {
+            MouseEvent {
+                kind: MouseEventKind::Down(MouseButton::Left),
+                row,
+                column,
+                modifiers,
+                ..
+            } => {
+                let editor = &mut cxt.editor;
+
+                let result = editor.tree.views().find_map(|(view, _focus)| {
+                    view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
+                        .map(|pos| (pos, view.id))
+                });
+
+                if let Some((pos, view_id)) = result {
+                    let doc = &mut editor.documents[editor.tree.get(view_id).doc];
+
+                    if modifiers == crossterm::event::KeyModifiers::ALT {
+                        let selection = doc.selection(view_id).clone();
+                        doc.set_selection(view_id, selection.push(Range::point(pos)));
+                    } else {
+                        doc.set_selection(view_id, Selection::point(pos));
+                    }
+
+                    editor.tree.focus = view_id;
+
+                    return EventResult::Consumed(None);
+                }
+
+                EventResult::Ignored
+            }
+
+            MouseEvent {
+                kind: MouseEventKind::Drag(MouseButton::Left),
+                row,
+                column,
+                ..
+            } => {
+                let (view, doc) = current!(cxt.editor);
+
+                let pos = match view.pos_at_screen_coords(doc, row, column) {
+                    Some(pos) => pos,
+                    None => return EventResult::Ignored,
+                };
+
+                let mut selection = doc.selection(view.id).clone();
+                let primary = selection.primary_mut();
+                *primary = Range::new(primary.anchor, pos);
+                doc.set_selection(view.id, selection);
+                EventResult::Consumed(None)
+            }
+
+            MouseEvent {
+                kind: MouseEventKind::ScrollUp | MouseEventKind::ScrollDown,
+                row,
+                column,
+                ..
+            } => {
+                let current_view = cxt.editor.tree.focus;
+
+                let direction = match event.kind {
+                    MouseEventKind::ScrollUp => Direction::Backward,
+                    MouseEventKind::ScrollDown => Direction::Forward,
+                    _ => unreachable!(),
+                };
+
+                let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
+                    view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
+                        .map(|_| view.id)
+                });
+
+                match result {
+                    Some(view_id) => cxt.editor.tree.focus = view_id,
+                    None => return EventResult::Ignored,
+                }
+
+                let offset = cxt.editor.config.scroll_lines.abs() as usize;
+                commands::scroll(cxt, offset, direction);
+
+                cxt.editor.tree.focus = current_view;
+
+                EventResult::Consumed(None)
+            }
+            _ => EventResult::Ignored,
+        }
+    }
+}
+
 impl Component for EditorView {
     fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
+        let mut cxt = commands::Context {
+            selected_register: helix_view::RegisterSelection::default(),
+            editor: &mut cx.editor,
+            count: None,
+            callback: None,
+            on_next_key_callback: None,
+            jobs: cx.jobs,
+        };
+
         match event {
             Event::Resize(_width, _height) => {
                 // Ignore this event, we handle resizing just before rendering to screen.
@@ -706,20 +811,11 @@ impl Component for EditorView {
                 let mut key = KeyEvent::from(key);
                 canonicalize_key(&mut key);
                 // clear status
-                cx.editor.status_msg = None;
+                cxt.editor.status_msg = None;
 
-                let (_, doc) = current!(cx.editor);
+                let (_, doc) = current!(cxt.editor);
                 let mode = doc.mode();
 
-                let mut cxt = commands::Context {
-                    selected_register: helix_view::RegisterSelection::default(),
-                    editor: &mut cx.editor,
-                    count: None,
-                    callback: None,
-                    on_next_key_callback: None,
-                    jobs: cx.jobs,
-                };
-
                 if let Some(on_next_key) = self.on_next_key.take() {
                     // if there's a command waiting input, do that first
                     on_next_key(&mut cxt, key);
@@ -773,12 +869,12 @@ impl Component for EditorView {
 
                 // if the command consumed the last view, skip the render.
                 // on the next loop cycle the Application will then terminate.
-                if cx.editor.should_close() {
+                if cxt.editor.should_close() {
                     return EventResult::Ignored;
                 }
 
-                let (view, doc) = current!(cx.editor);
-                view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
+                let (view, doc) = current!(cxt.editor);
+                view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff);
 
                 // mode transitions
                 match (mode, doc.mode()) {
@@ -805,58 +901,8 @@ impl Component for EditorView {
 
                 EventResult::Consumed(callback)
             }
-            Event::Mouse(MouseEvent {
-                kind: MouseEventKind::Down(MouseButton::Left),
-                row,
-                column,
-                modifiers,
-                ..
-            }) => {
-                let editor = &mut cx.editor;
 
-                let result = editor.tree.views().find_map(|(view, _focus)| {
-                    view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
-                        .map(|pos| (pos, view.id))
-                });
-
-                if let Some((pos, view_id)) = result {
-                    let doc = &mut editor.documents[editor.tree.get(view_id).doc];
-
-                    if modifiers == crossterm::event::KeyModifiers::ALT {
-                        let selection = doc.selection(view_id).clone();
-                        doc.set_selection(view_id, selection.push(Range::point(pos)));
-                    } else {
-                        doc.set_selection(view_id, Selection::point(pos));
-                    }
-
-                    editor.tree.focus = view_id;
-
-                    return EventResult::Consumed(None);
-                }
-
-                EventResult::Ignored
-            }
-
-            Event::Mouse(MouseEvent {
-                kind: MouseEventKind::Drag(MouseButton::Left),
-                row,
-                column,
-                ..
-            }) => {
-                let (view, doc) = current!(cx.editor);
-
-                let pos = match view.pos_at_screen_coords(doc, row, column) {
-                    Some(pos) => pos,
-                    None => return EventResult::Ignored,
-                };
-
-                let mut selection = doc.selection(view.id).clone();
-                let primary = selection.primary_mut();
-                *primary = Range::new(primary.anchor, pos);
-                doc.set_selection(view.id, selection);
-                EventResult::Consumed(None)
-            }
-            Event::Mouse(_) => EventResult::Ignored,
+            Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt),
         }
     }
 
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index e5ba0d51..ec3cedd6 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -21,10 +21,12 @@ use helix_core::Position;
 use serde::Deserialize;
 
 #[derive(Debug, Clone, PartialEq, Deserialize)]
-#[serde(rename_all = "kebab-case")]
+#[serde(rename_all = "kebab-case", default)]
 pub struct Config {
     /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
     pub scrolloff: usize,
+    /// Number of lines to scroll at once. Defaults to 3
+    pub scroll_lines: isize,
     /// Mouse support. Defaults to true.
     pub mouse: bool,
 }
@@ -33,6 +35,7 @@ impl Default for Config {
     fn default() -> Self {
         Self {
             scrolloff: 5,
+            scroll_lines: 3,
             mouse: true,
         }
     }