summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 12:43:53 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commitc787d59a987ec601cd7b65399543afe6d37a615d (patch)
treeb593436d7aa4c37cb0e59eef5412dbb77ae5faad /src
parent8b0bed0427064794725f95cb7b020d7d96956975 (diff)
downloadpardes-c787d59a987ec601cd7b65399543afe6d37a615d.tar.gz
pardes-c787d59a987ec601cd7b65399543afe6d37a615d.zip
Route mouse thumb buttons to jump history
Diffstat (limited to 'src')
-rw-r--r--src/detached/wire.zig2
-rw-r--r--src/gui/gui.zig2
-rw-r--r--src/macos.zig4
-rw-r--r--src/macos/Sources/PardesView.swift24
-rw-r--r--src/macos/pardes.h2
-rw-r--r--src/pardes.zig31
-rw-r--r--src/web.zig2
-rw-r--r--src/web/app.mjs27
8 files changed, 83 insertions, 11 deletions
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index ab19bcfb..989991e0 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -81,6 +81,8 @@ const ButtonTag = enum(u8) {
wheel_left = 0x05,
wheel_right = 0x06,
none = 0x07,
+ back = 0x08,
+ forward = 0x09,
};
const KindTag = enum(u8) { press = 0x00, release = 0x01, motion = 0x02, drag = 0x03 };
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 082c8159..a0f0a3e4 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -3029,6 +3029,8 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
1 => .left,
2 => .middle,
3 => .right,
+ 4 => .back,
+ 5 => .forward,
else => return,
};
g.pad_x = b.x;
diff --git a/src/macos.zig b/src/macos.zig
index 96598b90..5c523acd 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -1490,6 +1490,8 @@ export fn pardes_mouse(button_arg: c_int, kind_arg: c_int, col: u16, row: u16, m
5 => .wheel_left,
6 => .wheel_right,
7 => .none,
+ 8 => .back,
+ 9 => .forward,
else => return,
};
const kind: pardes.Mouse.Kind = switch (kind_arg) {
@@ -2365,6 +2367,8 @@ test "pardes.h matches the Zig boundary" {
try expectEqual(c.PARDES_MOUSE_WHEEL_LEFT, @intFromEnum(pardes.Mouse.Button.wheel_left));
try expectEqual(c.PARDES_MOUSE_WHEEL_RIGHT, @intFromEnum(pardes.Mouse.Button.wheel_right));
try expectEqual(c.PARDES_MOUSE_NONE, @intFromEnum(pardes.Mouse.Button.none));
+ try expectEqual(c.PARDES_MOUSE_BACK, @intFromEnum(pardes.Mouse.Button.back));
+ try expectEqual(c.PARDES_MOUSE_FORWARD, @intFromEnum(pardes.Mouse.Button.forward));
try expectEqual(c.PARDES_MOUSE_PRESS, @intFromEnum(pardes.Mouse.Kind.press));
try expectEqual(c.PARDES_MOUSE_RELEASE, @intFromEnum(pardes.Mouse.Kind.release));
try expectEqual(c.PARDES_MOUSE_MOTION, @intFromEnum(pardes.Mouse.Kind.motion));
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index 5c5bc8ba..bfc8a060 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -1730,11 +1730,25 @@ final class PardesView: NSView {
override func rightMouseDown(with event: NSEvent) { beginClick(PARDES_MOUSE_RIGHT, event) }
override func rightMouseDragged(with event: NSEvent) { continueClick(event) }
override func rightMouseUp(with event: NSEvent) { endClick(event) }
- // otherMouse covers button 2 and up. acme's vocabulary stops at three, so
- // every one of them lands on middle rather than being invented into a fourth.
- override func otherMouseDown(with event: NSEvent) { beginClick(PARDES_MOUSE_MIDDLE, event) }
- override func otherMouseDragged(with event: NSEvent) { continueClick(event) }
- override func otherMouseUp(with event: NSEvent) { endClick(event) }
+ // Thumb navigation has its own stream so it cannot replace a held
+ // selection button or inherit a resting trackpad's finger count.
+ override func otherMouseDown(with event: NSEvent) {
+ if event.buttonNumber == 2 { beginClick(PARDES_MOUSE_MIDDLE, event); return }
+ guard event.buttonNumber == 3 || event.buttonNumber == 4,
+ let at = cell(for: event) else { return }
+ press(event.buttonNumber == 3 ? PARDES_MOUSE_BACK : PARDES_MOUSE_FORWARD,
+ at: at, mods: pointerModifiers)
+ }
+ override func otherMouseDragged(with event: NSEvent) {
+ if event.buttonNumber == 2 { continueClick(event) }
+ }
+ override func otherMouseUp(with event: NSEvent) {
+ if event.buttonNumber == 2 { endClick(event); return }
+ guard event.buttonNumber == 3 || event.buttonNumber == 4,
+ let at = cell(for: event) else { return }
+ release(event.buttonNumber == 3 ? PARDES_MOUSE_BACK : PARDES_MOUSE_FORWARD,
+ at: at, mods: pointerModifiers)
+ }
/// A deep press, which is a second gesture layered on the click already in
/// flight. AppKit keeps sending stage-2 events while the finger stays down,
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
index 845c8096..546273d9 100644
--- a/src/macos/pardes.h
+++ b/src/macos/pardes.h
@@ -167,6 +167,8 @@ typedef enum {
PARDES_MOUSE_WHEEL_LEFT = 5,
PARDES_MOUSE_WHEEL_RIGHT = 6,
PARDES_MOUSE_NONE = 7,
+ PARDES_MOUSE_BACK = 8,
+ PARDES_MOUSE_FORWARD = 9,
} pardes_mouse_button_e;
typedef enum {
diff --git a/src/pardes.zig b/src/pardes.zig
index 1dbe2e81..efd82474 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4314,7 +4314,7 @@ test "normal header Enter executes its command rather than searching for it" {
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, wheel_left, wheel_right, none };
+ pub const Button = enum { left, middle, right, wheel_up, wheel_down, wheel_left, wheel_right, none, back, forward };
pub const Kind = enum { press, release, motion, drag };
button: Button,
kind: Kind,
@@ -10747,6 +10747,10 @@ pub const Pardes = struct {
}
fn handleMouse(p: *Pardes, m: Mouse) void {
+ if (m.button == .back or m.button == .forward) {
+ if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1);
+ return;
+ }
const raw_col = @min(m.col, p.screen_w -| 1);
const raw_row = @min(m.row, p.screen_h -| 1);
p.pointer_raw_col = raw_col;
@@ -10842,7 +10846,7 @@ pub const Pardes = struct {
}
switch (m.button) {
- .none => {}, // hover tracked above; motion has no other meaning
+ .none, .back, .forward => {}, // hover and navigation handled above
.wheel_up, .wheel_down => {
if (m.kind != .press) return;
// re-arms the drift guard below; vertical always scrolls, so
@@ -13948,3 +13952,26 @@ test "jump history terminal rectangle follows output and clears on reflow" {
try std.testing.expect(pane.pointerSelection(pane.sel[0]) == null);
try std.testing.expectEqual(.none, pane.sel[0].state);
}
+
+test "mouse thumb buttons navigate once without disturbing selection gestures" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("alpha beta\ngamma delta\nepsilon zeta\n");
+ pane.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true);
+ const selection = pane.primaryRange(pane.file.?.content, 0);
+ p.njumps = 3;
+ p.jcur = 2;
+ for (0..3) |i| p.jumps[i] = .{ .pane = 0, .serial = pane.serial, .line = @intCast(i + 1), .col = 1 };
+ p.drag = .{ .select = .{ .id = 0, .button = config.select_button } };
+ const drag = p.drag;
+ p.update(.{ .mouse = .{ .button = .back, .kind = .press, .col = 0, .row = 0 } });
+ try std.testing.expectEqual(@as(usize, 1), p.jcur);
+ for ([_]Mouse.Kind{ .drag, .release, .motion }) |kind|
+ p.update(.{ .mouse = .{ .button = .back, .kind = kind, .col = 0, .row = 0 } });
+ try std.testing.expectEqual(@as(usize, 1), p.jcur);
+ p.update(.{ .mouse = .{ .button = .forward, .kind = .press, .col = 0, .row = 0 } });
+ p.update(.{ .mouse = .{ .button = .forward, .kind = .release, .col = 0, .row = 0 } });
+ try std.testing.expectEqual(@as(usize, 2), p.jcur);
+ try std.testing.expectEqualDeep(selection, pane.primaryRange(pane.file.?.content, 0));
+ try std.testing.expectEqualDeep(drag, p.drag);
+}
diff --git a/src/web.zig b/src/web.zig
index 3a9446b6..bceaaeeb 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -217,6 +217,8 @@ export fn pardes_mouse(button_arg: u32, kind_arg: u32, col_arg: u32, row_arg: u3
5 => .wheel_left,
6 => .wheel_right,
7 => .none,
+ 8 => .back,
+ 9 => .forward,
else => return,
};
const kind: pardes.Mouse.Kind = switch (kind_arg) {
diff --git a/src/web/app.mjs b/src/web/app.mjs
index fcb9c23d..be61de89 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -7,7 +7,7 @@ const DEFAULT_BG = "#121212";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
-const BUTTON = Object.freeze({ left: 0, middle: 1, right: 2, wheelUp: 3, wheelDown: 4, wheelLeft: 5, wheelRight: 6, none: 7 });
+const BUTTON = Object.freeze({ left: 0, middle: 1, right: 2, wheelUp: 3, wheelDown: 4, wheelLeft: 5, wheelRight: 6, none: 7, back: 8, forward: 9 });
const MOUSE = Object.freeze({ press: 0, release: 1, motion: 2, drag: 3 });
const SPECIAL_KEYS = Object.freeze({
Enter: 0x0d,
@@ -214,11 +214,22 @@ export class PardesRuntime {
return { x: rect.left + (col + 0.5) * this.cellWidth, y: rect.top + (row + 0.5) * this.cellHeight };
}
- pointerDown(event) {
+ mouseDown(event) {
event.preventDefault();
+ if (event.button !== 3 && event.button !== 4) return;
+ this.root.focus({ preventScroll: true });
+ const point = this.point(event);
+ this.wasm.pardes_mouse(pointerButton(event.button), MOUSE.press, point.col, point.row, modifiers(event));
+ }
+
+ pointerDown(event) {
+ // Mouse defaults are cancelled by mousedown. Cancelling pointerdown
+ // would suppress that stream, including thumb presses while left is held.
+ if (event.button === 3 || event.button === 4) return;
+ if (event.pointerType === "touch") event.preventDefault();
this.root.focus({ preventScroll: true });
- this.root.setPointerCapture?.(event.pointerId);
const point = this.point(event);
+ this.root.setPointerCapture?.(event.pointerId);
if (event.pointerType === "touch") {
const packed = this.wasm.pardes_chrome_target(point.col, point.row) >>> 0;
const chrome = packed !== 0xffffffff;
@@ -233,6 +244,7 @@ export class PardesRuntime {
}
pointerMove(event) {
+ if (event.button === 3 || event.button === 4) return;
const point = this.point(event);
const active = this.pointer?.id === event.pointerId ? this.pointer : null;
if (!active) {
@@ -268,6 +280,10 @@ export class PardesRuntime {
}
pointerUp(event) {
+ if (event.button === 3 || event.button === 4) {
+ event.preventDefault();
+ return;
+ }
const active = this.pointer?.id === event.pointerId ? this.pointer : null;
if (!active) return;
event.preventDefault();
@@ -303,6 +319,9 @@ export class PardesRuntime {
this.root.addEventListener("keydown", (event) => this.key(event));
this.root.addEventListener("paste", (event) => this.paste(event));
this.root.addEventListener("contextmenu", (event) => event.preventDefault());
+ this.root.addEventListener("auxclick", (event) => event.preventDefault());
+ // Mouse events retain additional button presses while another button is held.
+ this.root.addEventListener("mousedown", (event) => this.mouseDown(event));
this.root.addEventListener("pointerdown", (event) => this.pointerDown(event));
this.root.addEventListener("pointermove", (event) => this.pointerMove(event));
this.root.addEventListener("pointerup", (event) => this.pointerUp(event));
@@ -453,7 +472,7 @@ function modifiers(event) {
}
function pointerButton(button) {
- return button === 1 ? BUTTON.middle : button === 2 ? BUTTON.right : BUTTON.left;
+ return button === 1 ? BUTTON.middle : button === 2 ? BUTTON.right : button === 3 ? BUTTON.back : button === 4 ? BUTTON.forward : BUTTON.left;
}
function clamp(value, low, high) {