diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 13:49:09 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 387d6a39bad76c5eb2f56328f7cf7869a9a10559 (patch) | |
| tree | cf7aa5209e1d0c9dc522833239b2a710ca1351d9 /src/gui/gui.zig | |
| parent | 3921bfe63980552eddeaa930d5d921d9a2868b37 (diff) | |
| download | pardes-387d6a39bad76c5eb2f56328f7cf7869a9a10559.tar.gz pardes-387d6a39bad76c5eb2f56328f7cf7869a9a10559.zip | |
A wheel over a terminal whose program owns it goes to the program: its mouse report when it tracks the mouse, arrow keys on the alternate screen with alternate scroll, so codex, htop and less scroll
codex 0.158 moved its chat onto the alternate screen with any-event SGR
mouse tracking (1049, 1000/1002/1003/1006, 1007 off); its sign-in screen
keeps 1007 on with tracking off. pardes never forwarded a wheel to a
program, only scrolled ghostty's scrollback, which the alternate screen
does not have, so the wheel did nothing there. A shell on the main screen
still scrolls its scrollback as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 183 |
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 |
