summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-06 21:08:40 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitf442004dcd8e91114765a918201ecabe99f69eeb (patch)
tree141d8991a753e82cb3d897c8a717230c2c82913e /src
parent49f8c696ea208d2653521699d77d7e8f48cac183 (diff)
downloadpardes-f442004dcd8e91114765a918201ecabe99f69eeb.tar.gz
pardes-f442004dcd8e91114765a918201ecabe99f69eeb.zip
horizontal scroll for file panes (terminals wrap at pty width — nothing to scroll): pane.hscroll cuts content columns after the lineno gutter in bodyText (post edit-splice, so typed runs shift with the text), no scrollbar. Drivers: horizontal wheel (new wheel_left/right Mouse.Buttons, SGR 66/67 via vaxis on the tty, SDL wheel.x on the gui, ±4 cols, clamped at 0, right overscroll allowed) and keyboard movement — ensureCursorVisible grew the horizontal mirror of the scrolloff reconcile, so $ pulls the view right with 3 columns of margin and 0 reels it home; the wheel never moves the cursor. Shifted alongside: syntax recolor pass, modal cursor paint (hidden when off-view), vsel highlight, click->content-col mapping, cursorWordSel. Byte columns like the rest of the file-pane code. New hscroll.snap golden: wheel shift with fixed gutter, left clamp, $ margin follow, 0 home. 30/30.
Diffstat (limited to 'src')
-rw-r--r--src/gui.zig12
-rw-r--r--src/pardes.zig54
-rw-r--r--src/tty.zig2
3 files changed, 58 insertions, 10 deletions
diff --git a/src/gui.zig b/src/gui.zig
index 15631efa..60d9501f 100644
--- a/src/gui.zig
+++ b/src/gui.zig
@@ -1080,6 +1080,8 @@ const StdinFeed = struct {
.right => .right,
.wheel_up => .wheel_up,
.wheel_down => .wheel_down,
+ .wheel_left => .wheel_left,
+ .wheel_right => .wheel_right,
.none => .none,
else => null,
};
@@ -1266,16 +1268,22 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
},
c.SDL_EVENT_MOUSE_WHEEL => {
const w = sev.wheel;
- if (w.y == 0) return;
+ if (w.y == 0 and w.x == 0) return;
g.mouse_x = w.mouse_x;
g.mouse_y = w.mouse_y;
const mc = mouseCell(g, core, w.mouse_x, w.mouse_y);
- core.update(.{ .mouse = .{
+ if (w.y != 0) core.update(.{ .mouse = .{
.button = if (w.y > 0) .wheel_up else .wheel_down,
.kind = .press,
.col = mc.col,
.row = mc.row,
} });
+ if (w.x != 0) core.update(.{ .mouse = .{
+ .button = if (w.x > 0) .wheel_right else .wheel_left,
+ .kind = .press,
+ .col = mc.col,
+ .row = mc.row,
+ } });
},
c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
const f = sev.tfinger;
diff --git a/src/pardes.zig b/src/pardes.zig
index ebc54426..32a58f89 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -284,7 +284,7 @@ pub const Surface = struct {
pub const Mouse = struct {
// sel indexing relies on left/middle/right being 0/1/2
- pub const Button = enum { left, middle, right, wheel_up, wheel_down, none };
+ pub const Button = enum { left, middle, right, wheel_up, wheel_down, wheel_left, wheel_right, none };
pub const Kind = enum { press, release, motion, drag };
button: Button,
kind: Kind,
@@ -498,6 +498,12 @@ pub const Pane = struct {
cur_pinned: bool = false,
cur_row: i32 = 0,
cur_col: i32 = 0,
+ /// horizontal scroll, file panes only (terminals wrap at pty width, they
+ /// never have wider lines): content columns hidden left of the gutter.
+ /// No scrollbar — the wheel and cursor movement (with scrolloff) drive it,
+ /// the goal is just being able to read long lines. Byte columns, like the
+ /// rest of the file-pane code.
+ hscroll: i32 = 0,
cur_off: u16 = 0,
sel: [3]Sel = @splat(.{}),
edits: [MAX_EDITS]Edit = @splat(.{}),
@@ -552,6 +558,17 @@ pub const Pane = struct {
if (pane.file) |f| to = @min(to, @as(i32, @intCast(modal.lineCount(f.content) -| 1)));
pane.scrollBy(@max(0, to - last));
}
+ // the horizontal mirror, files only: keep scroll_off columns of
+ // context around the cursor (wheel-driven hscroll is exempt — it
+ // never moves the cursor, and a cursor move pulls the view back)
+ if (pane.file != null) {
+ const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, PREFIX_W));
+ const hmargin: i32 = @min(scroll_off, @divTrunc(w - 1, 2));
+ if (pane.cur_col < pane.hscroll + hmargin)
+ pane.hscroll = @max(0, pane.cur_col - hmargin)
+ else if (pane.cur_col > pane.hscroll + w - 1 - hmargin)
+ pane.hscroll = pane.cur_col - (w - 1 - hmargin);
+ }
}
/// the last navigable row: file -> last content line; terminal -> viewport bottom
@@ -1172,7 +1189,7 @@ pub const Pardes = struct {
/// coords: row 0 is the tag; file panes carry the line-number prefix)
fn cursorWordSel(pane: *Pane) Sel {
const vrow = (pane.cur_row - pane.scroll()) + @as(i32, BOX_H);
- const vcol = if (pane.file != null) pane.cur_col + @as(i32, PREFIX_W) else pane.cur_col;
+ const vcol = if (pane.file != null) pane.cur_col - pane.hscroll + @as(i32, PREFIX_W) else pane.cur_col;
return .{ .state = .done, .c0 = vcol, .c1 = vcol, .r0 = vrow, .r1 = vrow };
}
@@ -2368,6 +2385,15 @@ pub const Pardes = struct {
const id = hovered orelse return;
p.panes[id].?.scrollBy(if (m.button == .wheel_up) -1 else 1);
},
+ .wheel_left, .wheel_right => {
+ if (m.kind != .press) return;
+ const id = hovered orelse return;
+ const pane = p.panes[id].?;
+ // ponytail: no right clamp — overscroll shows blank and the
+ // next cursor move or left wheel pulls it back
+ if (pane.file != null)
+ pane.hscroll = @max(0, pane.hscroll + @as(i32, if (m.button == .wheel_right) 4 else -4));
+ },
.left => switch (m.kind) {
.press => {
// middle+left chord: a left press during a middle select-
@@ -2560,7 +2586,7 @@ pub const Pardes = struct {
const body_vis = sl.r1 - @as(i32, BOX_H);
if (body_vis >= 0 and pane.mode != .tty) {
pane.cur_row = body_vis + pane.scroll();
- pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, PREFIX_W)) else sl.c1;
+ pane.cur_col = if (pane.file != null) @max(0, sl.c1 - @as(i32, PREFIX_W)) + pane.hscroll else sl.c1;
pane.cur_off = if (pane.runAt(pane.cur_row, pane.cur_col)) |i| pane.edits[i].len else 0;
pane.cur_pinned = true;
pane.mode = .normal;
@@ -3602,13 +3628,14 @@ pub const Pardes = struct {
while (skip < f.scroll) : (skip += 1) {
if (flines.next() == null) break;
}
+ const hs: usize = @intCast(@max(0, pane.hscroll));
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
const line = flines.next() orelse break;
const base = @intFromPtr(line.ptr) - @intFromPtr(f.content.ptr);
var c: usize = 0;
- while (c < line.len and PREFIX_W + c < tw) : (c += 1) {
- const idx = base + c;
+ while (hs + c < line.len and PREFIX_W + c < tw) : (c += 1) {
+ const idx = base + hs + c;
if (idx < f.highlight_start) continue;
const hidx = idx - f.highlight_start;
if (hidx >= f.highlights.len) continue;
@@ -3697,12 +3724,13 @@ pub const Pardes = struct {
if (pane.mode == .normal and pane.vsel.active) {
const bnd = vselBounds(pane);
const vpfx: i32 = if (pane.file != null) PREFIX_W else 0;
+ const vhs: i32 = if (pane.file != null) pane.hscroll else 0;
var ar: i32 = bnd.lo_row;
while (ar <= bnd.hi_row) : (ar += 1) {
const prow = ar - off + @as(i32, BOX_H);
if (prow < 0 or prow >= @as(i32, r.h)) continue;
- const cstart: i32 = if (ar == bnd.lo_row) bnd.lo_col + vpfx else vpfx;
- const cend: i32 = if (ar == bnd.hi_row) bnd.hi_col + vpfx else @as(i32, tw) - 1;
+ const cstart: i32 = if (ar == bnd.lo_row) bnd.lo_col - vhs + vpfx else vpfx;
+ const cend: i32 = if (ar == bnd.hi_row) bnd.hi_col - vhs + vpfx else @as(i32, tw) - 1;
var col: i32 = @max(cstart, vpfx);
while (col <= cend and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y + @as(u16, @intCast(prow)));
@@ -3726,7 +3754,7 @@ pub const Pardes = struct {
// insert mode maps the run anchor to its visual column; normal
// mode keeps cur_col already-visual.
const cx = if (pane.file != null)
- @as(i32, PREFIX_W) + ccol + @as(i32, coff)
+ @as(i32, PREFIX_W) + ccol + @as(i32, coff) - pane.hscroll
else if (pane.mode == .insert)
pane.renderedCol(crow, ccol, coff)
else
@@ -3861,6 +3889,7 @@ pub const Pardes = struct {
while (i < pane.rows) : (i += 1) {
if (i > 0) try out.append(arena, '\n');
const abs: i32 = @as(i32, @intCast(f.scroll)) + @as(i32, @intCast(i));
+ const row_start = out.items.len;
if (flines.next()) |text| {
var lbuf: [16]u8 = undefined;
// unsigned: {d} prints a leading '+' for signed ints
@@ -3871,6 +3900,15 @@ pub const Pardes = struct {
try base.appendSlice(arena, text);
try spliceRow(arena, &out, pane, abs, base.items);
} else try spliceRow(arena, &out, pane, abs, "");
+ // horizontal scroll: cut hscroll columns after the gutter,
+ // from the fully composed row (edit splices shift with it)
+ if (pane.hscroll > 0) {
+ const cut_at = row_start + PREFIX_W;
+ if (out.items.len > cut_at) {
+ const cut = @min(@as(usize, @intCast(pane.hscroll)), out.items.len - cut_at);
+ out.replaceRangeAssumeCapacity(cut_at, cut, &.{});
+ }
+ }
}
return out.items;
}
diff --git a/src/tty.zig b/src/tty.zig
index c62ace3d..f503303a 100644
--- a/src/tty.zig
+++ b/src/tty.zig
@@ -163,6 +163,8 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void {
.right => .right,
.wheel_up => .wheel_up,
.wheel_down => .wheel_down,
+ .wheel_left => .wheel_left,
+ .wheel_right => .wheel_right,
.none => .none, // button-less motion: hover tracking
else => null,
};