Scroll by less than a full cell #3206
Replies: 7 comments 10 replies
|
also would really like to see this feature 😄 |
|
This would make for a really smooth experience, both on laptops with touchpads (or discrete touchpad) but also for devices that have high-resolution scrolling capabilities. These aren't too common, but some open-source trackballs (and keyboards with trackballs) use high-resolution sensors and have started adding this functionality. Whether or not something will handle the high-resolution scrolling events is hit-or-miss—Chromium (etc.) and Nautilus do, but Firefox and GNOME terminal don't. FTR, GNOME terminal does scroll smoothly with touchpads, just not with high-resolution scroll wheels (or devices presenting as such). I don't think any of the high-performance GPU-rendered terminals do this at all... I even seem to remember one of them flat out refusing. In case it's of interest, GNOME terminal lets you scroll smoothly (again, on a touchpad) through the shell history, but I think that's the extent of it. It doesn't affect any applications; e.g., in (n)vim, scrolling is discretized to number of lines, since (AIUI) the application either only receives or only understands regular scroll detents and can't scroll in fractions of lines. |
This comment was marked as off-topic.
This comment was marked as off-topic.
|
I made a hacky version of this using shaders and a ghostty patch Screen.Recording.2025-08-25.at.8.40.35.PM.movdiff --git a/src/Surface.zig b/src/Surface.zig
index 96aaf84d8..85e8abcaa 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -3252,13 +3252,12 @@ pub fn scrollCallback(
break :y .{};
}
- // We scroll by the number of rows in the offset and save the remainder
- const amount = poff / cell_size;
+ // We scroll by the number of rows in the offset, rounded towards zero and save the remainder
+ const amount: f64 = @trunc(poff / cell_size);
assert(@abs(amount) >= 1);
self.mouse.pending_scroll_y = poff - (amount * cell_size);
- // Round towards zero.
- const delta: isize = @intFromFloat(@trunc(amount));
+ const delta: isize = @intFromFloat(amount);
assert(@abs(delta) >= 1);
break :y .{ .delta = delta };
@@ -3278,10 +3277,10 @@ pub fn scrollCallback(
break :x .{};
}
- const amount = poff / cell_size;
+ const amount: f64 = @trunc(poff / cell_size);
assert(@abs(amount) >= 1);
self.mouse.pending_scroll_x = poff - (amount * cell_size);
- const delta: isize = @intFromFloat(@trunc(amount));
+ const delta: isize = @intFromFloat(amount);
assert(@abs(delta) >= 1);
break :x .{ .delta = delta };
};
diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig
index 39eec7b43..ce144b1fa 100644
--- a/src/renderer/generic.zig
+++ b/src/renderer/generic.zig
@@ -746,6 +746,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.current_cursor_color = @splat(0),
.previous_cursor_color = @splat(0),
.cursor_change_time = 0,
+ .pending_scroll = @splat(0),
},
.bg_image_buffer = undefined,
@@ -2287,6 +2288,22 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
0,
};
+ const surface: *Surface = @fieldParentPtr("renderer", self);
+ var pending_y = surface.mouse.pending_scroll_y;
+ if (pending_y != 0) {
+ if (surface.io.terminal.flags.mouse_event != .none) {
+ pending_y = 0;
+ } else {
+ const top_left = surface.io.terminal.screens.active.pages.getTopLeft(.viewport);
+ if (pending_y > 0 and top_left.up(1) == null) pending_y = 0;
+ if (pending_y < 0 and surface.io.terminal.screens.active.pages.pinIsActive(top_left)) pending_y = 0;
+ }
+ }
+ self.custom_shader_uniforms.pending_scroll = .{
+ 0,
+ @floatCast(pending_y),
+ };
+
// Update custom cursor uniforms, if we have a cursor.
if (self.cells.getCursorGlyph()) |cursor| {
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl
index 6d9cf0f68..689edad0c 100644
--- a/src/renderer/shaders/shadertoy_prefix.glsl
+++ b/src/renderer/shaders/shadertoy_prefix.glsl
@@ -16,6 +16,7 @@ layout(binding = 1, std140) uniform Globals {
uniform vec4 iCurrentCursorColor;
uniform vec4 iPreviousCursorColor;
uniform float iTimeCursorChange;
+ uniform vec2 iPendingScroll;
};
layout(binding = 0) uniform sampler2D iChannel0;
diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig
index 0d096c0fc..d6cd8f4f6 100644
--- a/src/renderer/shadertoy.zig
+++ b/src/renderer/shadertoy.zig
@@ -25,6 +25,7 @@ pub const Uniforms = extern struct {
current_cursor_color: [4]f32 align(16),
previous_cursor_color: [4]f32 align(16),
cursor_change_time: f32 align(4),
+ pending_scroll: [2]f32 align(8),
};
/// The target to load shaders for.
diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig
index 9e14e2a75..90880feb5 100644
--- a/src/terminal/PageList.zig
+++ b/src/terminal/PageList.zig
@@ -3223,7 +3223,7 @@ pub fn pinIsValid(self: *const PageList, p: Pin) bool {
/// Returns the viewport for the given pin, preferring to pin to
/// "active" if the pin is within the active area.
-fn pinIsActive(self: *const PageList, p: Pin) bool {
+pub fn pinIsActive(self: *const PageList, p: Pin) bool {
// If the pin is in the active page, then we can quickly determine
// if we're beyond the end.
const active = self.getTopLeft(.active);// smoothscroll.glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
fragCoord -= iPendingScroll;
fragColor = texture(iChannel0, fragCoord / iResolution.xy);
}What it would need:
|
This comment was marked as spam.
This comment was marked as spam.
|
Are there any updates on this? |
|
The UX benefit of smooth scrolling is that it is easier to see how far you're scrolling, without a loss of context. I'd like to add that VT320 terminals had it, and it looked amazing back then too: https://www.youtube.com/watch?v=tSJfzrSA0ec I understand why this is probably very low priority, but myself and many other would love to see this feature! |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Few terminals do this but I like it when they do. When scrolling on a trackpad, it would be nice to be able to stop partway through a cell rather than having the minimum scroll amount being a full cell (except when scrolling is handled by an application, like vim).
my proposal: (xfce4-terminal and some others do this)
EXAMPLE.SMOOTH.mp4
current scrolling:
EXAMPLE.DISCRETE.mp4
I looked at this a little bit and it doesn't seem like it should be too hard to implement, there's already a property for pending_scroll_y - it changing needs to trigger a rerender, have the value passed through to rendering, ask the shader to vertically offset the result by that amount, and also maybe render an extra line above/below and clip so that there's never blank space that turns into a line when you get past a certain scroll point.
All reactions