summaryrefslogtreecommitdiff
path: root/src/mouse.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/mouse.zig')
-rw-r--r--src/mouse.zig161
1 files changed, 127 insertions, 34 deletions
diff --git a/src/mouse.zig b/src/mouse.zig
index cb0378de..2280020d 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -70,6 +70,84 @@ pub const Drag = union(enum) {
}
};
+/// A middle or right sweep over a column's tag or the workspace's, held as
+/// a pane tag's is: painted in its button's tint while the button is down,
+/// and run (or looked for) in that tag's context when it comes up. `anchor`
+/// and `cur` are byte offsets into the header's text.
+pub const HeaderSweep = struct {
+ /// The column's serial; null for the workspace's tag.
+ serial: ?u32,
+ button: Mouse.Button,
+ anchor: usize,
+ cur: usize,
+
+ /// The column index the sweep is over, or null for the workspace's
+ /// tag; `error.Gone` once its column has been closed.
+ pub fn column(s: HeaderSweep, p: *const Pardes) error{Gone}!?usize {
+ const serial = s.serial orelse return null;
+ return layout.columnBySerial(p, serial) orelse error.Gone;
+ }
+
+ /// The swept bytes, empty for a click that has not moved.
+ pub fn span(s: HeaderSweep, len: usize) struct { lo: usize, hi: usize } {
+ return .{ .lo = @min(s.anchor, s.cur, len), .hi = @min(@max(s.anchor, s.cur), len) };
+ }
+};
+
+/// What a pointer at (mcol, mrow) is over in a column's tag, or the
+/// workspace's for null: the header's text, the line, and the byte offset
+/// in that text. `clamp` holds a held sweep to the header's rows and text,
+/// as a pane tag's sweep stays on its tag.
+fn headerPoint(p: *Pardes, column: ?usize, mcol: u16, mrow: u16, clamp: bool) ?struct { t: *panes.Text, text: []const u8, line: u16, at: usize } {
+ const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
+ const x = if (column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
+ const focused = p.header_focus and p.header_column == column;
+ const text = edit.flatSurface(p, t) catch return null;
+ const base = if (column != null) p.topBarHeight() else 0;
+ const rows = if (column != null) p.columnBarHeight() else p.topBarHeight();
+ const row: u16 = if (clamp) @min(mrow -| base, rows -| 1) else mrow -| base;
+ const top: usize = if (focused) p.header_top else 0;
+ const lines = modal.lineCount(text);
+ const line: u16 = if (clamp) @intCast(@min(row + top, lines -| 1)) else @intCast(row + top);
+ if (line > 0 and line >= lines) return null;
+ const bar = modal.lineSlice(text, line);
+ const scroll = if (focused) p.header_scroll else 0;
+ const kind: Layer.Kind = if (column != null) .column else .workspace;
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, kind, column orelse 0, p.pointer_tag_hit, clamp, row) orelse (mcol -| x)) + scroll));
+ return .{ .t = t, .text = text, .line = line, .at = modal.lineStartOffset(text, line) + at };
+}
+
+/// A header sweep's button came up: a click runs the word under it, or the
+/// header's selection when it is on that; a sweep runs what it swept.
+fn headerSweepRun(p: *Pardes, sweep: HeaderSweep) void {
+ const column = sweep.column(p) catch return;
+ const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
+ const text = edit.flatSurface(p, t) catch return;
+ const swept = sweep.span(text.len);
+ const selected = if (swept.lo < swept.hi) text[swept.lo..swept.hi] else click: {
+ const line = std.mem.count(u8, text[0..swept.lo], "\n");
+ const start = modal.lineStartOffset(text, line);
+ const bar = modal.lineSlice(text, line);
+ const at = swept.lo - start;
+ const focused = p.header_focus and p.header_column == column;
+ const on = if (focused) tagline.headerSelection(t, text, line) else null;
+ break :click if (on) |sp| (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at)) else wordAtCol(bar, at);
+ };
+ const word = p.scratch.allocator().dupe(u8, selected) catch return;
+ tagline.exitHeader(p);
+ // A column tag's command acts on that column, empty or not.
+ p.exec_column = column;
+ defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
+ if (word.len == 0) return;
+ if (sweep.button == config.exec_button) {
+ const named = std.meta.stringToEnum(Builtin, word);
+ const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active);
+ _ = exec.execute(p, p.active, exec.withArg(p, word, held));
+ } else exec.runBuiltin(p, config.look_cmd, p.active, "", word);
+}
+
fn clampBorderCol(lx: u16, lw: u16, rw: u16, mcol: u16) u16 {
const lo = lx + config.MINW;
const hi = lx +| lw +| rw -| config.MINW;
@@ -255,7 +333,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const previous_tag_hit = p.pointer_tag_hit;
p.pointer_tag_hit = if (m.tag_hit) |point| p.reprojectTagHit(point) else null;
if (m.tag_hit != null and p.pointer_tag_hit == null and m.kind == .press) return;
- if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) {
+ if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag or p.header_sweep != null)) {
if (previous_tag_hit) |previous| {
var continued = previous;
continued.pixel_x = (@as(f32, @floatFromInt(raw_col)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_w));
@@ -263,7 +341,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.pointer_tag_hit = continued;
}
}
- if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) {
+ if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag or p.header_sweep != null)) {
if (previous_tag_hit) |previous| if (p.pointer_tag_hit) |*current| {
current.kind = previous.kind;
current.id = previous.id;
@@ -289,7 +367,13 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.raw_hover_intent = false;
const mapped = p.presentation.pointer(p.screen_w, p.screen_h, raw_col, raw_row) orelse {
look.cancelLookHover(p);
- if (m.kind == .release) dragRelease(p, m.button);
+ if (m.kind == .release) {
+ if (p.header_sweep) |sweep| if (sweep.button == m.button) {
+ p.header_sweep = null;
+ return headerSweepRun(p, sweep);
+ };
+ dragRelease(p, m.button);
+ }
return;
};
const mcol = mapped.col;
@@ -309,6 +393,32 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.chord_arg = null;
}
}
+ if (p.header_sweep) |*sweep| {
+ if (m.kind == .press and switch (m.button) {
+ .left, .middle, .right => true,
+ else => false,
+ }) {
+ // acme 2-3 / 3-2: the other of execute and look cancels the
+ // sweep. The select button is inert: a header's command already
+ // takes the held selection as its argument, which is what a 2-1
+ // chord would give it.
+ if (m.button != config.select_button) p.header_sweep = null;
+ return;
+ }
+ if (m.button == sweep.button and (m.kind == .drag or m.kind == .release)) {
+ const column = sweep.column(p) catch {
+ p.header_sweep = null;
+ return;
+ };
+ if (headerPoint(p, column, mcol, mrow, true)) |point| sweep.cur = point.at;
+ if (m.kind == .release) {
+ const done = sweep.*;
+ p.header_sweep = null;
+ headerSweepRun(p, done);
+ }
+ return;
+ }
+ }
if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) {
for (0..p.ncol) |column| {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
@@ -360,43 +470,26 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
};
if (column == null or mcol < x) return;
}
- const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
- const focused = p.header_focus and p.header_column == column;
- const text = edit.flatSurface(p, t) catch return;
- const row: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0);
- const line: u16 = @intCast(row + if (focused) p.header_top else 0);
- if (line > 0 and line >= modal.lineCount(text)) return;
- const bar = modal.lineSlice(text, line);
- const scroll = if (focused) p.header_scroll else 0;
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, row) orelse (mcol -| x)) + scroll));
+ const point = headerPoint(p, column, mcol, mrow, false) orelse return;
if (m.button == config.select_button) {
+ const t = point.t;
+ const at = point.at - modal.lineStartOffset(point.text, point.line);
// A clicked header is typed straight into, as a clicked tag is.
- if (!focused) tagline.enterHeader(p, column);
+ if (!(p.header_focus and p.header_column == column)) tagline.enterHeader(p, column);
t.mode = .insert;
- t.cur_row = line;
+ t.cur_row = point.line;
t.cur_col = @intCast(at);
- t.vsel = .{ .active = false, .row = line, .col = @intCast(at) };
+ t.vsel = .{ .active = false, .row = point.line, .col = @intCast(at) };
p.header_drag = true;
} else {
- const span = if (focused) tagline.headerSelection(t, text, line) else null;
- const selected = if (span) |sp|
- (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at))
- else
- wordAtCol(bar, at);
- const word = p.scratch.allocator().dupe(u8, selected) catch return;
- tagline.exitHeader(p);
- // A column tag's command acts on that column, empty or not.
- p.exec_column = column;
- defer p.exec_column = null;
- p.exec_header = true;
- defer p.exec_header = false;
- if (word.len > 0) {
- if (m.button == config.exec_button) {
- const named = std.meta.stringToEnum(Builtin, word);
- const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active);
- _ = exec.execute(p, p.active, exec.withArg(p, word, held));
- } else exec.runBuiltin(p, config.look_cmd, p.active, "", word);
- }
+ // Held, as a pane tag's sweep is: it runs when the button
+ // comes up (headerSweepRun).
+ p.header_sweep = .{
+ .serial = if (column) |c| p.col_serial[c] else null,
+ .button = m.button,
+ .anchor = point.at,
+ .cur = point.at,
+ };
}
return;
}