summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 06:41:28 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitff252345bc3e1edc1aee93ca826f294bf9a7874f (patch)
tree61c8d8ff2a13e7ceca77d439eeff8edc41fa644c /src/gui/gui.zig
parent09ec515c5e94de741104038cd61f18b2b997e78c (diff)
downloadpardes-ff252345bc3e1edc1aee93ca826f294bf9a7874f.tar.gz
pardes-ff252345bc3e1edc1aee93ca826f294bf9a7874f.zip
G6: a wheel's notch scrolls the core at once and the picture glides after it
A mouse wheel's notch (SDL's whole-number step, notchOf) scrolls the core by its lines at once, so keys and clicks act where the text is, and the picture follows on the Motion flavour's spring at half again its pace, critically damped whatever the flavour (text never passes its mark), carried from its current speed (animation.Spring.carry), so notches in a row flow on. How far the rows moved is measured on the next frame against the last (Glide.measure: a wrapped line's rows, a terminal's output, with a couple of rows' slack for the cursor's line and its number); the rows that left are kept, nearest first, and drawn in the gap the lag opens; the lag is never more than the body's height. A touchpad's fractions move the picture under the finger as before; `Motion off` lands a notch at once; a PDF page scrolls as before. The glide runs on the core's continuous path: Pardes.shell_continuous keeps frames coming every loop while it moves and carries a virtual clock with it; idle, nothing. The test feed takes wheel events (ESC]777;mouse;wheel;<amount>;<x>;<y>BEL). Tests: notch vs fraction; a carried spring keeps its speed and never passes its mark; a notch over two-row lines lags two rows, keeps the rows that left, settles frame by frame and leaves the continuous path, and lands at once with Motion off. Gates as G5. Feel review: .scratch/render/scroll/glide-{smooth,crisp,bouncy,off}{,-quarter}.mp4 and strips (a notch down, three, then two up).
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig381
1 files changed, 375 insertions, 6 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 092bb3bf..449cae15 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1866,6 +1866,10 @@ const Gui = struct {
scroll_lag: f32 = 0, // picture position - core position, in rows
scroll_edge: []pardes.Cell = &.{}, // the row that just left the pane
scroll_edge_len: u16 = 0,
+ /// G6: a wheel's notches, whole lines, scroll the core at once and the
+ /// picture follows on a spring (Glide), never the other way round.
+ scroll_notch: i32 = 0,
+ glide: Glide = .{},
/// The cursor's blink (blinkNow): when the frame drawn last arrived,
/// a redraw owed for an edge, and this frame's cursor mode and alpha.
@@ -2574,6 +2578,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
defer if (g.font_bytes.len != 0) gpa.free(g.font_bytes); // set by Font, if it ran
defer c.ui_font_free(g.font);
defer gpa.free(g.scroll_edge); // grown on demand by stepScroll
+ defer g.glide.deinit(gpa);
resetGlyphAtlas(&g);
if (attach) |requested| return attachRequested(gpa, &g, requested);
@@ -3104,6 +3109,7 @@ const StdinFeed = struct {
var seq_start: usize = 0;
while (seq_start < len) {
// ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>BEL
+ // ESC]777;mouse;wheel;<amount>;<x>;<y>BEL
// A mouse event at window pixels, built as SDL would and run
// through dispatch, so a driver exercises the real pointer path
// (pixel to cell, tag and body hits). SGR mouse carries only cells.
@@ -3118,11 +3124,21 @@ const StdinFeed = struct {
if (g) |gp| {
var parts = std.mem.splitScalar(u8, body[0..end], ';');
const kind = parts.next() orelse "";
- const button = std.fmt.parseInt(u8, parts.next() orelse "", 10) catch 0;
+ const second = parts.next() orelse "";
+ const button = std.fmt.parseInt(u8, second, 10) catch 0;
const x = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
const y = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
var sev = std.mem.zeroes(c.SDL_Event);
- if (std.mem.eql(u8, kind, "motion")) {
+ if (std.mem.eql(u8, kind, "wheel")) {
+ // ESC]777;mouse;wheel;<y, SDL's sign: up positive>;<x>;<y>BEL:
+ // a whole number is a mouse's notches, else a touchpad's.
+ const amount = std.fmt.parseFloat(f32, second) catch 0;
+ sev.type = c.SDL_EVENT_MOUSE_WHEEL;
+ sev.wheel.y = amount;
+ sev.wheel.integer_y = @intFromFloat(@trunc(amount));
+ sev.wheel.mouse_x = x;
+ sev.wheel.mouse_y = y;
+ } else if (std.mem.eql(u8, kind, "motion")) {
sev.type = c.SDL_EVENT_MOUSE_MOTION;
sev.motion.x = x;
sev.motion.y = y;
@@ -3533,7 +3549,12 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
g.scroll_pane = id;
g.scroll_col = mc.col;
g.scroll_row = mc.row;
- g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
+ // A mouse wheel's notch is a whole line and
+ // glides (G6); a touchpad's fraction moves the
+ // picture by itself, under the finger.
+ if (notchOf(w.y, w.integer_y)) |lines| {
+ g.scroll_notch +|= lines;
+ } else g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
}
}
} else {
@@ -4213,7 +4234,7 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
} else for (s.ptys) |slot| {
if (slot) |pt| if (pt.kill_at != 0) break true;
} else false;
- const polls = g.scroll_lag != 0 or g.scroll_delta != 0 or g.gamepad != null or s.test_mode or core.needs_frame or retiring or
+ const polls = g.scroll_lag != 0 or g.scroll_delta != 0 or g.scroll_notch != 0 or g.glide.moving() or g.gamepad != null or s.test_mode or core.needs_frame or retiring or
g.post.animating(core.settings.shader_animation);
const minimized = !s.test_mode and c.SDL_GetWindowFlags(g.window) & (c.SDL_WINDOW_MINIMIZED | c.SDL_WINDOW_OCCLUDED) != 0;
// A latency trace is fed on stdin, whose poll below paces the loop:
@@ -4797,14 +4818,356 @@ test "fractional scroll reversals preserve signed distance" {
try std.testing.expect(reverse.lag >= 0 and reverse.lag < 1);
}
+/// A mouse wheel's step as whole lines (down positive), or null for a
+/// touchpad's fraction, which follows the finger as it is.
+fn notchOf(raw_y: f32, integer_y: i32) ?i32 {
+ if (!std.math.isFinite(raw_y) or integer_y == 0 or raw_y != @round(raw_y)) return null;
+ return @intFromFloat(std.math.clamp(-raw_y, -64, 64));
+}
+
+test "a wheel's notch is whole lines, a touchpad's fraction is not" {
+ try std.testing.expectEqual(@as(?i32, 1), notchOf(-1, -1));
+ try std.testing.expectEqual(@as(?i32, -3), notchOf(3, 3));
+ try std.testing.expectEqual(@as(?i32, null), notchOf(-0.25, 0));
+ try std.testing.expectEqual(@as(?i32, null), notchOf(-1.5, -1));
+ try std.testing.expectEqual(@as(?i32, null), notchOf(std.math.nan(f32), 1));
+}
+
+/// G6 (docs/render-pipeline.md ยง9.1): a wheel's lines scroll the core at
+/// once, so every key and click acts on where the text is, and the picture
+/// glides there on the Motion flavour's spring (critically damped: text
+/// never passes its mark). How far the rows moved is measured on the next
+/// frame against the last, a line wrapped over several rows or a terminal's
+/// output included, and the rows that left are kept to draw in the gap.
+const Glide = struct {
+ spring: pardes.animation.Spring = .{},
+ /// The picture's offset from the core's rows now (rows; negative, the
+ /// picture still shows what is above).
+ offset: f32 = 0,
+ now_ns: u64 = 0,
+ pane: usize = 0,
+ rect: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 },
+ body_y: u16 = 0,
+ body_h: u16 = 0,
+ /// A scroll made and not yet measured: its direction, and the body's
+ /// rows before it.
+ pending: bool = false,
+ dir: i8 = 0,
+ before: []pardes.Cell = &.{},
+ /// The rows past the body's top (`above`) and bottom (`below`), nearest
+ /// first, `bw` cells each: what the gap shows while the picture lags.
+ above: []pardes.Cell = &.{},
+ below: []pardes.Cell = &.{},
+ nabove: u16 = 0,
+ nbelow: u16 = 0,
+ /// The Motion flavour the last loop step read.
+ motion: pardes.animation.Motion = .{},
+
+ fn moving(glide: *const Glide) bool {
+ return !glide.spring.settled or glide.pending;
+ }
+
+ fn stop(glide: *Glide) void {
+ glide.spring = .{};
+ glide.offset = 0;
+ glide.pending = false;
+ glide.nabove = 0;
+ glide.nbelow = 0;
+ }
+
+ fn deinit(glide: *Glide, gpa: std.mem.Allocator) void {
+ gpa.free(glide.before);
+ gpa.free(glide.above);
+ gpa.free(glide.below);
+ }
+
+ fn bw(glide: *const Glide) u16 {
+ return glide.rect.w -| config.GUTTER;
+ }
+
+ /// The core's rows moved by `rows` (down positive) since the last
+ /// frame: the picture now lags by as many, carrying its speed.
+ fn kick(glide: *Glide, rows: f32, now_ns: u64, motion: pardes.animation.Motion) void {
+ // Follows input: the flavour's pace, half again, never overshooting.
+ var pace = motion;
+ pace.omega = motion.omega * 1.5;
+ glide.spring.carry(-rows, now_ns, pace);
+ // Never more than a body's rows behind: a flick past that jumps.
+ const limit: f32 = @floatFromInt(glide.body_h);
+ const now = glide.spring.value(now_ns);
+ if (@abs(now) > limit) glide.spring.carry(std.math.clamp(now, -limit, limit) - now, now_ns, pace);
+ glide.offset = glide.spring.value(now_ns);
+ }
+
+ /// Finds how far the pane's body rows moved, `dir` rows at a time, by
+ /// the rows of `surface` against `before`; keeps the rows that left.
+ fn measure(glide: *Glide, gpa: std.mem.Allocator, surface: *const pardes.Surface) void {
+ glide.pending = false;
+ const w = glide.bw();
+ const h = glide.body_h;
+ const x0 = glide.rect.x + config.GUTTER;
+ if (w == 0 or h == 0 or x0 + w > surface.cols or glide.body_y + h > surface.rows or glide.before.len < @as(usize, w) * h) return glide.stop();
+ const now = struct {
+ fn row(s: *const pardes.Surface, x: u16, y: u16, n: u16) []const pardes.Cell {
+ return s.cells[@as(usize, y) * s.cols + x ..][0..n];
+ }
+ };
+ // The least shift under which the rows agree, but for a few: the
+ // cursor's line and its number's weight move with the scroll.
+ const shift: u16 = found: for (1..h) |k| {
+ const kk: u16 = @intCast(k);
+ const allowed = @max(2, (h - kk) / 8);
+ var misses: usize = 0;
+ for (0..h - kk) |r| {
+ const rr: u16 = @intCast(r);
+ const old_row = glide.before[@as(usize, if (glide.dir > 0) rr + kk else rr) * w ..][0..w];
+ const new_row = now.row(surface, x0, glide.body_y + if (glide.dir > 0) rr else rr + kk, w);
+ if (!sameRow(old_row, new_row)) {
+ misses += 1;
+ if (misses > allowed) continue :found;
+ }
+ }
+ // Too few rows left to tell: no glide.
+ if (h - kk < 3) break :found 0;
+ break :found kk;
+ } else 0;
+ if (shift == 0) return glide.stop();
+ // The rows that left: past the top going down, the bottom going up.
+ if (!glide.ensure(gpa)) return glide.stop();
+ const keep = if (glide.dir > 0) &glide.above else &glide.below;
+ const count = if (glide.dir > 0) &glide.nabove else &glide.nbelow;
+ const kept = @min(count.*, h - shift);
+ std.mem.copyBackwards(pardes.Cell, keep.*[@as(usize, shift) * w ..][0 .. @as(usize, kept) * w], keep.*[0 .. @as(usize, kept) * w]);
+ for (0..shift) |i| {
+ const src_row: usize = if (glide.dir > 0) shift - 1 - i else h - shift + i;
+ @memcpy(keep.*[i * w ..][0..w], glide.before[src_row * w ..][0..w]);
+ }
+ count.* = kept + shift;
+ // What came into view is no longer past the other edge.
+ const other = if (glide.dir > 0) &glide.below else &glide.above;
+ const other_n = if (glide.dir > 0) &glide.nbelow else &glide.nabove;
+ const dropped = @min(other_n.*, shift);
+ std.mem.copyForwards(pardes.Cell, other.*[0 .. @as(usize, other_n.* - dropped) * w], other.*[@as(usize, dropped) * w ..][0 .. @as(usize, other_n.* - dropped) * w]);
+ other_n.* -= dropped;
+ glide.kick(@as(f32, @floatFromInt(shift)) * @as(f32, @floatFromInt(glide.dir)), glide.now_ns, glide.motion);
+ }
+
+ fn ensure(glide: *Glide, gpa: std.mem.Allocator) bool {
+ const n = @as(usize, glide.bw()) * glide.body_h;
+ inline for (.{ &glide.above, &glide.below }) |buffer| if (buffer.len < n) {
+ const grown = gpa.realloc(buffer.*, n) catch return false;
+ buffer.* = grown;
+ };
+ return true;
+ }
+};
+
+fn sameRow(a: []const pardes.Cell, b: []const pardes.Cell) bool {
+ for (a, b) |x, y| {
+ if (x.default != y.default) return false;
+ if (x.default) continue;
+ if (!std.mem.eql(u8, x.grapheme(), y.grapheme()) or !std.meta.eql(x.style, y.style)) return false;
+ }
+ return true;
+}
+
+/// Feeds a wheel's notches to the core, snapshotting the body first so the
+/// next frame can measure the move (Glide.measure); advances the glide and
+/// asks for frames while it moves.
+fn stepGlide(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
+ const glide = &g.glide;
+ // The glide runs on the core's continuous path: frames every loop, and
+ // a virtual clock carried on.
+ defer core.shell_continuous = glide.moving();
+ glide.now_ns = core.now_ns;
+ glide.motion = pardes.animation.Motion.of(core.settings.motion);
+ if (!glide.spring.settled) {
+ _ = glide.spring.step(core.now_ns);
+ core.needs_frame = true;
+ if (glide.spring.settled) {
+ glide.offset = 0;
+ glide.nabove = 0;
+ glide.nbelow = 0;
+ }
+ }
+ const notch = g.scroll_notch;
+ if (notch == 0) return;
+ g.scroll_notch = 0;
+ const id = g.scroll_pane orelse return;
+ const pane = core.panes[id] orelse return resetScroll(g);
+ const r = core.rects[id];
+ if (r.h <= pane.tag_rows or r.w <= config.GUTTER) return resetScroll(g);
+ const body_y = core.bodyTop(pane, r);
+ const body_h = r.h -| pane.tag_rows;
+ // A new pane or place: what was kept belongs to another picture.
+ if (glide.pane != id or !std.meta.eql(glide.rect, r) or glide.body_y != body_y or glide.body_h != body_h or glide.pending) glide.stop();
+ glide.pane = id;
+ glide.rect = r;
+ glide.body_y = body_y;
+ glide.body_h = body_h;
+ // The body as the last frame drew it, which the move is measured from.
+ const s = &core.surface;
+ const w = r.w - config.GUTTER;
+ const x0 = r.x + config.GUTTER;
+ const animate = !glide.motion.instant and x0 + w <= s.cols and body_y + body_h <= s.rows and s.cells.len != 0;
+ if (animate) {
+ const n = @as(usize, w) * body_h;
+ if (glide.before.len < n) glide.before = gpa.realloc(glide.before, n) catch &.{};
+ if (glide.before.len >= n) {
+ for (0..body_h) |row| @memcpy(glide.before[row * w ..][0..w], s.cells[(@as(usize, body_y) + row) * s.cols + x0 ..][0..w]);
+ }
+ }
+ var left = notch;
+ var moved = false;
+ while (left != 0) {
+ const down = left > 0;
+ left += if (down) -1 else 1;
+ const was = pane.scroll();
+ core.update(.{ .mouse = .{ .button = if (down) .wheel_down else .wheel_up, .kind = .press, .col = g.scroll_col, .row = g.scroll_row } });
+ if (pane.scroll() == was) break;
+ moved = true;
+ }
+ if (!moved) return;
+ core.needs_frame = true;
+ if (animate and glide.before.len >= @as(usize, w) * body_h) {
+ glide.pending = true;
+ glide.dir = @intCast(std.math.sign(notch));
+ }
+}
+
+/// The glide's picture: the pane's body rows moved by the offset, and in
+/// the gap it opens the rows that left, as `emitScrollRows` draws a
+/// touchpad's fraction.
+fn emitGlideRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: Ground) u32 {
+ const glide = &g.glide;
+ for (surface.panelTracks()) |track|
+ if (track.active() and track.pane == glide.pane) return 0;
+ const r = glide.rect;
+ const bh = glide.body_h;
+ const bw = glide.bw();
+ const x0 = r.x + config.GUTTER;
+ const y0 = glide.body_y;
+ if (bw == 0 or bh == 0 or x0 + bw > surface.cols or y0 + bh > surface.rows) return 0;
+ // The scissor the shifted rows are drawn in is the scroll's.
+ g.scroll_rect = r;
+ g.scroll_body_y = y0;
+ g.scroll_body_h = bh;
+ var shifted = layout;
+ shifted.y_off -= glide.offset * layout.h;
+ const cursor_idx: u32 = if (surface.cursor) |cu| @as(u32, cu.y) * surface.cols + cu.x else std.math.maxInt(u32);
+ const cursor_bar = if (surface.cursor) |cu| cu.bar else false;
+ var n: u32 = 0;
+ var row = y0;
+ while (row < y0 + bh) : (row += 1) {
+ const line = surface.cells[@as(usize, row) * surface.cols ..][0..surface.cols];
+ const row_cursor: ?usize = if (surface.cursor) |cu| if (cu.y == row) cu.x else null else null;
+ var col = x0;
+ while (col < x0 + bw) : (col += 1) {
+ const sidx: u32 = @as(u32, row) * surface.cols + col;
+ emitInstance(g, instances, base + n, col, row, shifted, win_w, win_h, null, cellFontRole(surface.at(col, row)), line, col, row_cursor, false, sidx == cursor_idx and !cursor_bar, page);
+ n += 1;
+ }
+ }
+ // The gap: rows past the top while the picture lags a move down, past
+ // the bottom for one up; as many as it opens.
+ const gap: u16 = @intFromFloat(@min(@ceil(@abs(glide.offset)), @as(f32, @floatFromInt(bh))));
+ const kept = if (glide.offset < 0) glide.nabove else glide.nbelow;
+ const rows = if (glide.offset < 0) glide.above else glide.below;
+ for (0..@min(gap, kept)) |i| {
+ const cells = rows[i * bw ..][0..bw];
+ // Row -1 - i above the body, or bh + i below it, in grid rows.
+ const at_row: i32 = if (glide.offset < 0) @as(i32, y0) - 1 - @as(i32, @intCast(i)) else @as(i32, y0) + bh + @as(i32, @intCast(i));
+ var at = shifted;
+ // emitInstance takes an unsigned row: move the layout instead.
+ at.y_off += @as(f32, @floatFromInt(at_row - @as(i32, y0))) * layout.h;
+ for (0..bw) |c_i| {
+ emitInstance(g, instances, base + n, x0 + @as(u16, @intCast(c_i)), y0, at, win_w, win_h, null, cellFontRole(&cells[c_i]), cells, c_i, null, false, false, page);
+ n += 1;
+ }
+ }
+ return n;
+}
+
+test "a notch scrolls the core at once and the picture glides after it, a wrapped line's rows at a time" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ // Lines of two rows each at this width, numbered so no two rows match.
+ var text: std.ArrayList(u8) = .empty;
+ defer text.deinit(gpa);
+ for (0..60) |i| {
+ try text.print(gpa, "{d:0>4} ", .{i});
+ try text.appendNTimes(gpa, 'a' + @as(u8, @intCast(i % 26)), 100);
+ try text.append(gpa, '\n');
+ }
+ _ = try core.setTestFile(text.items);
+ core.settings.motion = .smooth;
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const ms = std.time.ns_per_ms;
+ var now: u64 = std.time.ns_per_s;
+ core.advance(now);
+ var s = try core.render(arena.allocator());
+ var g: Gui = undefined;
+ g.glide = .{};
+ defer g.glide.deinit(gpa);
+ g.scroll_delta = 0;
+ g.scroll_lag = 0;
+ g.scroll_edge_len = 0;
+ const r = core.rects[core.active];
+ g.scroll_pane = core.active;
+ g.scroll_col = r.x + 10;
+ g.scroll_row = r.y + r.h - 2;
+ const body_y = core.bodyTop(core.panes[core.active].?, r);
+ const row_before = try gpa.dupe(pardes.Cell, s.cells[@as(usize, body_y + 1) * s.cols + r.x + config.GUTTER ..][0 .. r.w - config.GUTTER]);
+ defer gpa.free(row_before);
+ // One notch down: the core is a line on at once.
+ g.scroll_notch = 1;
+ const scrolled = core.panes[core.active].?.scroll();
+ stepGlide(&g, core, gpa);
+ try std.testing.expectEqual(scrolled + 1, core.panes[core.active].?.scroll());
+ try std.testing.expect(g.glide.pending);
+ // On the core's continuous path while it moves: frames every loop.
+ try std.testing.expect(core.shell_continuous and core.continuous());
+ _ = arena.reset(.retain_capacity);
+ s = try core.render(arena.allocator());
+ g.glide.measure(gpa, s);
+ // Its two rows: the picture two rows behind, the rows that left kept,
+ // nearest first.
+ try std.testing.expectApproxEqAbs(@as(f32, -2), g.glide.offset, 1e-4);
+ try std.testing.expectEqual(@as(u16, 2), g.glide.nabove);
+ try std.testing.expect(sameRow(g.glide.above[0 .. r.w - config.GUTTER], row_before));
+ // Frame by frame it comes back, never past, and settles.
+ var frames: usize = 0;
+ while (g.glide.moving()) : (frames += 1) {
+ now += 7 * ms;
+ core.advance(now);
+ stepGlide(&g, core, gpa);
+ try std.testing.expect(g.glide.spring.value(now) <= 1e-4);
+ if (frames > 200) return error.NeverSettled;
+ }
+ try std.testing.expect(frames > 5);
+ try std.testing.expectEqual(@as(f32, 0), g.glide.offset);
+ try std.testing.expect(!core.shell_continuous);
+ // Motion off: a notch lands at once.
+ core.settings.motion = .off;
+ g.scroll_notch = 1;
+ stepGlide(&g, core, gpa);
+ try std.testing.expect(!g.glide.pending and !g.glide.moving());
+}
+
+
fn resetScroll(g: *Gui) void {
g.scroll_pane = null;
g.scroll_delta = 0;
g.scroll_lag = 0;
g.scroll_edge_len = 0;
+ g.scroll_notch = 0;
+ g.glide.stop();
}
fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
+ stepGlide(g, core, gpa);
const id = g.scroll_pane orelse return;
const pane = core.panes[id] orelse {
resetScroll(g);
@@ -4813,7 +5176,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
const queued_delta = g.scroll_delta;
g.scroll_delta = 0;
if (queued_delta == 0) {
- if (g.scroll_lag == 0) resetScroll(g);
+ if (g.scroll_lag == 0 and g.scroll_notch == 0 and !g.glide.moving()) resetScroll(g);
return;
}
const r = core.rects[id];
@@ -4868,6 +5231,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
}
fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32, page: Ground) u32 {
+ if (g.glide.offset != 0) return emitGlideRows(g, instances, base, surface, layout, win_w, win_h, page);
const scroll_pane = g.scroll_pane orelse return 0;
if (g.scroll_lag == 0) return 0;
for (surface.panelTracks()) |track|
@@ -5434,6 +5798,10 @@ fn renderFrame(
defer if (building) build_zone.end();
const layout = fixedCellLayout(g);
const chrome = &surface.chrome;
+ // G6: what the scroll just done moved the pane's rows by, measured on
+ // this frame against the last (the glide follows it from there).
+ if (g.glide.pending) g.glide.measure(gpa, surface);
+ if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns);
cell_decor = CellDecor.of(chrome);
// Plaques need an opaque window: with WindowOpacity under 100 the cells
// are drawn as they are, and the plaques not at all.
@@ -5570,7 +5938,8 @@ fn renderFrame(
}
const decor_len: u32 = @intCast(g.decor.items.len);
if (cell_total != 0 or decor_len != 0) {
- const capacity = std.math.add(u32, cell_total, cells + decor_len) catch return error.GpuCapacity;
+ // The shifted rows of a scroll: its body, and a glide's rows past it.
+ const capacity = std.math.add(u32, cell_total, 2 * cells + decor_len) catch return error.GpuCapacity;
try ensureVbuf(g, capacity);
const vptr: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(g.device, g.vxfer.?, false) orelse {