summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:39:50 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commite56a8ba396566eaab818b6c742850b5842e880bb (patch)
tree346e0ccf2b1a738bee2b7a185ff560e324c85417 /src/gui
parent1d0f8cd158bb47941c0187d32422787c92e8feeb (diff)
parent387d6a39bad76c5eb2f56328f7cf7869a9a10559 (diff)
downloadpardes-e56a8ba396566eaab818b6c742850b5842e880bb.tar.gz
pardes-e56a8ba396566eaab818b6c742850b5842e880bb.zip
Merge: round 26 + codex wheel (the 0.8 candidate)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig183
1 files changed, 134 insertions, 49 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index be961477..045f22d9 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1882,6 +1882,9 @@ const Gui = struct {
/// 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,
+ /// A touchpad's fraction toward the next turn sent to a program that
+ /// owns the wheel (terminal.ownsWheel).
+ program_wheel: f32 = 0,
glide: Glide = .{},
/// G9 and G11: the selection's and the hovered word's glow blocks as
/// last drawn, for their fade-in.
@@ -3578,55 +3581,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
};
g.pointer_mapped = true;
g.pointer_cell = mc;
- if (w.y != 0 and std.math.isFinite(w.y)) {
- if (in.core) |core| {
- const hit: ?usize = for (core.panes, 0..) |slot, i| {
- if (slot == null) continue;
- const r = core.rects[i];
- if (mc.col >= r.x and mc.col < r.x + r.w and mc.row >= r.y and mc.row < r.y + r.h) break i;
- } else null;
- if (g.scroll_pane) |old| if (hit == null or hit.? != old) {
- resetScroll(g);
- };
- if (hit) |id| {
- const pdf_target = if (comptime pardes.pdf_enabled)
- core.native_images and core.panes[id].?.pdfPage() != null
- else
- false;
- if (pdf_target) {
- resetScroll(g);
- in.post(.{ .pdf_scroll = .{
- .pane = @intCast(id),
- .delta_pixels = -w.y * @as(f32, @floatFromInt(g.cell_h)),
- } });
- } else {
- g.scroll_pane = id;
- g.scroll_col = mc.col;
- g.scroll_row = mc.row;
- // 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 {
- g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
- while (g.scroll_delta >= 1) : (g.scroll_delta -= 1)
- in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
- while (g.scroll_delta <= -1) : (g.scroll_delta += 1)
- in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
- }
- }
- if (w.x != 0) in.post(.{ .mouse = .{
- .button = if (w.x > 0) .wheel_right else .wheel_left,
- .kind = .press,
- .col = mc.col,
- .row = mc.row,
- .tag_hit = mc.tag_hit,
- .body_hit = mc.body_hit,
- } });
+ wheelAt(g, in, .{ .x = w.x, .y = w.y, .integer_y = w.integer_y }, mc);
},
c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
const f = sev.tfinger;
@@ -3933,6 +3888,73 @@ test "a click at the window's corner lands in the corner cell with a post chain
/// Where a window point lands: the cell drawn under it. Input is identity:
/// no post pass moves a pixel (the bundled Crt has no barrel), so a click
/// is exactly where it looks.
+/// A wheel turned over grid cell `mc` (SDL's sign: up positive): the
+/// part of dispatch after the pointer is placed, so a test can turn it.
+fn wheelAt(g: *Gui, in: *Input, w: WheelTurn, mc: MouseCell) void {
+ if (w.y != 0 and std.math.isFinite(w.y)) {
+ if (in.core) |core| {
+ const hit: ?usize = for (core.panes, 0..) |slot, i| {
+ if (slot == null) continue;
+ const r = core.rects[i];
+ if (mc.col >= r.x and mc.col < r.x + r.w and mc.row >= r.y and mc.row < r.y + r.h) break i;
+ } else null;
+ if (g.scroll_pane) |old| if (hit == null or hit.? != old) {
+ resetScroll(g);
+ };
+ if (hit) |id| {
+ const pdf_target = if (comptime pardes.pdf_enabled)
+ core.native_images and core.panes[id].?.pdfPage() != null
+ else
+ false;
+ const pane = core.panes[id].?;
+ if (pdf_target) {
+ resetScroll(g);
+ in.post(.{ .pdf_scroll = .{
+ .pane = @intCast(id),
+ .delta_pixels = -w.y * @as(f32, @floatFromInt(g.cell_h)),
+ } });
+ } else if (pane.isTerminal() and pardes.panes.terminal.ownsWheel(pane)) {
+ // The program's (codex, htop, less): every whole
+ // turn goes to it, no glide, for nothing of ours
+ // moves.
+ resetScroll(g);
+ g.program_wheel = accumulateWheelDelta(g.program_wheel, w.y);
+ while (g.program_wheel >= 1) : (g.program_wheel -= 1)
+ in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ while (g.program_wheel <= -1) : (g.program_wheel += 1)
+ in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ } else {
+ g.scroll_pane = id;
+ g.scroll_col = mc.col;
+ g.scroll_row = mc.row;
+ // 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 {
+ g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
+ while (g.scroll_delta >= 1) : (g.scroll_delta -= 1)
+ in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ while (g.scroll_delta <= -1) : (g.scroll_delta += 1)
+ in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .tag_hit = mc.tag_hit, .body_hit = mc.body_hit } });
+ }
+ }
+ if (w.x != 0) in.post(.{ .mouse = .{
+ .button = if (w.x > 0) .wheel_right else .wheel_left,
+ .kind = .press,
+ .col = mc.col,
+ .row = mc.row,
+ .tag_hit = mc.tag_hit,
+ .body_hit = mc.body_hit,
+ } });
+}
+
+const WheelTurn = struct { x: f32 = 0, y: f32 = 0, integer_y: i32 = 0 };
+
fn mouseCellWithGeometry(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell {
const physical = windowPointToPixels(geometry, x, y);
return gridCellAtPixels(g, core, physical.x, physical.y);
@@ -4896,6 +4918,69 @@ test "a wheel's notch is whole lines, a touchpad's fraction is not" {
try std.testing.expectEqual(@as(?i32, null), notchOf(std.math.nan(f32), 1));
}
+test "a wheel over codex in a terminal reaches its pty as the mouse reports it asked for" {
+ if (comptime !pardes.panes.terminal.enabled) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 16, .tty_only = true });
+ defer core.deinit();
+ var frame: std.heap.ArenaAllocator = .init(gpa);
+ defer frame.deinit();
+ _ = try core.render(frame.allocator());
+ var g: Gui = undefined;
+ g.scroll_pane = null;
+ g.scroll_delta = 0;
+ g.scroll_lag = 0;
+ g.scroll_edge_len = 0;
+ g.scroll_notch = 0;
+ g.program_wheel = 0;
+ g.glide = .{};
+ g.cell_h = 20;
+ var in: Input = .{ .core = core };
+ var sent: std.ArrayList(u8) = .empty;
+ defer sent.deinit(gpa);
+ const Sent = struct {
+ fn take(core_: *pardes.Pardes, out: *std.ArrayList(u8)) ![]const u8 {
+ out.clearRetainingCapacity();
+ while (core_.nextEffect()) |effect| {
+ if (effect == .write and effect.write.pane == 0) try out.appendSlice(std.testing.allocator, effect.write.bytes.slice());
+ }
+ return out.items;
+ }
+ };
+ _ = try Sent.take(core, &sent);
+ const pane = core.panes[0].?;
+ const r = core.rects[0];
+ const mc: MouseCell = .{ .col = r.x + config.GUTTER + 5, .row = core.bodyTop(pane, r) + 3 };
+ // What codex 0.159's chat sets on start.
+ core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h\x1b[>7u\x1b[?1004h\x1b[?1049h\x1b[?1007l\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h" } });
+ _ = try Sent.take(core, &sent);
+ // A notch up, then a fast turn of three down: each its report, none
+ // left behind for a glide.
+ wheelAt(&g, &in, .{ .y = 1, .integer_y = 1 }, mc);
+ try std.testing.expectEqualStrings("\x1b[<64;6;4M", try Sent.take(core, &sent));
+ wheelAt(&g, &in, .{ .y = -3, .integer_y = -3 }, mc);
+ try std.testing.expectEqualStrings("\x1b[<65;6;4M" ** 3, try Sent.take(core, &sent));
+ try std.testing.expectEqual(@as(i32, 0), g.scroll_notch);
+ try std.testing.expect(g.scroll_pane == null);
+ // A touchpad's fractions add up to a turn.
+ wheelAt(&g, &in, .{ .y = 0.5 }, mc);
+ try std.testing.expectEqualStrings("", try Sent.take(core, &sent));
+ wheelAt(&g, &in, .{ .y = 0.5 }, mc);
+ try std.testing.expectEqualStrings("\x1b[<64;6;4M", try Sent.take(core, &sent));
+ // Its sign-in screen: mouse off, alternate scroll on, so arrow keys.
+ core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1007h" } });
+ _ = try Sent.take(core, &sent);
+ wheelAt(&g, &in, .{ .y = -1, .integer_y = -1 }, mc);
+ try std.testing.expectEqualStrings("\x1b[B", try Sent.take(core, &sent));
+ // Back on the main screen a shell's wheel is ours: a notch to glide,
+ // nothing to the pty.
+ core.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1049l" } });
+ _ = try Sent.take(core, &sent);
+ wheelAt(&g, &in, .{ .y = 1, .integer_y = 1 }, mc);
+ try std.testing.expectEqualStrings("", try Sent.take(core, &sent));
+ try std.testing.expectEqual(@as(i32, -1), g.scroll_notch);
+}
+
/// 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