Smooth Scroll
MonoTerm implements pixel-level scroll accumulation for fluid scrolling experience, especially on trackpads.The Problem: Row-Based Scrolling
Traditional terminals scroll by whole rows. This creates jerky movement on trackpads that send sub-row pixel deltas.╔═══════════════════════════════════════════════════════════════════════════════╗
║ TRADITIONAL ROW-BASED SCROLLING ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Trackpad sends: deltaY = 3px, 5px, 2px, 4px, 6px... ║
║ ║
║ Row height = 20px ║
║ ║
║ ┌─────────────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ Input: 3px 5px 2px 4px 6px (total: 20px) │ ║
║ │ │ │ │ │ │ │ ║
║ │ ▼ ▼ ▼ ▼ ▼ │ ║
║ │ Result: NONE NONE NONE NONE JUMP 1 ROW │ ║
║ │ │ ║
║ │ ────────────────────────┬──── │ ║
║ │ Nothing │ Sudden jump │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────────────┘ ║
║ ║
║ User experience: Laggy, then sudden jump ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
The Solution: Pixel Accumulation
MonoTerm accumulates sub-row pixel deltas until a full row is reached.╔═══════════════════════════════════════════════════════════════════════════════╗
║ PIXEL ACCUMULATION SCROLLING ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Trackpad sends: deltaY = 3px, 5px, 2px, 4px, 6px... ║
║ ║
║ Row height = 20px ║
║ ║
║ ┌─────────────────────────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ pendingY accumulator: │ ║
║ │ │ ║
║ │ Input: 3px 5px 2px 4px 6px │ ║
║ │ │ │ │ │ │ │ ║
║ │ ▼ ▼ ▼ ▼ ▼ │ ║
║ │ pendingY: 3 → 8 → 10 → 14 → 20 │ ║
║ │ │ │ │ │ │ │ ║
║ │ ▼ ▼ ▼ ▼ ▼ │ ║
║ │ Action: wait wait wait wait SCROLL 1 ROW │ ║
║ │ pendingY = 0 │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────────────────────────┘ ║
║ ║
║ User experience: Smooth, predictable scrolling ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
State Interface
interface SmoothScrollState {
pendingY: number; // Accumulated pixels not yet scrolled
rowHeight: number; // Current row height in pixels
isAnimating: boolean; // Animation in progress
animationId: number; // RAF handle for cancellation
}
Trackpad vs Mouse Detection
MonoTerm detects input device type and applies different scroll behavior.╔═══════════════════════════════════════════════════════════════════════════════╗
║ INPUT DEVICE DETECTION ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Detection logic: ║
║ ───────────────── ║
║ ║
║ isPrecision = Math.abs(ev.deltaY) < 10 && ║
║ ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL ║
║ ║
║ ║
║ ┌─────────────────────────────┐ ┌─────────────────────────────┐ ║
║ │ │ │ │ ║
║ │ TRACKPAD (isPrecision) │ │ MOUSE WHEEL (!isPrecision) │ ║
║ │ │ │ │ ║
║ │ deltaY: 1-9 pixels │ │ deltaY: 100+ pixels │ ║
║ │ deltaMode: PIXEL │ │ deltaMode: LINE or PAGE │ ║
║ │ │ │ │ ║
║ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ ║
║ │ │ Pixel accumulation │ │ │ │ Direct row scroll │ │ ║
║ │ │ + Bezier animation │ │ │ │ (traditional behavior)│ │ ║
║ │ └───────────────────────┘ │ │ └───────────────────────┘ │ ║
║ │ │ │ │ ║
║ └─────────────────────────────┘ └─────────────────────────────┘ ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Bezier Animation
For smooth deceleration, MonoTerm applies easing functions to scroll animation.╔═══════════════════════════════════════════════════════════════════════════════╗
║ BEZIER EASING FUNCTIONS ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Available easing curves: ║
║ ║
║ ║
║ ease-out (default) ease-in ease-in-out ║
║ ───────────────── ──────── ─────────── ║
║ f(t) = 1 - (1-t)³ f(t) = t³ cubic-bezier ║
║ ║
║ │ xxxxxxx │ x │ xxx ║
║ │ xx │ x │ xx xx ║
║ │ x │ x │ x x ║
║ │x │ xx │ x x ║
║ │x │ xx │ x x ║
║ │x │ xx │x x║
║ └─────────── └─────────── └─────────── ║
║ ║
║ Fast start, Slow start, Smooth both ║
║ smooth stop fast end ends ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Configuration
Users can customize scroll behavior via settings:| Setting | Default | Description |
|---|---|---|
terminalSmoothScrollDuration | 150ms | Animation duration |
terminalSmoothScrollEasing | ease-out | Easing function |
╔═══════════════════════════════════════════════════════════════════════════════╗
║ SCROLL BEHAVIOR FLOW ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Wheel Event ║
║ │ ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ Check alt buffer │──── YES ───▶ Direct scroll (vim/tmux mode) ║
║ └─────────┬───────────┘ ║
║ │ NO ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ isPrecision? │──── NO ────▶ Traditional row scroll ║
║ └─────────┬───────────┘ ║
║ │ YES (trackpad) ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ Add to pendingY │ ║
║ └─────────┬───────────┘ ║
║ │ ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ |pendingY| >= row? │──── NO ────▶ Wait for more input ║
║ └─────────┬───────────┘ ║
║ │ YES ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ Calculate rows │ ║
║ │ rows = pendingY/h │ ║
║ └─────────┬───────────┘ ║
║ │ ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ Animate scroll │ ║
║ │ (Bezier easing) │ ║
║ └─────────┬───────────┘ ║
║ │ ║
║ ▼ ║
║ ┌─────────────────────┐ ║
║ │ pendingY = remainder│ ║
║ └─────────────────────┘ ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Alt Buffer Bypass
When in alternate screen mode (vim, tmux, less), smooth scroll is bypassed for native app handling.╔═══════════════════════════════════════════════════════════════════════════════╗
║ ALT BUFFER DETECTION ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Normal buffer (shell): ║
║ ────────────────────── ║
║ ┌─────────────────────────────────────┐ ║
║ │ $ ls -la │ ║
║ │ total 48 │ ← Smooth scroll enabled ║
║ │ drwxr-xr-x 12 user staff 384 │ ║
║ │ -rw-r--r-- 1 user staff 1024 │ ║
║ └─────────────────────────────────────┘ ║
║ ║
║ ║
║ Alt buffer (vim/tmux): ║
║ ────────────────────── ║
║ ┌─────────────────────────────────────┐ ║
║ │ ~ │ ║
║ │ ~ VIM - file.txt │ ← Direct scroll to app ║
║ │ ~ │ (vim handles scrolling) ║
║ │ -- INSERT -- │ ║
║ └─────────────────────────────────────┘ ║
║ ║
║ ║
║ Detection: term.buffer.active.type === 'alternate' ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Implementation
Key code location:terminal-scrollbar.ts
private handleWheel(ev: WheelEvent): void {
// Alt buffer bypass
if (this.term.buffer.active.type === 'alternate') {
return; // Let the app handle it
}
const isPrecision = Math.abs(ev.deltaY) < 10 &&
ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL;
if (isPrecision) {
// Trackpad: accumulate pixels
this.state.pendingY += ev.deltaY;
const rowHeight = this.getRowHeight();
const rows = Math.trunc(this.state.pendingY / rowHeight);
if (rows !== 0) {
this.animateScroll(rows);
this.state.pendingY %= rowHeight; // Keep remainder
}
} else {
// Mouse wheel: direct scroll
const rows = Math.sign(ev.deltaY) * 3; // 3 rows per notch
this.term.scrollLines(rows);
}
}
Related Documentation
Sync Rendering
Immediate render for flicker-free display
xterm.js Renderer
How xterm.js is used as display component