diff options
Diffstat (limited to 'src/gui')
| -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 |
