mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
feat(terminal): animate wheel scrolling instead of jumping a notch at once
Sub-line scroll positions were already in place — the view keeps a fractional remainder and paints the grid shifted by it — but the position was a function of the event, not of time. A notch arrived and the whole distance was applied at once, so whether it looked smooth came down to how fine-grained the platform's deltas happened to be. A macOS trackpad reports pixels, so it did. A wheel on Windows reports whole lines (gpui multiplies the notch by the system's scroll-lines setting, three by default), the fraction came out zero every time, and the view jumped three lines per notch. The sub-line machinery was present and never engaged. A 120-step notch is one discrete pulse; no arithmetic on the delta recovers a continuous gesture from it. So make position a function of time: a notch adds to a remaining distance and each frame consumes a share of what is left, ~120ms to land, exponential, with a sub-pixel remainder snapped rather than approached since every frame of it costs a repaint. Line deltas are discrete and get animated; pixel deltas are continuous and do not — putting an animation between a trackpad and the grid would only add lag. Mouse reporting and alternate-scroll keep forwarding whole lines, which cannot be spread over frames either. The distance in flight is relative rather than an absolute target, so output arriving mid-scroll shifts the grid without dragging the animation elsewhere. Everything that moves the view on its own cancels what is in flight first. Settings -> Terminal -> Scrolling -> Smooth scrolling, on by default. Also pin the scratch config dir in the terminal-view test harness: building a view reads the config, and which test got there first decided whether that touched the real user directory.
This commit is contained in:
@@ -63,6 +63,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
touch one it didn't, so a hand-written skill that happens to share the
|
||||
directory name survives. (#248)
|
||||
|
||||
- **Smooth scrolling for wheel mice** — a notch now eases into place over a
|
||||
handful of frames instead of jumping the whole distance at once.
|
||||
**Settings → Terminal → Scrolling → Smooth scrolling**, on by default.
|
||||
|
||||
Sub-line scroll positions have been in place for a while: the view keeps a
|
||||
fractional remainder and paints the grid shifted by it, so the scrollback
|
||||
need not land on a line boundary. But the position was a function of the
|
||||
*event*, not of *time* — a notch arrived and the whole thing was applied at
|
||||
once. Whether that looked smooth came down entirely to how fine-grained the
|
||||
platform's deltas were. A macOS trackpad reports pixels, so it did. A wheel
|
||||
on Windows reports whole lines (gpui multiplies the notch by the system's
|
||||
scroll-lines setting, three by default), the fraction came out zero every
|
||||
time, and the view jumped three lines per notch — the sub-line machinery was
|
||||
present and never engaged. That is the "no smooth scroll on Windows" report.
|
||||
|
||||
A 120-step wheel notch is one discrete pulse; no amount of arithmetic on the
|
||||
delta recovers a continuous gesture from it. So the fix is to make position a
|
||||
function of time: a notch adds to a remaining distance, and each frame
|
||||
consumes a share of what is left. Roughly 120ms to land, exponential, so most
|
||||
of the travel happens immediately and the tail is invisible — under a pixel
|
||||
of remainder is snapped rather than approached, since every frame of it costs
|
||||
a repaint.
|
||||
|
||||
Trackpads keep the direct path. They already deliver a continuous pixel
|
||||
stream, and putting an animation between the fingers and the grid would only
|
||||
add lag. The split falls out of the event itself — line deltas are discrete
|
||||
and get animated, pixel deltas are continuous and do not. Mouse reporting and
|
||||
alternate-scroll are untouched too: those forward whole lines to the
|
||||
application, which cannot be spread over frames.
|
||||
|
||||
The distance in flight is relative, not an absolute target, so output
|
||||
arriving mid-scroll shifts the grid without dragging the animation somewhere
|
||||
else. Everything that moves the view on its own — jumping to a prompt,
|
||||
dragging a selection past the edge, clearing the scrollback, the keyboard and
|
||||
mouse-reporting paths — cancels what is in flight first.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Scoop shims work again when tty7 is launched from a hardened Windows
|
||||
|
||||
@@ -195,6 +195,11 @@ pub struct Config {
|
||||
pub mouse_hide_while_typing: bool,
|
||||
pub focus_follows_mouse: bool,
|
||||
pub mouse_scroll_multiplier: f32,
|
||||
/// Spread a wheel notch over several frames instead of jumping the whole
|
||||
/// distance at once. Only affects discrete wheel input — trackpads already
|
||||
/// deliver a continuous pixel stream, and animating that would just add lag.
|
||||
#[serde(default = "default_true")]
|
||||
pub smooth_scroll: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub mouse_reporting: bool,
|
||||
pub clipboard_trim_trailing_spaces: bool,
|
||||
@@ -438,6 +443,7 @@ impl Default for Config {
|
||||
mouse_hide_while_typing: true,
|
||||
focus_follows_mouse: false,
|
||||
mouse_scroll_multiplier: 1.0,
|
||||
smooth_scroll: true,
|
||||
mouse_reporting: true,
|
||||
clipboard_trim_trailing_spaces: false,
|
||||
copy_on_select: false,
|
||||
|
||||
+269
-6
@@ -85,6 +85,25 @@ struct DragScroll {
|
||||
side: Side,
|
||||
}
|
||||
|
||||
/// In-flight wheel animation. `remaining` is what is left to scroll, in lines,
|
||||
/// relative to wherever the view happens to be — deliberately not an absolute
|
||||
/// target, so output arriving mid-animation shifts the grid under us without
|
||||
/// dragging the animation somewhere else.
|
||||
#[derive(Clone, Copy)]
|
||||
struct ScrollAnim {
|
||||
remaining: f32,
|
||||
last: std::time::Instant,
|
||||
}
|
||||
|
||||
/// Fraction of the remaining distance consumed per [`SCROLL_ANIM_FRAME`].
|
||||
const SCROLL_ANIM_SMOOTH: f32 = 0.4;
|
||||
/// The frame `SCROLL_ANIM_SMOOTH` is calibrated against, and the tick interval.
|
||||
const SCROLL_ANIM_FRAME: std::time::Duration = std::time::Duration::from_millis(16);
|
||||
/// Below this much left to travel, land instead of asymptoting toward it. A
|
||||
/// twentieth of a line is around a pixel — the tail of an exponential decay is
|
||||
/// invisible long before it ends, and every frame of it costs a full repaint.
|
||||
const SCROLL_ANIM_MIN: f32 = 0.05;
|
||||
|
||||
fn cwd_is_on_host(pane_runs_remotely: bool, host_is_local: bool) -> bool {
|
||||
match pane_runs_remotely {
|
||||
false => host_is_local,
|
||||
@@ -118,6 +137,8 @@ pub struct TerminalView {
|
||||
selecting: bool,
|
||||
drag_scroll: Option<DragScroll>,
|
||||
drag_scroll_epoch: u64,
|
||||
scroll_anim: Option<ScrollAnim>,
|
||||
scroll_anim_epoch: u64,
|
||||
pub title: String,
|
||||
pub marked_text: String,
|
||||
last_mouse_cell: Option<(usize, usize)>,
|
||||
@@ -915,6 +936,8 @@ impl TerminalView {
|
||||
selecting: false,
|
||||
drag_scroll: None,
|
||||
drag_scroll_epoch: 0,
|
||||
scroll_anim: None,
|
||||
scroll_anim_epoch: 0,
|
||||
title: "tty7".to_string(),
|
||||
marked_text: String::new(),
|
||||
last_mouse_cell: None,
|
||||
@@ -1904,6 +1927,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn jump_to_prompt(&mut self) {
|
||||
self.cancel_scroll_anim();
|
||||
let mut term = self.terminal.term.lock();
|
||||
term.selection = None;
|
||||
term.scroll_display(Scroll::Bottom);
|
||||
@@ -2071,6 +2095,7 @@ impl TerminalView {
|
||||
if lines == 0 {
|
||||
return;
|
||||
}
|
||||
self.cancel_scroll_anim();
|
||||
let mut mode = *self.terminal.term.lock().mode();
|
||||
if !self.report_mouse {
|
||||
mode.remove(TermMode::MOUSE_MODE);
|
||||
@@ -2400,6 +2425,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
pub fn clear_scrollback(&mut self, cx: &mut Context<Self>) {
|
||||
self.cancel_scroll_anim();
|
||||
self.terminal.term.lock().grid_mut().clear_history();
|
||||
self.scroll_frac = 0.;
|
||||
self.terminal.marks().clear();
|
||||
@@ -3918,6 +3944,7 @@ impl TerminalView {
|
||||
let Some(ds) = self.drag_scroll else {
|
||||
return false;
|
||||
};
|
||||
self.cancel_scroll_anim();
|
||||
let mut term = self.terminal.term.lock();
|
||||
let before = term.grid().display_offset();
|
||||
term.scroll_display(Scroll::Delta(drag_scroll_step(ds.overshoot)));
|
||||
@@ -3963,9 +3990,12 @@ impl TerminalView {
|
||||
|
||||
fn on_scroll(&mut self, ev: &ScrollWheelEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let mult = cx.global::<Config>().mouse_scroll_multiplier;
|
||||
let raw = match ev.delta {
|
||||
ScrollDelta::Lines(p) => p.y,
|
||||
ScrollDelta::Pixels(p) => p.y.as_f32() / self.line_height.as_f32(),
|
||||
// A wheel notch is one discrete pulse — applying it whole is what makes
|
||||
// scrolling look like it jumps. A trackpad already sends a fine-grained
|
||||
// pixel stream, so it stays on the direct path.
|
||||
let (raw, discrete) = match ev.delta {
|
||||
ScrollDelta::Lines(p) => (p.y, true),
|
||||
ScrollDelta::Pixels(p) => (p.y.as_f32() / self.line_height.as_f32(), false),
|
||||
};
|
||||
let delta = raw * mult;
|
||||
|
||||
@@ -3975,6 +4005,9 @@ impl TerminalView {
|
||||
|| mode.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
|
||||
};
|
||||
if quantized {
|
||||
// Whole lines are what the application gets told about, so this path
|
||||
// cannot be spread over frames.
|
||||
self.cancel_scroll_anim();
|
||||
let total = self.scroll_debt + delta;
|
||||
let lines = total.trunc() as i32;
|
||||
self.scroll_debt = total - lines as f32;
|
||||
@@ -3984,10 +4017,80 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
|
||||
self.smooth_scroll(delta, cx);
|
||||
if discrete && cx.global::<Config>().smooth_scroll {
|
||||
self.queue_scroll_anim(delta, cx);
|
||||
} else {
|
||||
self.cancel_scroll_anim();
|
||||
self.smooth_scroll(delta, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_scroll(&mut self, delta: f32, cx: &mut Context<Self>) {
|
||||
/// Add `delta` to the in-flight animation, starting the frame loop if it is
|
||||
/// idle. Successive notches accumulate rather than restart, so spinning the
|
||||
/// wheel fast still lands exactly where the notches asked for.
|
||||
fn queue_scroll_anim(&mut self, delta: f32, cx: &mut Context<Self>) {
|
||||
if let Some(anim) = self.scroll_anim.as_mut() {
|
||||
anim.remaining += delta;
|
||||
return;
|
||||
}
|
||||
self.scroll_anim = Some(ScrollAnim {
|
||||
remaining: delta,
|
||||
last: std::time::Instant::now(),
|
||||
});
|
||||
self.scroll_anim_epoch += 1;
|
||||
let epoch = self.scroll_anim_epoch;
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(SCROLL_ANIM_FRAME).await;
|
||||
if !matches!(
|
||||
this.update(cx, |view, cx| view.scroll_anim_tick(epoch, cx)),
|
||||
Ok(true)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Drop whatever is left to travel. Every other way the display offset moves
|
||||
/// — jumping to a prompt, dragging a selection past the edge, the keyboard
|
||||
/// and mouse-reporting paths — has to come through here first, or the
|
||||
/// animation would keep walking away from wherever it put us.
|
||||
fn cancel_scroll_anim(&mut self) {
|
||||
if self.scroll_anim.take().is_some() {
|
||||
self.scroll_anim_epoch += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_anim_tick(&mut self, epoch: u64, cx: &mut Context<Self>) -> bool {
|
||||
if epoch != self.scroll_anim_epoch {
|
||||
return false;
|
||||
}
|
||||
let Some(anim) = self.scroll_anim.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
let now = std::time::Instant::now();
|
||||
let dt = now.duration_since(anim.last);
|
||||
anim.last = now;
|
||||
let (step, last) = scroll_anim_step(anim.remaining, dt);
|
||||
anim.remaining -= step;
|
||||
if last {
|
||||
self.scroll_anim = None;
|
||||
}
|
||||
// Hitting the top or the bottom of the scrollback consumes nothing; the
|
||||
// remaining distance has nowhere to go, so stop instead of decaying it
|
||||
// against the clamp for another 100ms.
|
||||
let moved = self.smooth_scroll(step, cx);
|
||||
if !moved {
|
||||
self.cancel_scroll_anim();
|
||||
return false;
|
||||
}
|
||||
!last
|
||||
}
|
||||
|
||||
/// Apply `delta` lines right now. Returns whether anything actually moved.
|
||||
fn smooth_scroll(&mut self, delta: f32, cx: &mut Context<Self>) -> bool {
|
||||
let mut term = self.terminal.term.lock();
|
||||
let offset = term.grid().display_offset();
|
||||
let max = term.grid().history_size();
|
||||
@@ -3999,7 +4102,9 @@ impl TerminalView {
|
||||
if jump != 0 || frac != self.scroll_frac {
|
||||
self.scroll_frac = frac;
|
||||
cx.notify();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn grid_line(
|
||||
@@ -5357,6 +5462,23 @@ fn smooth_scroll_step(offset: usize, frac: f32, delta: f32, max: usize) -> (i32,
|
||||
(new_offset as i32 - offset as i32, pos - new_offset)
|
||||
}
|
||||
|
||||
/// How much of `remaining` to consume this frame, and whether this is the last
|
||||
/// one. Decay is scaled by the real elapsed time so a dropped frame covers the
|
||||
/// ground it missed instead of stretching the animation out.
|
||||
fn scroll_anim_step(remaining: f32, dt: std::time::Duration) -> (f32, bool) {
|
||||
if remaining.abs() <= SCROLL_ANIM_MIN {
|
||||
return (remaining, true);
|
||||
}
|
||||
let frames = (dt.as_secs_f32() / SCROLL_ANIM_FRAME.as_secs_f32()).clamp(0.1, 8.);
|
||||
let consumed = 1. - (1. - SCROLL_ANIM_SMOOTH).powf(frames);
|
||||
let step = remaining * consumed;
|
||||
if (remaining - step).abs() <= SCROLL_ANIM_MIN {
|
||||
(remaining, true)
|
||||
} else {
|
||||
(step, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn drag_scroll_step(overshoot: f32) -> i32 {
|
||||
let lines = overshoot.abs().ceil().clamp(1., 8.) as i32;
|
||||
if overshoot < 0. { -lines } else { lines }
|
||||
@@ -5369,6 +5491,7 @@ mod tests {
|
||||
compose_notification_title, cwd_is_on_host, display_width, is_typeahead_interrupt,
|
||||
loopback_plan, observe_typeahead_for_owner,
|
||||
};
|
||||
use super::{SCROLL_ANIM_FRAME, scroll_anim_step};
|
||||
use super::{
|
||||
drag_scroll_step, encode_mouse, escape_candidate, expand_file_command_template,
|
||||
fallback_chain, fig_icon_emoji, fig_icon_glyph, focus_report_bytes, input_overflow_shift,
|
||||
@@ -6100,6 +6223,58 @@ mod tests {
|
||||
assert_eq!(smooth_scroll_step(0, 0.0, 2.5, 0), (0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_anim_step_converges_and_lands() {
|
||||
let frame = SCROLL_ANIM_FRAME;
|
||||
// A notch is spread over frames instead of being applied whole.
|
||||
let (step, last) = scroll_anim_step(3.0, frame);
|
||||
assert!(!last);
|
||||
assert!(
|
||||
step > 0. && step < 3.0,
|
||||
"took {step} of 3 lines in one frame"
|
||||
);
|
||||
|
||||
// And it converges: no notch is left hanging.
|
||||
let mut remaining = 3.0_f32;
|
||||
let mut frames = 0u32;
|
||||
loop {
|
||||
let (step, last) = scroll_anim_step(remaining, frame);
|
||||
remaining -= step;
|
||||
frames += 1;
|
||||
if last {
|
||||
break;
|
||||
}
|
||||
assert!(frames < 200, "still {remaining} lines short after {frames}");
|
||||
}
|
||||
assert!(remaining.abs() < 1e-4, "landed {remaining} lines off");
|
||||
// Slow enough to read as motion, fast enough not to feel like lag.
|
||||
let ms = frames * SCROLL_ANIM_FRAME.as_millis() as u32;
|
||||
assert!((60..=200).contains(&ms), "a 3-line notch took {ms}ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_anim_step_covers_dropped_frames() {
|
||||
// A late tick has to make up the ground it missed, or a busy pane would
|
||||
// scroll slower than an idle one.
|
||||
let (one, _) = scroll_anim_step(10.0, SCROLL_ANIM_FRAME);
|
||||
let (four, _) = scroll_anim_step(10.0, SCROLL_ANIM_FRAME * 4);
|
||||
assert!(four > one * 2., "{four} should far outpace {one}");
|
||||
// But the catch-up is capped, so a tick after a long stall never
|
||||
// overshoots what was actually asked for.
|
||||
let (stalled, _) = scroll_anim_step(10.0, std::time::Duration::from_secs(5));
|
||||
assert!(
|
||||
stalled > four && stalled <= 10.0,
|
||||
"stalled tick took {stalled}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_anim_step_lands_on_a_negligible_remainder() {
|
||||
let (step, last) = scroll_anim_step(-0.005, SCROLL_ANIM_FRAME);
|
||||
assert!(last);
|
||||
assert_eq!(step, -0.005);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_scroll_step_scales_with_overshoot_and_caps() {
|
||||
assert_eq!(drag_scroll_step(0.2), 1);
|
||||
@@ -6478,10 +6653,14 @@ pub(crate) fn quiet_test_ssh_pane(
|
||||
mod gpui_tests {
|
||||
use super::*;
|
||||
use crate::daemon::protocol::{ClientMsg, DaemonMsg};
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{TestAppContext, point};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
fn harness(cx: &mut TestAppContext) -> (gpui::WindowHandle<TerminalView>, UnixStream) {
|
||||
// Building a view reads the config. Whether that hit the real user
|
||||
// directory used to come down to which test happened to pin the
|
||||
// scratch dir first.
|
||||
crate::core::config::pin_test_config_dir();
|
||||
cx.executor().allow_parking();
|
||||
let (client_side, daemon_side) = UnixStream::pair().unwrap();
|
||||
cx.update(|cx| {
|
||||
@@ -7943,6 +8122,90 @@ mod gpui_tests {
|
||||
view.terminal.term.lock().grid().display_offset()
|
||||
}
|
||||
|
||||
fn wheel(delta: gpui::ScrollDelta) -> ScrollWheelEvent {
|
||||
ScrollWheelEvent {
|
||||
delta,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole point of the animation: a notch must not land in one go.
|
||||
#[gpui::test]
|
||||
fn a_wheel_notch_is_spread_over_frames(cx: &mut TestAppContext) {
|
||||
let (window, _daemon) = harness(cx);
|
||||
window
|
||||
.update(cx, |view, w, cx| {
|
||||
scroll_into_history(view, 10);
|
||||
view.on_scroll(&wheel(gpui::ScrollDelta::Lines(point(0., -3.))), w, cx);
|
||||
assert_eq!(
|
||||
display_offset(view),
|
||||
10,
|
||||
"the notch was applied whole, before a single frame ran"
|
||||
);
|
||||
assert_eq!(view.scroll_frac, 0., "and not even a sliver of it");
|
||||
assert!(view.scroll_anim.is_some(), "nothing was left to animate");
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// A trackpad is already a continuous stream — animating it would only put
|
||||
/// lag between the fingers and the grid.
|
||||
#[gpui::test]
|
||||
fn a_trackpad_still_scrolls_the_instant_it_is_touched(cx: &mut TestAppContext) {
|
||||
let (window, _daemon) = harness(cx);
|
||||
window
|
||||
.update(cx, |view, w, cx| {
|
||||
scroll_into_history(view, 10);
|
||||
let lines = view.line_height.as_f32() * -3.;
|
||||
view.on_scroll(
|
||||
&wheel(gpui::ScrollDelta::Pixels(point(px(0.), px(lines)))),
|
||||
w,
|
||||
cx,
|
||||
);
|
||||
assert_eq!(display_offset(view), 7, "the pixels were held back");
|
||||
assert!(
|
||||
view.scroll_anim.is_none(),
|
||||
"a trackpad started an animation"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn turning_smooth_scrolling_off_restores_the_direct_path(cx: &mut TestAppContext) {
|
||||
let (window, _daemon) = harness(cx);
|
||||
cx.update(|cx| cx.global_mut::<Config>().smooth_scroll = false);
|
||||
window
|
||||
.update(cx, |view, w, cx| {
|
||||
scroll_into_history(view, 10);
|
||||
view.on_scroll(&wheel(gpui::ScrollDelta::Lines(point(0., -3.))), w, cx);
|
||||
assert_eq!(display_offset(view), 7);
|
||||
assert!(view.scroll_anim.is_none());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// An animation that kept walking after the view was moved out from under
|
||||
/// it would drag the user back off the prompt they just jumped to.
|
||||
#[gpui::test]
|
||||
fn moving_the_viewport_cancels_an_animation_in_flight(cx: &mut TestAppContext) {
|
||||
let (window, _daemon) = harness(cx);
|
||||
window
|
||||
.update(cx, |view, w, cx| {
|
||||
scroll_into_history(view, 10);
|
||||
view.on_scroll(&wheel(gpui::ScrollDelta::Lines(point(0., -3.))), w, cx);
|
||||
assert!(view.scroll_anim.is_some());
|
||||
|
||||
view.jump_to_prompt();
|
||||
assert!(
|
||||
view.scroll_anim.is_none(),
|
||||
"the animation outlived the jump"
|
||||
);
|
||||
assert_eq!(display_offset(view), 0);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn history_recall_snaps_the_viewport_back_to_the_prompt(cx: &mut TestAppContext) {
|
||||
let (window, _daemon) = harness(cx);
|
||||
|
||||
@@ -2082,6 +2082,10 @@ impl Tty7App {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_smooth_scroll(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.smooth_scroll = on);
|
||||
}
|
||||
|
||||
pub(crate) fn set_clipboard_trim(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.clipboard_trim_trailing_spaces = on);
|
||||
}
|
||||
|
||||
@@ -294,6 +294,11 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsScrollbackDesc => "Lines of history kept per pane. Applies to new panes.",
|
||||
L10nKey::SettingsScrollSpeed => "Scroll speed",
|
||||
L10nKey::SettingsScrollSpeedDesc => "Multiplier applied to mouse-wheel scrolling.",
|
||||
L10nKey::SettingsSmoothScroll => "Smooth scrolling",
|
||||
L10nKey::SettingsSmoothScrollDesc => {
|
||||
"Ease each wheel notch into place instead of jumping the whole way at once. \
|
||||
Trackpads scroll continuously already and are unaffected."
|
||||
}
|
||||
L10nKey::SettingsMouse => "Mouse",
|
||||
L10nKey::SettingsFocusFollowsMouse => "Focus follows mouse",
|
||||
L10nKey::SettingsFocusFollowsMouseDesc => "Hovering a pane focuses it without a click.",
|
||||
|
||||
@@ -292,6 +292,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
}
|
||||
L10nKey::SettingsScrollSpeed => "スクロール速度",
|
||||
L10nKey::SettingsScrollSpeedDesc => "マウスホイールのスクロールに適用する倍率",
|
||||
L10nKey::SettingsSmoothScroll => "スムーズスクロール",
|
||||
L10nKey::SettingsSmoothScrollDesc => {
|
||||
"ホイール1ノッチ分を一気に飛ばさず、数フレームかけて動かす。\
|
||||
トラックパッドは元から連続的なので影響しない"
|
||||
}
|
||||
L10nKey::SettingsMouse => "マウス",
|
||||
L10nKey::SettingsFocusFollowsMouse => "フォーカスがマウスに追従する",
|
||||
L10nKey::SettingsFocusFollowsMouseDesc => {
|
||||
|
||||
@@ -283,6 +283,8 @@ pub enum L10nKey {
|
||||
SettingsScrollbackDesc,
|
||||
SettingsScrollSpeed,
|
||||
SettingsScrollSpeedDesc,
|
||||
SettingsSmoothScroll,
|
||||
SettingsSmoothScrollDesc,
|
||||
SettingsMouse,
|
||||
SettingsFocusFollowsMouse,
|
||||
SettingsFocusFollowsMouseDesc,
|
||||
|
||||
@@ -256,6 +256,10 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsScrollbackDesc => "每个窗格保留的历史行数。仅适用于新窗格。",
|
||||
L10nKey::SettingsScrollSpeed => "滚动速度",
|
||||
L10nKey::SettingsScrollSpeedDesc => "应用于鼠标滚轮滚动的倍率。",
|
||||
L10nKey::SettingsSmoothScroll => "平滑滚动",
|
||||
L10nKey::SettingsSmoothScrollDesc => {
|
||||
"滚轮每一格分几帧滚到位,而不是一次跳完。触控板本来就是连续滚动,不受影响。"
|
||||
}
|
||||
L10nKey::SettingsMouse => "鼠标",
|
||||
L10nKey::SettingsFocusFollowsMouse => "焦点跟随鼠标",
|
||||
L10nKey::SettingsFocusFollowsMouseDesc => "悬停窗格即聚焦,无需点击。",
|
||||
|
||||
@@ -3444,6 +3444,7 @@ impl Tty7App {
|
||||
let mouse_hide = cfg.mouse_hide_while_typing;
|
||||
let focus_follows = cfg.focus_follows_mouse;
|
||||
let scroll_mult = cfg.mouse_scroll_multiplier;
|
||||
let smooth_scroll = cfg.smooth_scroll;
|
||||
let mouse_reporting = cfg.mouse_reporting;
|
||||
let bell = cfg.bell;
|
||||
let scrollback_idx = match cfg.scrollback_limit {
|
||||
@@ -3544,6 +3545,10 @@ impl Tty7App {
|
||||
.child(format!("{scroll_mult:.2}×")),
|
||||
)
|
||||
.into_any_element();
|
||||
let smooth_scroll_switch = crate::ui::theme::switch("term-smooth-scroll", cx)
|
||||
.checked(smooth_scroll)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_smooth_scroll(*on, cx)))
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.child(self.render_shell_group(cx))
|
||||
@@ -3561,6 +3566,12 @@ impl Tty7App {
|
||||
scroll_control,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
t(L10nKey::SettingsSmoothScroll),
|
||||
t(L10nKey::SettingsSmoothScrollDesc),
|
||||
smooth_scroll_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.section_rule(cx))
|
||||
.child(self.section_header(t(L10nKey::SettingsMouse), cx))
|
||||
.child(self.settings_row(
|
||||
|
||||
Reference in New Issue
Block a user