summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gui/gui.zig41
-rw-r--r--src/layout.zig18
-rw-r--r--src/pardes.zig86
3 files changed, 100 insertions, 45 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index a4cc7672..989f6b25 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -2876,6 +2876,43 @@ const StdinFeed = struct {
var seq_start: usize = 0;
while (seq_start < len) {
+ // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>BEL
+ // A mouse event at window pixels, built as SDL would and run
+ // through dispatch, so a driver exercises the real pointer path
+ // (pixel to cell, tag and body hits). SGR mouse carries only cells.
+ const mouse_prefix = "\x1b]777;mouse;";
+ if (std.mem.startsWith(u8, f.buf[seq_start..len], mouse_prefix)) {
+ const body = f.buf[seq_start + mouse_prefix.len .. len];
+ const end = std.mem.indexOfScalar(u8, body, 0x07) orelse {
+ std.mem.copyForwards(u8, f.buf[0 .. len - seq_start], f.buf[seq_start..len]);
+ f.fill = len - seq_start;
+ break;
+ };
+ if (g) |gp| {
+ var parts = std.mem.splitScalar(u8, body[0..end], ';');
+ const kind = parts.next() orelse "";
+ const button = std.fmt.parseInt(u8, parts.next() orelse "", 10) catch 0;
+ const x = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
+ const y = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
+ var sev = std.mem.zeroes(c.SDL_Event);
+ if (std.mem.eql(u8, kind, "motion")) {
+ sev.type = c.SDL_EVENT_MOUSE_MOTION;
+ sev.motion.x = x;
+ sev.motion.y = y;
+ sev.motion.state = if (button == 0 or button > 5) 0 else @as(u32, 1) << @intCast(button - 1);
+ } else {
+ sev.type = if (std.mem.eql(u8, kind, "down")) c.SDL_EVENT_MOUSE_BUTTON_DOWN else c.SDL_EVENT_MOUSE_BUTTON_UP;
+ sev.button.button = button;
+ sev.button.down = std.mem.eql(u8, kind, "down");
+ sev.button.x = x;
+ sev.button.y = y;
+ }
+ dispatch(gp, in, &sev);
+ out.n_events += 1;
+ }
+ seq_start += mouse_prefix.len + end + 1;
+ continue;
+ }
const prefix = "\x1b]777;finger;";
if (std.mem.startsWith(u8, f.buf[seq_start..len], prefix)) {
const body = f.buf[seq_start + prefix.len .. len];
@@ -2909,7 +2946,9 @@ const StdinFeed = struct {
seq_start += prefix.len + end + 1;
continue;
}
- if (len - seq_start < prefix.len and std.mem.startsWith(u8, prefix, f.buf[seq_start..len])) {
+ if ((len - seq_start < prefix.len and std.mem.startsWith(u8, prefix, f.buf[seq_start..len])) or
+ (len - seq_start < mouse_prefix.len and std.mem.startsWith(u8, mouse_prefix, f.buf[seq_start..len])))
+ {
std.mem.copyForwards(u8, f.buf[0 .. len - seq_start], f.buf[seq_start..len]);
f.fill = len - seq_start;
break;
diff --git a/src/layout.zig b/src/layout.zig
index 8db05a07..3637f30e 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -713,14 +713,26 @@ pub fn columnBySerial(p: *const Pardes, serial: u32) ?usize {
return null;
}
-pub fn columnMoveTarget(p: *const Pardes, source: usize, x: u16) usize {
- if (source >= p.ncol) return source;
+/// What releasing column `source`'s grip at `x` does: past a neighbour's
+/// middle it takes that column's place; short of it, as in acme, the
+/// column's left edge moves to `x - grab`, clamped as setColumnPairWidth
+/// clamps it. Still on the grip, or at the leftmost column, nothing moves.
+/// The drag preview and the release both ask here, so they cannot disagree.
+pub const ColumnDrop = union(enum) { stay, reorder: usize, edge: u16 };
+
+pub fn columnDrop(p: *const Pardes, source: usize, x: u16, grab: u16) ColumnDrop {
+ if (source >= p.ncol) return .stay;
var insertion: usize = 0;
for (0..p.ncol) |column| {
if (column == source) continue;
if (x >= p.col_x[column] + p.col_w[column] / 2) insertion += 1;
}
- return insertion;
+ if (insertion != source) return .{ .reorder = insertion };
+ if (source == 0 or (x >= p.col_x[source] and x < p.col_x[source] + config.GUTTER)) return .stay;
+ if (p.col_w[source - 1] + p.col_w[source] < 2 * config.MINW) return .stay;
+ const lo = p.col_x[source - 1] + config.MINW;
+ const hi = (p.col_x[source] + p.col_w[source]) -| config.MINW;
+ return .{ .edge = std.math.clamp(x -| grab, lo, @max(lo, hi)) };
}
/// Share out the width of columns `left` and `left + 1` so the left one is
diff --git a/src/pardes.zig b/src/pardes.zig
index f968511a..4786bd70 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5155,7 +5155,7 @@ const Drag = union(enum) {
move: struct { id: usize, cur_x: u16, cur_y: u16 },
/// `grab`: how far into the grip it was pressed, so the column's left
/// edge lands at `cur_x - grab`. A release still on the grip moves nothing.
- column_move: struct { column: usize, serial: u32, cur_x: u16, grab: u16 = 0 },
+ column_move: struct { serial: u32, cur_x: u16, grab: u16 = 0 },
tag: struct { id: usize },
/// chorded: a 1-2/1-3 cut/paste chord fired during this left drag —
/// the drag's own release is then inert
@@ -7793,7 +7793,7 @@ pub const Pardes = struct {
.key => |key| p.handleKey(key),
.mouse => |m| {
if (m.kind == .release) switch (p.drag) {
- .border_v, .border_h, .move => p.presentation.snap_once = true,
+ .border_v, .border_h, .move, .column_move => p.presentation.snap_once = true,
else => {},
};
p.handleMouse(m);
@@ -12017,7 +12017,7 @@ pub const Pardes = struct {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
if (m.button == config.select_button) {
p.header_drag = false;
- p.drag = .{ .column_move = .{ .column = column, .serial = p.col_serial[column], .cur_x = mcol, .grab = mcol - p.col_x[column] } };
+ p.drag = .{ .column_move = .{ .serial = p.col_serial[column], .cur_x = mcol, .grab = mcol - p.col_x[column] } };
}
return;
}
@@ -12488,15 +12488,10 @@ pub const Pardes = struct {
},
.border_h => |d| if (d.cur_y != seamRowOf(p, d.col, d.top_idx))
layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y),
- .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| {
- const target = layout.columnMoveTarget(p, column, d.cur_x);
- if (target != column) {
- layout.reorderColumn(p, column, target);
- } else if (column > 0 and (d.cur_x < p.col_x[column] or d.cur_x >= p.col_x[column] + config.GUTTER)) {
- // Dropped short of another column's place: as in acme,
- // the column's left edge moves to where it was dropped.
- layout.setColumnPairWidth(p, column - 1, (d.cur_x -| d.grab) -| p.col_x[column - 1]);
- }
+ .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| switch (layout.columnDrop(p, column, d.cur_x, d.grab)) {
+ .stay => {},
+ .reorder => |target| layout.reorderColumn(p, column, target),
+ .edge => |x| layout.setColumnPairWidth(p, column - 1, x - p.col_x[column - 1]),
},
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
@@ -14091,7 +14086,10 @@ pub const Pardes = struct {
const active_col = if (layout.findPane(p, p.active)) |f| f.col == c else false;
const bg = if (active_col and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
const fg = if (active_col and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg;
- const grip_bg = chrome.column_box_dim;
+ // A grip is muted until it is grabbed; then it lights, as a pane's
+ // box does for the active pane, so the drag shows what it holds.
+ const grabbed = p.drag == .column_move and layout.columnBySerial(p, p.drag.column_move.serial) == c;
+ const grip_bg = if (grabbed) chrome.column_box else chrome.column_box_dim;
const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10;
const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 };
const grip_style: CellStyle = .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline };
@@ -14106,7 +14104,8 @@ pub const Pardes = struct {
.bg = .{ .rgb = bg },
.font_role = .tagline,
});
- if (p.pointer_inside and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) {
+ // A carried column's pointer is aiming at a place, not a word.
+ if (p.pointer_inside and p.drag != .column_move and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) {
const bar = p.columnTag(c);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll;
@@ -14154,33 +14153,38 @@ pub const Pardes = struct {
var col = p.col_x[d.col];
while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
},
- .column_move => |d| {
- const source = layout.columnBySerial(p, d.serial) orelse d.column;
- const target = layout.columnMoveTarget(p, source, d.cur_x);
- if (source < p.ncol and target != source) {
- const boundary = if (target < source) p.col_x[target] else p.col_x[target] + p.col_w[target];
- const x = @min(boundary, s.cols -| 1);
- // Leave the outside insertion rail visible above compact
- // text layers. This clips presentation only, never reflows.
- if (boundary == s.cols) {
- for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
- layer.viewport.w -|= 1;
- };
- for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
- layer.viewport.w -|= 1;
- };
- }
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
- } else if (source > 0 and source < p.ncol and (d.cur_x < p.col_x[source] or d.cur_x >= p.col_x[source] + config.GUTTER) and
- p.col_w[source - 1] + p.col_w[source] >= 2 * config.MINW)
- {
- // Where the left edge would land: the border drag's rail.
- const lo = p.col_x[source - 1] + config.MINW;
- const hi = (p.col_x[source] + p.col_w[source]) -| config.MINW;
- const x = @min(std.math.clamp(d.cur_x -| d.grab, lo, @max(lo, hi)), s.cols -| 1);
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
+ .column_move => |d| rail: {
+ const source = layout.columnBySerial(p, d.serial) orelse break :rail;
+ // Where the column's left edge would land, reordered or not.
+ const edge: u16 = switch (layout.columnDrop(p, source, d.cur_x, d.grab)) {
+ .stay => break :rail,
+ .reorder => |target| if (target < source) p.col_x[target] else p.col_x[target] + p.col_w[target],
+ .edge => |x| x,
+ };
+ // Leave the outside insertion rail visible above compact
+ // text layers. This clips presentation only, never reflows.
+ if (edge == s.cols) {
+ for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
+ layer.viewport.w -|= 1;
+ };
+ for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
+ layer.viewport.w -|= 1;
+ };
+ }
+ // The rail runs in the seam cell left of the edge, where a
+ // border drag's does: the edge cell itself is a gutter, which
+ // the GUI paints as a scroll rail that swallowed the dashes.
+ // At the window's left edge there is no seam, so it runs just
+ // past that scroll rail instead. It takes the lit grip's
+ // color: this is where the grabbed column lands.
+ const x = @min(if (edge == 0) config.GUTTER - 1 else edge - 1, s.cols -| 1);
+ var row: u16 = p.topBarHeight();
+ while (row < s.rows) : (row += 1) {
+ // Past the left edge's scroll rail the rail would cut
+ // through column 0's grip and pane boxes: leave them whole.
+ if (edge == 0 and s.at(x, row).style.font_role == .tagline) continue;
+ s.overlayDash(x, row, "╎");
+ s.at(x, row).style.fg = .{ .rgb = chrome.column_box };
}
},
.move => |d| if (d.cur_x < s.cols) {