touchpad initial support
This commit is contained in:
@@ -421,6 +421,125 @@ assert terminal.process.written == "", repr(terminal.process.written)
|
|||||||
print("alternate scroll tests passed (3 lines per tick: arrows on alt screen, SGR when tracking, inert otherwise)")
|
print("alternate scroll tests passed (3 lines per tick: arrows on alt screen, SGR when tracking, inert otherwise)")
|
||||||
|
|
||||||
|
|
||||||
|
# the trackpad never fires the mousemap on macOS (#1838): the poller harvests
|
||||||
|
# viewport displacement from the follow anchor into wheel lines, snaps back.
|
||||||
|
# ticks are stepped one at a time — the poller reschedules itself, so a
|
||||||
|
# blanket sublime.run_timeouts() would loop forever
|
||||||
|
terminal, stream = make_terminal(rows=5, cols=20)
|
||||||
|
Terminal._terminals[id(terminal)] = terminal
|
||||||
|
terminal.view.id = lambda: id(terminal)
|
||||||
|
terminal.view.text = "\n".join("row {}".format(i) for i in range(8))
|
||||||
|
terminal.offset = 3
|
||||||
|
ANCHOR = 3 * LINE_HEIGHT
|
||||||
|
terminal.viewport_y = ANCHOR
|
||||||
|
terminal.view.viewport = (0, ANCHOR)
|
||||||
|
stream.feed("\x1b[?1002h\x1b[?1006h")
|
||||||
|
|
||||||
|
del sublime._timeouts[:]
|
||||||
|
terminal._sync_wheel_flag()
|
||||||
|
assert terminal._trackpad_running
|
||||||
|
terminal._sync_wheel_flag()
|
||||||
|
assert len(sublime._timeouts) == 1, "one timer chain per terminal"
|
||||||
|
|
||||||
|
|
||||||
|
def tick():
|
||||||
|
sublime._timeouts.pop(0)()
|
||||||
|
|
||||||
|
|
||||||
|
def displace(pixels):
|
||||||
|
terminal.process.written = ""
|
||||||
|
terminal.view.viewport = (0, ANCHOR + pixels)
|
||||||
|
tick()
|
||||||
|
return terminal.process.written
|
||||||
|
|
||||||
|
|
||||||
|
# the first tick only arms the baseline; a gesture on it must stay silent
|
||||||
|
assert displace(50) == "", repr(terminal.process.written)
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR + 50), "arming must not snap"
|
||||||
|
|
||||||
|
# 2.5 lines down: two wheel-down reports, snap back, the fraction kept
|
||||||
|
assert displace(50) == "\x1b[<65;1;3M" * 2
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR), terminal.view.viewport
|
||||||
|
assert terminal._trackpad_acc == 0.5, terminal._trackpad_acc
|
||||||
|
|
||||||
|
# half a line more completes the kept fraction into one report
|
||||||
|
assert displace(10) == "\x1b[<65;1;3M"
|
||||||
|
|
||||||
|
# a resting tick clears the remainder, so it cannot fire a gesture later
|
||||||
|
assert displace(10) == ""
|
||||||
|
assert displace(0) == ""
|
||||||
|
assert terminal._trackpad_acc == 0, terminal._trackpad_acc
|
||||||
|
|
||||||
|
# upward displacement reports wheel-up
|
||||||
|
assert displace(-30) == "\x1b[<64;1;3M"
|
||||||
|
|
||||||
|
# a renderer re-anchor (fold reset, follow-scroll) is not a gesture
|
||||||
|
displace(0)
|
||||||
|
terminal.viewport_y = 0
|
||||||
|
terminal.view.viewport = (0, 0)
|
||||||
|
tick()
|
||||||
|
assert terminal.process.written == "", repr(terminal.process.written)
|
||||||
|
terminal.viewport_y = ANCHOR
|
||||||
|
terminal.view.viewport = (0, ANCHOR)
|
||||||
|
tick()
|
||||||
|
|
||||||
|
# a queued follow-scroll must not starve the poller: apps repaint while
|
||||||
|
# scrolling (less redraws per line, claude constantly), so this races every
|
||||||
|
# gesture and used to read as shaking
|
||||||
|
terminal._scroll_pending = True
|
||||||
|
assert displace(40) == "\x1b[<65;1;3M" * 2
|
||||||
|
terminal._scroll_pending = False
|
||||||
|
|
||||||
|
# nor may the follow-scroll itself discard un-harvested displacement while
|
||||||
|
# the anchor is unchanged: the poller owns the pin
|
||||||
|
terminal.view.viewport = (0, ANCHOR + 30)
|
||||||
|
scroll_to_cursor(terminal.view, terminal)
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR + 30), terminal.view.viewport
|
||||||
|
terminal.process.written = ""
|
||||||
|
tick()
|
||||||
|
assert terminal.process.written == "\x1b[<65;1;3M", repr(terminal.process.written)
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR), terminal.view.viewport
|
||||||
|
displace(0) # drop the half-line remainder
|
||||||
|
|
||||||
|
# a real anchor change still pins: that is the follow-scroll's job
|
||||||
|
terminal.view.text += "row 8\n"
|
||||||
|
scroll_to_cursor(terminal.view, terminal)
|
||||||
|
assert terminal.viewport_y == ANCHOR + LINE_HEIGHT, terminal.viewport_y
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR + LINE_HEIGHT), terminal.view.viewport
|
||||||
|
terminal.view.text = terminal.view.text[:-len("row 8\n")]
|
||||||
|
terminal.viewport_y = ANCHOR
|
||||||
|
terminal.view.viewport = (0, ANCHOR)
|
||||||
|
tick() # settle back on the restored anchor
|
||||||
|
|
||||||
|
# a jump of a viewport or more (scrollbar, page scroll) disengages: the user
|
||||||
|
# browses the scrollback natively until they come back to the anchor
|
||||||
|
extent = terminal.view.viewport_extent()[1]
|
||||||
|
assert displace(-extent) == ""
|
||||||
|
assert not terminal._trackpad_engaged
|
||||||
|
assert terminal.view.viewport == (0, ANCHOR - extent), "browsing must not snap"
|
||||||
|
assert displace(-30) == "", "still browsing"
|
||||||
|
terminal.view.viewport = (0, ANCHOR)
|
||||||
|
tick() # back at the anchor: re-engage
|
||||||
|
assert terminal._trackpad_engaged
|
||||||
|
assert displace(20) == "\x1b[<65;1;3M"
|
||||||
|
|
||||||
|
# on the alt screen without tracking the same displacement turns into arrows
|
||||||
|
stream.feed("\x1b[?1002l\x1b[?1006l\x1b[?1049h")
|
||||||
|
terminal._sync_wheel_flag()
|
||||||
|
assert len(sublime._timeouts) == 1, "capture never lapsed, no second chain"
|
||||||
|
tick() # alt entry left the anchor alone, but spend a tick anyway
|
||||||
|
assert displace(-20) == "\x1b[A"
|
||||||
|
|
||||||
|
# capture released: the poller stops and unhooks itself
|
||||||
|
stream.feed("\x1b[?1049l")
|
||||||
|
terminal._sync_wheel_flag()
|
||||||
|
tick()
|
||||||
|
assert not terminal._trackpad_running
|
||||||
|
assert sublime._timeouts == [], sublime._timeouts
|
||||||
|
|
||||||
|
print("trackpad tests passed (displacement harvested, fractions kept, jumps browse, poller unhooks)")
|
||||||
|
|
||||||
|
|
||||||
# PgUp/PgDn with the configured modifier pages the view instead of reaching the
|
# PgUp/PgDn with the configured modifier pages the view instead of reaching the
|
||||||
# app; every other combination is sent as before. 24 scrollback rows above an
|
# app; every other combination is sent as before. 24 scrollback rows above an
|
||||||
# 8-row screen, viewport at the follow anchor
|
# 8-row screen, viewport at the follow anchor
|
||||||
@@ -546,34 +665,56 @@ stream.feed("\x1b[?1049h")
|
|||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
||||||
assert terminal.viewport_y == 0, terminal.viewport_y
|
assert terminal.viewport_y == 0, terminal.viewport_y
|
||||||
assert terminal.view.settings().get("scroll_past_end") is False, "alt screen must not be scrollable"
|
assert terminal.view.settings().get("scroll_past_end") is True, \
|
||||||
|
"the scroll range must stay: the trackpad poller reads it"
|
||||||
|
|
||||||
# a second render leaves the fold alone
|
# a second render leaves the fold alone
|
||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
||||||
|
|
||||||
# a disturbed fold is re-asserted on the next render
|
# a disturbed fold is re-asserted on the next render, keeping the anchor:
|
||||||
|
# apps repaint the boundary row every scroll, and a per-repair reset made the
|
||||||
|
# poller read the thrashing anchor as phantom down-lines (dead trackpad-up)
|
||||||
|
terminal.viewport_y = 60
|
||||||
terminal.view.folds = [(0, fold_end - 2)]
|
terminal.view.folds = [(0, fold_end - 2)]
|
||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
|
||||||
|
assert terminal.viewport_y == 60, terminal.viewport_y
|
||||||
|
|
||||||
stream.feed("\x1b[?1049l")
|
stream.feed("\x1b[?1049l")
|
||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [], terminal.view.folds
|
assert terminal.view.folds == [], terminal.view.folds
|
||||||
assert terminal.view.settings().get("scroll_past_end") is True, "primary screen scrolls past end again"
|
assert terminal.view.settings().get("scroll_past_end") is True, "left untouched"
|
||||||
|
|
||||||
# without scrollback there is nothing to fold, but the scroll lock still applies
|
# without scrollback there is nothing to fold
|
||||||
terminal, stream = make_terminal(rows=5, cols=20)
|
terminal, stream = make_terminal(rows=5, cols=20)
|
||||||
terminal.view = FakeView("r0\nr1\nr2\nr3\nr4\n")
|
terminal.view = FakeView("r0\nr1\nr2\nr3\nr4\n")
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal.view)
|
||||||
stream.feed("\x1b[?1049h")
|
stream.feed("\x1b[?1049h")
|
||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [], terminal.view.folds
|
assert terminal.view.folds == [], terminal.view.folds
|
||||||
assert terminal.view.settings().get("scroll_past_end") is False, "alt screen must not be scrollable"
|
|
||||||
|
|
||||||
print("alt fold tests passed (scrollback folded on entry, restored on exit)")
|
print("alt fold tests passed (scrollback folded on entry, restored on exit)")
|
||||||
|
|
||||||
|
|
||||||
|
# a fresh view gets one blank scrollback row: sublime has no layout above row
|
||||||
|
# 0, so without it the trackpad poller has no upward slack — and on the alt
|
||||||
|
# screen no line ever retires to provide it (trackpad-up was dead in less)
|
||||||
|
terminal, stream = make_terminal(rows=5, cols=20)
|
||||||
|
Terminal._terminals[id(terminal)] = terminal
|
||||||
|
terminal.view.id = lambda: id(terminal)
|
||||||
|
command = make_command(terminal.view)
|
||||||
|
stream.feed("hello")
|
||||||
|
command.run(None)
|
||||||
|
assert terminal.offset == 1, terminal.offset
|
||||||
|
assert terminal.view.text.startswith("\nhello"), repr(terminal.view.text)
|
||||||
|
command.run(None) # only a fresh view is seeded
|
||||||
|
assert terminal.offset == 1, terminal.offset
|
||||||
|
del sublime._timeouts[:] # drop the follow-scrolls the renders queued
|
||||||
|
|
||||||
|
print("seed row tests passed (blank row above a fresh screen, seeded once)")
|
||||||
|
|
||||||
|
|
||||||
# a row whose first cells were never written: they read back as default cells
|
# a row whose first cells were never written: they read back as default cells
|
||||||
# (StaticDefaultDict), so they form their own uncoloured run and the coloured
|
# (StaticDefaultDict), so they form their own uncoloured run and the coloured
|
||||||
# run that follows keeps the right column
|
# run that follows keeps the right column
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ class VtermKeypressCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
def page_scrolled(self, terminal, key=None, **mods):
|
def page_scrolled(self, terminal, key=None, **mods):
|
||||||
"""Page the view one screen over the scrollback, before vterm_show_cursor,
|
"""Page the view one screen over the scrollback, before vterm_show_cursor,
|
||||||
whose scroll would pull the viewport straight back down. The alt screen is
|
whose scroll would pull the viewport straight back down. The alt screen
|
||||||
folded and has no scroll range, so an app there keeps the key.
|
has its scrollback folded away, so an app there keeps the key.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: the view consumed the key; False sends it to the app.
|
bool: the view consumed the key; False sends it to the app.
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
""" SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select """
|
""" SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select """
|
||||||
|
import math
|
||||||
|
|
||||||
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
|
from .render import following
|
||||||
from .terminal import Terminal
|
from .terminal import Terminal
|
||||||
|
|
||||||
# SGR button numbers for the wheel; 64 is the "scroll" bit plus direction
|
# SGR button numbers for the wheel; 64 is the "scroll" bit plus direction
|
||||||
@@ -11,6 +15,27 @@ WHEEL_DOWN = 65
|
|||||||
# line per report and has no setting of its own
|
# line per report and has no setting of its own
|
||||||
WHEEL_LINES = 3
|
WHEEL_LINES = 3
|
||||||
|
|
||||||
|
# trackpad poll period: one frame at 60 Hz, so a snapped-back displacement
|
||||||
|
# never exceeds a frame's worth of motion
|
||||||
|
TRACKPAD_POLL_MS = 16
|
||||||
|
|
||||||
|
|
||||||
|
def send_scroll(terminal, up, lines, cell=None):
|
||||||
|
"""Report `lines` scroll lines to the app, or fake arrow keys on the alt screen.
|
||||||
|
|
||||||
|
One report per line, never one batched write: pi matches the SGR form
|
||||||
|
against a whole stdin chunk and drops anything longer."""
|
||||||
|
if terminal.screen.mouse_reporting_enabled():
|
||||||
|
if cell is None:
|
||||||
|
cell = (1, terminal.screen.lines // 2 + 1)
|
||||||
|
for _ in range(lines):
|
||||||
|
terminal.send_mouse(WHEEL_UP if up else WHEEL_DOWN, cell[0], cell[1], pressed=True)
|
||||||
|
elif terminal.screen.alternate_buffer_mode:
|
||||||
|
# alternate scroll: an alt-screen app that does not track the mouse
|
||||||
|
# gets arrow keys per wheel tick, as kitty/iTerm2/xterm do
|
||||||
|
for _ in range(lines):
|
||||||
|
terminal.send_key(key="up" if up else "down")
|
||||||
|
|
||||||
|
|
||||||
def _event_to_cell(view, terminal, event, clamp=False):
|
def _event_to_cell(view, terminal, event, clamp=False):
|
||||||
"""Map a mouse event to 1-based (col, row) on the live screen, or None.
|
"""Map a mouse event to 1-based (col, row) on the live screen, or None.
|
||||||
@@ -72,7 +97,7 @@ class VtermScrollCommand(sublime_plugin.TextCommand):
|
|||||||
"""Bound to scroll wheel in the mousemap while the app tracks the mouse or is on the alt screen."""
|
"""Bound to scroll wheel in the mousemap while the app tracks the mouse or is on the alt screen."""
|
||||||
|
|
||||||
def run(self, edit, event=None, up=False):
|
def run(self, edit, event=None, up=False):
|
||||||
"""Report the wheel to the app, or fake arrow keys on the alt screen.
|
"""Report one wheel notch to the app.
|
||||||
|
|
||||||
Only bound while the app owns the wheel; otherwise sublime scrolls the
|
Only bound while the app owns the wheel; otherwise sublime scrolls the
|
||||||
view over the real scrollback text and this never runs.
|
view over the real scrollback text and this never runs.
|
||||||
@@ -80,20 +105,65 @@ class VtermScrollCommand(sublime_plugin.TextCommand):
|
|||||||
terminal = Terminal.from_id(self.view.id())
|
terminal = Terminal.from_id(self.view.id())
|
||||||
if not terminal:
|
if not terminal:
|
||||||
return
|
return
|
||||||
if terminal.screen.mouse_reporting_enabled():
|
cell = None
|
||||||
cell = _event_to_cell(self.view, terminal, event, clamp=True) if event else None
|
if terminal.screen.mouse_reporting_enabled() and event:
|
||||||
if cell is None:
|
cell = _event_to_cell(self.view, terminal, event, clamp=True)
|
||||||
cell = (1, terminal.screen.lines // 2 + 1)
|
send_scroll(terminal, up, WHEEL_LINES, cell)
|
||||||
# one report per line, never one batched write: pi matches the SGR
|
|
||||||
# form against a whole stdin chunk and drops anything longer
|
|
||||||
for _ in range(WHEEL_LINES):
|
|
||||||
terminal.send_mouse(WHEEL_UP if up else WHEEL_DOWN, cell[0], cell[1], pressed=True)
|
|
||||||
elif terminal.screen.alternate_buffer_mode:
|
|
||||||
# alternate scroll: an alt-screen app that does not track the mouse
|
|
||||||
# gets arrow keys per wheel tick, as kitty/iTerm2/xterm do
|
|
||||||
for _ in range(WHEEL_LINES):
|
|
||||||
terminal.send_key(key="up" if up else "down")
|
|
||||||
|
|
||||||
def want_event(self):
|
def want_event(self):
|
||||||
"""Ask sublime for the wheel coordinates."""
|
"""Ask sublime for the wheel coordinates."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_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
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
terminal._trackpad_running = False
|
||||||
|
return
|
||||||
|
anchor = terminal.viewport_y
|
||||||
|
extent = view.viewport_extent()[1]
|
||||||
|
if (anchor, extent) != terminal._trackpad_baseline:
|
||||||
|
# the renderer moved the anchor (follow-scroll, fold, resize): re-arm
|
||||||
|
# around the new position instead of reading the move as a gesture
|
||||||
|
terminal._trackpad_baseline = (anchor, extent)
|
||||||
|
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:
|
||||||
|
# at rest a stale fraction must not fire on the next gesture
|
||||||
|
terminal._trackpad_acc = 0.0
|
||||||
|
elif abs(dy) >= extent:
|
||||||
|
# scrollbar/minimap jump or page scroll, not a trackpad gesture:
|
||||||
|
# let the user browse until they return to the anchor
|
||||||
|
terminal._trackpad_engaged = False
|
||||||
|
else:
|
||||||
|
terminal._trackpad_acc += dy / view.line_height()
|
||||||
|
lines = math.trunc(terminal._trackpad_acc)
|
||||||
|
terminal._trackpad_acc -= lines
|
||||||
|
if lines:
|
||||||
|
send_scroll(terminal, up=lines < 0, lines=abs(lines))
|
||||||
|
view.set_viewport_position((0, anchor), False)
|
||||||
|
sublime.set_timeout(lambda: _trackpad_tick(terminal), TRACKPAD_POLL_MS)
|
||||||
|
|||||||
@@ -172,6 +172,14 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
terminal.pending_clear_screen = False
|
terminal.pending_clear_screen = False
|
||||||
self.clear_view(edit, terminal, wipe_screen=True)
|
self.clear_view(edit, terminal, wipe_screen=True)
|
||||||
|
|
||||||
|
if view.size() == 0 and terminal.offset == 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
|
||||||
|
# the alt screen is never
|
||||||
|
view.insert(edit, 0, "\n")
|
||||||
|
terminal.offset = 1
|
||||||
|
|
||||||
self.update_lines(edit, terminal)
|
self.update_lines(edit, terminal)
|
||||||
self.sync_alt_fold(terminal)
|
self.sync_alt_fold(terminal)
|
||||||
|
|
||||||
@@ -200,25 +208,27 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
def sync_alt_fold(self, terminal):
|
def sync_alt_fold(self, terminal):
|
||||||
"""Hide the scrollback while the app owns the alt screen: fold it away
|
"""Hide the scrollback while the app owns the alt screen: fold it away
|
||||||
(the view text is its only copy) and drop past-end scrolling so the
|
(the view text is its only copy). The scroll range stays: the trackpad
|
||||||
view has no scroll range. The fold is re-checked every render — the
|
poller pins the viewport and needs the slack to read the trackpad.
|
||||||
screen's first row is replaced on every region scroll, and an edit at
|
The fold is re-checked every render — the screen's first row is
|
||||||
the fold boundary can silently disturb a fold."""
|
replaced on every region scroll, and an edit at the fold boundary can
|
||||||
|
silently disturb a fold."""
|
||||||
view = self.view
|
view = self.view
|
||||||
alt = terminal.screen.alternate_buffer_mode
|
alt = terminal.screen.alternate_buffer_mode
|
||||||
if not alt and not terminal.applied_alt_screen:
|
if not alt and not terminal.applied_alt_screen:
|
||||||
return
|
return
|
||||||
if alt != terminal.applied_alt_screen:
|
|
||||||
terminal.applied_alt_screen = alt
|
terminal.applied_alt_screen = alt
|
||||||
view.settings().set("scroll_past_end", not alt)
|
|
||||||
end = view.text_point(terminal.offset, 0) - 1 if alt and terminal.offset else 0
|
end = view.text_point(terminal.offset, 0) - 1 if alt and terminal.offset else 0
|
||||||
want = [(0, end)] if end > 0 else []
|
want = [(0, end)] if end > 0 else []
|
||||||
if [(r.begin(), r.end()) for r in view.folded_regions()] == want:
|
had = [(r.begin(), r.end()) for r in view.folded_regions()]
|
||||||
|
if had == want:
|
||||||
return
|
return
|
||||||
view.unfold(sublime.Region(0, view.size()))
|
view.unfold(sublime.Region(0, view.size()))
|
||||||
if want and view.fold(sublime.Region(0, end)):
|
if want and view.fold(sublime.Region(0, end)) and not had:
|
||||||
# a fold collapses the layout; a stale viewport anchor would stop
|
# a fresh fold collapses the layout; a stale viewport anchor would
|
||||||
# the follow-scroll gate from re-engaging
|
# 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
|
||||||
terminal.viewport_y = 0
|
terminal.viewport_y = 0
|
||||||
|
|
||||||
def sync_cursor_style(self, terminal):
|
def sync_cursor_style(self, terminal):
|
||||||
@@ -535,6 +545,11 @@ def scroll_to_cursor(view, terminal):
|
|||||||
"""Follow the output: pin the viewport to the bottom of the live screen."""
|
"""Follow the output: pin the viewport to the bottom of the live screen."""
|
||||||
terminal._scroll_pending = False
|
terminal._scroll_pending = False
|
||||||
y = bottom_y(view, terminal)
|
y = bottom_y(view, terminal)
|
||||||
|
if y == terminal.viewport_y and terminal._trackpad_running and terminal._trackpad_engaged:
|
||||||
|
# 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
|
||||||
|
return
|
||||||
terminal.viewport_y = y
|
terminal.viewport_y = y
|
||||||
view.set_viewport_position((0, y), False)
|
view.set_viewport_position((0, y), False)
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ class Terminal:
|
|||||||
self._send_lock = threading.Lock()
|
self._send_lock = threading.Lock()
|
||||||
self._wheel_captured = False
|
self._wheel_captured = False
|
||||||
self._scroll_pending = False
|
self._scroll_pending = False
|
||||||
|
# trackpad poller (mouse.ensure_trackpad): reschedule guard, fractional
|
||||||
|
# line remainder, harvesting vs. browsing, last seen (anchor, extent)
|
||||||
|
self._trackpad_running = False
|
||||||
|
self._trackpad_acc = 0.0
|
||||||
|
self._trackpad_engaged = True
|
||||||
|
self._trackpad_baseline = None
|
||||||
self.pending_clear = False
|
self.pending_clear = False
|
||||||
self.pending_clear_screen = False
|
self.pending_clear_screen = False
|
||||||
# a forced frame (set on activation); rendering stays on the renderer
|
# a forced frame (set on activation); rendering stays on the renderer
|
||||||
@@ -392,6 +398,11 @@ class Terminal:
|
|||||||
if captured != self._wheel_captured:
|
if captured != self._wheel_captured:
|
||||||
self._wheel_captured = captured
|
self._wheel_captured = captured
|
||||||
self.view.settings().set("vterm_view.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)
|
||||||
|
|
||||||
def send_focus(self, focused):
|
def send_focus(self, focused):
|
||||||
"""Report focus gain or loss, when the app asked for it (?1004)."""
|
"""Report focus gain or loss, when the app asked for it (?1004)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user