diff --git a/vterm/mouse.py b/vterm/mouse.py index d0a833b..4fe554b 100644 --- a/vterm/mouse.py +++ b/vterm/mouse.py @@ -1,6 +1,4 @@ """ SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select """ -import math - import sublime import sublime_plugin @@ -115,18 +113,17 @@ class VtermScrollCommand(sublime_plugin.TextCommand): return True -def ensure_trackpad(terminal): +def start_trackpad(terminal): """Start the poller that turns trackpad scrolling into wheel lines. The trackpad never fires the mousemap scroll bindings on macOS (sublime_text#1838); its only trace is viewport movement. While the app owns the wheel the mousemap keeps the physical wheel off the viewport, so any displacement from the follow anchor is trackpad scroll: harvest whole - lines from it, send them like wheel lines, and snap the viewport back.""" - if terminal._trackpad_running: - return + lines from it, send them like wheel lines, and snap the viewport back. + + The caller checks _trackpad_running; the first tick arms the baseline.""" terminal._trackpad_running = True - terminal._trackpad_acc = 0.0 terminal._trackpad_engaged = True terminal._trackpad_baseline = None sublime.set_timeout(lambda: _trackpad_tick(terminal), TRACKPAD_POLL_MS) @@ -136,7 +133,7 @@ def _trackpad_tick(terminal): """One poll: harvest viewport displacement, reschedule while captured.""" view = terminal.view if Terminal.from_id(view.id()) is not terminal or not terminal.alive \ - or not terminal._wheel_captured: + or not terminal._wheel_captured or not terminal.is_visible(): terminal._trackpad_running = False return anchor = terminal.viewport_y @@ -146,13 +143,13 @@ def _trackpad_tick(terminal): # around the new position instead of reading the move as a gesture terminal._trackpad_baseline = (anchor, extent) terminal._trackpad_acc = 0.0 + elif not terminal._trackpad_engaged: + if following(view, terminal): + terminal._trackpad_engaged = True + terminal._trackpad_acc = 0.0 else: dy = view.viewport_position()[1] - anchor - if not terminal._trackpad_engaged: - if following(view, terminal): - terminal._trackpad_engaged = True - terminal._trackpad_acc = 0.0 - elif dy == 0: + if dy == 0: # at rest a stale fraction must not fire on the next gesture terminal._trackpad_acc = 0.0 elif abs(dy) >= extent: @@ -161,7 +158,7 @@ def _trackpad_tick(terminal): terminal._trackpad_engaged = False else: terminal._trackpad_acc += dy / view.line_height() - lines = math.trunc(terminal._trackpad_acc) + lines = int(terminal._trackpad_acc) terminal._trackpad_acc -= lines if lines: send_scroll(terminal, up=lines < 0, lines=abs(lines)) diff --git a/vterm/render.py b/vterm/render.py index f758f96..2d02f7c 100644 --- a/vterm/render.py +++ b/vterm/render.py @@ -172,7 +172,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand): terminal.pending_clear_screen = False self.clear_view(edit, terminal, wipe_screen=True) - if view.size() == 0 and terminal.offset == 0: + if terminal.offset == 0 and view.size() == 0: # one blank scrollback row above a fresh screen: sublime has no # layout above row 0, and the trackpad poller needs upward slack — # without it, trackpad-up is dead until a line retires, which on @@ -217,15 +217,15 @@ class VtermRenderCommand(sublime_plugin.TextCommand): alt = terminal.screen.alternate_buffer_mode if not alt and not terminal.applied_alt_screen: return + entering = alt and not terminal.applied_alt_screen terminal.applied_alt_screen = alt end = view.text_point(terminal.offset, 0) - 1 if alt and terminal.offset else 0 want = [(0, end)] if end > 0 else [] - had = [(r.begin(), r.end()) for r in view.folded_regions()] - if had == want: + if [(r.begin(), r.end()) for r in view.folded_regions()] == want: return view.unfold(sublime.Region(0, view.size())) - if want and view.fold(sublime.Region(0, end)) and not had: - # a fresh fold collapses the layout; a stale viewport anchor would + if want and view.fold(sublime.Region(0, end)) and entering: + # entering collapses the layout; a stale viewport anchor would # stop the follow-scroll gate from re-engaging. re-asserting a # disturbed fold keeps the anchor: apps repaint the boundary row # every scroll, and resetting per repair thrashes the poller @@ -545,7 +545,7 @@ def scroll_to_cursor(view, terminal): """Follow the output: pin the viewport to the bottom of the live screen.""" terminal._scroll_pending = False y = bottom_y(view, terminal) - if y == terminal.viewport_y and terminal._trackpad_running and terminal._trackpad_engaged: + if y == terminal.viewport_y and terminal.trackpad_pinning(): # the anchor is unchanged and the trackpad poller owns the pin: # re-pinning here would discard displacement it has not harvested yet, # and apps repaint while scrolling, so this races every gesture diff --git a/vterm/terminal.py b/vterm/terminal.py index ff3830d..49f137a 100644 --- a/vterm/terminal.py +++ b/vterm/terminal.py @@ -109,7 +109,7 @@ class Terminal: self._send_lock = threading.Lock() self._wheel_captured = False self._scroll_pending = False - # trackpad poller (mouse.ensure_trackpad): reschedule guard, fractional + # trackpad poller (mouse.start_trackpad): reschedule guard, fractional # line remainder, harvesting vs. browsing, last seen (anchor, extent) self._trackpad_running = False self._trackpad_acc = 0.0 @@ -152,6 +152,10 @@ class Terminal: self._visible = group >= 0 and window.active_view_in_group(group) == self.view return self._visible + def trackpad_pinning(self): + """Whether the trackpad poller owns pinning the viewport to the anchor.""" + return self._trackpad_running and self._trackpad_engaged + def is_render_due(self): """Whether an unfocused terminal may draw again. @@ -398,11 +402,11 @@ class Terminal: if captured != self._wheel_captured: self._wheel_captured = captured self.view.settings().set("vterm_view.wheel_captured", captured) - if captured and not self._trackpad_running: - # checked every renderer tick, so a poller that just stopped on a - # capture flap is restarted within a frame or two - from .mouse import ensure_trackpad # module-level import would cycle - ensure_trackpad(self) + if captured and not self._trackpad_running and self.is_visible(): + # checked every renderer tick, so a poller that stopped on a + # capture flap or a hidden tab restarts within a frame or two + from .mouse import start_trackpad # module-level import would cycle + start_trackpad(self) def send_focus(self, focused): """Report focus gain or loss, when the app asked for it (?1004)."""