summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 17:30:38 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:34:36 -0300
commite462d566f710e714e3b7176672e98fc82a6dea62 (patch)
tree64b3e9dc1ef36aeedbaae7f7e3a77b556676b952
parentebe561b8083782a105a2ee29fb0a031d1c2427d3 (diff)
downloadpardes-e462d566f710e714e3b7176672e98fc82a6dea62.tar.gz
pardes-e462d566f710e714e3b7176672e98fc82a6dea62.zip
Separate pane and column grip hit areas from tagline text with a small gap
-rw-r--r--docs/tags.md3
-rw-r--r--src/gui/gui.zig10
-rw-r--r--src/macos.zig5
-rw-r--r--src/macos/Sources/PardesView.swift2
-rw-r--r--src/macos/pardes.h1
-rw-r--r--src/pardes.zig91
-rw-r--r--src/web.zig4
-rw-r--r--src/web/app.mjs11
-rw-r--r--test/column_drag.zig20
-rw-r--r--test/mode.zig2
-rw-r--r--test/panes.zig22
-rw-r--r--test/web-snapshots/touch.mjs5
12 files changed, 107 insertions, 69 deletions
diff --git a/docs/tags.md b/docs/tags.md
index eb11c0a9..b0af5824 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -6,7 +6,8 @@ pane (or its first pane when focus comes from another column). This makes
`New`, `Tty`, `Find`, and `Grep` available beside the work they act on.
`New` appears only in the column tag by default; pane tags keep their own
save, terminal, close, and collapse commands. `Tty` opens a new embedded terminal.
-Column command text starts after its drag grip. The column grip has a different
+Pane and column command text leave a small gap after their aligned drag grips.
+The column grip has a different
color from a pane grip. It stays blank and muted even when its column is active.
Drag it left or right to move the whole column; the
insertion line previews its destination. Only columns between the old and new
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 590e5aa8..87e19687 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -6528,7 +6528,7 @@ fn appendPaneChrome(
const body_end = body_y + r.h - pardes.BOX_H;
const x0 = @as(f32, @floatFromInt(r.x)) * layout.w;
const x1 = x0 + @as(f32, @floatFromInt(r.w)) * layout.w;
- const tag_cells = surface.cells[@as(usize, tag_y) * surface.cols + r.x + config.GUTTER ..][0 .. r.w - config.GUTTER];
+ const tag_cells = surface.cells[@as(usize, tag_y) * surface.cols + r.x + pardes.TAG_TEXT_INSET ..][0 .. r.w - pardes.TAG_TEXT_INSET];
const tag_visible = for (tag_cells) |*cell| {
if (cellFontRole(cell) != .tagline) break false;
} else true;
@@ -6568,8 +6568,8 @@ fn appendPaneChrome(
fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout, builder: *OverlayBuilder) void {
if (g.tagline_height < 6 or g.tagline_width < 3) return;
for (surface.tagLayers()) |layer| {
- if (layer.cols == 0 or layer.kind != .pane or layer.viewport.x < config.GUTTER) continue;
- const x = layer.viewport.x - config.GUTTER;
+ if (layer.cols == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET) continue;
+ const x = layer.viewport.x - pardes.TAG_TEXT_INSET;
const y = layer.viewport.y;
if (x >= surface.cols or y >= surface.rows) continue;
const cell = &surface.cells[@as(usize, y) * surface.cols + x];
@@ -6593,8 +6593,8 @@ test "column grips stay unmarked while pane marks retain physical centering" {
g.cell_h = 20;
var cells: [80]pardes.Cell = @splat(.{ .default = false, .style = .{ .fg = .{ .rgb = .{ 100, 120, 140 } } } });
var surface: pardes.Surface = .{ .cols = 20, .rows = 4, .cells = &cells };
- surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 7, .y = 1, .w = 13, .h = 1 }, .cols = 20 };
- surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 7, .y = 3, .w = 13, .h = 1 }, .cols = 20 };
+ surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 8, .y = 1, .w = 13, .h = 1 }, .cols = 20 };
+ surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 8, .y = 3, .w = 13, .h = 1 }, .cols = 20 };
var vertices: [24]OverlayVertex = undefined;
var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 200, .win_h = 80 };
appendTagGrips(&g, &surface, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
diff --git a/src/macos.zig b/src/macos.zig
index febb7f88..3003fd46 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -1876,6 +1876,10 @@ export fn pardes_cursor_x() i32 {
return if (st.core.surface.cursor) |c| c.x else -1;
}
+export fn pardes_tag_text_inset() u32 {
+ return pardes.TAG_TEXT_INSET;
+}
+
export fn pardes_grip_columns() u32 {
return @import("config.zig").GUTTER;
}
@@ -2418,6 +2422,7 @@ test "pardes.h declares every export the way it is defined" {
try expectSameAbi(@TypeOf(c.pardes_pointer_shape), @TypeOf(pardes_pointer_shape));
try expectSameAbi(@TypeOf(c.pardes_mouse_pixel), @TypeOf(pardes_mouse_pixel));
try expectSameAbi(@TypeOf(c.pardes_row_metrics), @TypeOf(pardes_row_metrics));
+ try expectSameAbi(@TypeOf(c.pardes_tag_text_inset), @TypeOf(pardes_tag_text_inset));
try expectSameAbi(@TypeOf(c.pardes_grip_columns), @TypeOf(pardes_grip_columns));
try expectSameAbi(@TypeOf(c.pardes_tag_layer_limit), @TypeOf(pardes_tag_layer_limit));
try expectSameAbi(@TypeOf(c.pardes_tag_layer_value), @TypeOf(pardes_tag_layer_value));
diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift
index ba05ea51..e2234264 100644
--- a/src/macos/Sources/PardesView.swift
+++ b/src/macos/Sources/PardesView.swift
@@ -1172,7 +1172,7 @@ final class PardesView: NSView {
private func drawTagGrip(_ ctx: CGContext, values: [Int], canonical: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) {
let width = Int(pardes_grip_columns())
- let x = values[4] - width, y = values[5]
+ let x = values[4] - Int(pardes_tag_text_inset()), y = values[5]
if values[1] != 2 || x < 0 || x >= cols || y >= rows || taglines.height < 6 || taglines.width < 3 { return }
var cell = canonical[y * cols + x]
let text = withUnsafeBytes(of: &cell.text) { String(decoding: $0.prefix(Int(cell.len)), as: UTF8.self) }
diff --git a/src/macos/pardes.h b/src/macos/pardes.h
index 15a76d04..48f38e58 100644
--- a/src/macos/pardes.h
+++ b/src/macos/pardes.h
@@ -341,6 +341,7 @@ void pardes_mouse_pixel(pardes_mouse_button_e button, pardes_mouse_kind_e kind,
uint16_t col, uint16_t row, uint32_t mods, float x, float y,
float body_w, float body_h, float tagline_w, float tagline_h);
void pardes_row_metrics(uint16_t body_w, uint16_t body_h, uint16_t tagline_w, uint16_t tagline_h);
+uint32_t pardes_tag_text_inset(void);
uint32_t pardes_grip_columns(void);
uint32_t pardes_tag_layer_limit(void);
uint32_t pardes_tag_layer_value(uint32_t index, uint32_t field);
diff --git a/src/pardes.zig b/src/pardes.zig
index bd7c43e9..d115bf2d 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -84,6 +84,8 @@ pub const TOPBAR_H: u16 = 1;
pub const COLBAR_H: u16 = 1;
pub const TagLine = @import("tag_line.zig").Line;
pub const BOX_H: u16 = 1;
+pub const TAG_GAP: u16 = 1;
+pub const TAG_TEXT_INSET: u16 = config.GUTTER + TAG_GAP;
pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 {
const spare = cell_h -| tagline_h;
@@ -1005,7 +1007,7 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" {
const pane = p.panes[0].?;
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(pane));
- const tag_x = p.rects[0].x + config.GUTTER;
+ const tag_x = p.rects[0].x + TAG_TEXT_INSET;
const tag_y = p.rects[0].y;
const rendered = try p.tagText(p.scratch.allocator(), pane);
@@ -1222,7 +1224,7 @@ test "Togglettymode builtin and keyboard chord share mode transitions including
p.acknowledgePanelPresentation(&.{});
const tag = try p.tagText(p.scratch.allocator(), pane);
- const x = p.rects[0].x + config.GUTTER + @as(u16, @intCast(std.mem.indexOf(u8, tag, "Togglettymode").?));
+ const x = p.rects[0].x + TAG_TEXT_INSET + @as(u16, @intCast(std.mem.indexOf(u8, tag, "Togglettymode").?));
const y = p.rects[0].y;
p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = x, .row = y } });
p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = x, .row = y } });
@@ -4381,8 +4383,8 @@ test "editable workspace and column tags replace selections and persist" {
p.update(.{ .key = .{ .cp = 'H', .text = "Help" } });
try std.testing.expect(std.mem.startsWith(u8, p.global_tag.slice(""), "Help Joincol"));
p.update(.{ .key = .{ .cp = Key.escape } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
p.update(.{ .key = .{ .cp = 'é', .text = "é" } });
try std.testing.expect(std.mem.startsWith(u8, p.columnTag(0), "éNew"));
p.update(.{ .key = .{ .cp = Key.backspace } });
@@ -4427,8 +4429,8 @@ test "column tags render edit hover and scroll from the column edge" {
var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
defer frame.deinit();
for (0..p.ncol) |column| {
- const x = p.col_x[column] + config.GUTTER;
- const width = p.col_w[column] - config.GUTTER;
+ const x = p.col_x[column] + TAG_TEXT_INSET;
+ const width = p.col_w[column] - TAG_TEXT_INSET;
const initial = try p.render(frame.allocator());
for ("New", 0..) |byte, offset|
try std.testing.expectEqual(byte, initial.at(x + @as(u16, @intCast(offset)), TOPBAR_H).text[0]);
@@ -4481,8 +4483,8 @@ test "column tag first-cell execution keeps New local to the clicked column" {
p.active = p.col_panes[1 - column][0];
p.acknowledgePanelPresentation(&.{});
const before = p.col_n;
- p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = p.col_x[column] + config.GUTTER, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = p.col_x[column] + config.GUTTER, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = p.col_x[column] + TAG_TEXT_INSET, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = p.col_x[column] + TAG_TEXT_INSET, .row = TOPBAR_H } });
try std.testing.expectEqual(before[column] + 1, p.col_n[column]);
try std.testing.expectEqual(before[1 - column], p.col_n[1 - column]);
try std.testing.expectEqual(column, layout.findPane(p, p.active).?.col);
@@ -4527,8 +4529,8 @@ test "ColumnTags reclaims the row and releases hidden header focus without losin
p.acknowledgePanelPresentation(&.{});
const shown = p.rects[0];
try std.testing.expectEqual(TOPBAR_H + COLBAR_H, shown.y);
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
try std.testing.expect(p.header_column != null and p.topbar_col != null);
p.update(.{ .command = "ColumnTags" });
@@ -4544,8 +4546,8 @@ test "ColumnTags reclaims the row and releases hidden header focus without losin
try std.testing.expect(p.settings.column_tags);
try std.testing.expectEqual(shown, p.rects[0]);
p.acknowledgePanelPresentation(&.{});
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
try std.testing.expect(p.header_column != null);
p.update(.{ .resize = .{ .cols = 60, .rows = 2 } });
try std.testing.expect(p.settings.column_tags);
@@ -6274,7 +6276,7 @@ pub const Pardes = struct {
return;
};
const text = p.headerText();
- const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] -| config.GUTTER else p.screen_w);
+ const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else p.screen_w);
const col = panes.File.rawDisplayCol(text, @min(cur, text.len));
if (col < p.header_scroll) p.header_scroll = col;
if (col >= p.header_scroll + width) p.header_scroll = col -| (width -| 1);
@@ -7377,7 +7379,7 @@ pub const Pardes = struct {
pane.tag_col = @intCast(@min(edit0 + lead, end));
}
if (pane.tag_saved_col == null) if (p.paneIdOf(pane)) |id| {
- const width = p.tagCapacity(p.rects[id].w -| config.GUTTER);
+ const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET);
// Reveal the suffix together with its commands, not merely the
// first command letter at the viewport's far right edge.
pane.tag_scroll = @intCast(panes.File.displayWidth(text) + 1 -| width);
@@ -11086,6 +11088,7 @@ pub const Pardes = struct {
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
const on_tag = row >= tag_y and row < tag_y + BOX_H;
+ if (on_tag and col < r.x + TAG_TEXT_INSET) return null;
if (!on_tag) if (body_hit) |supplied| {
const mapped_hit = p.bodyHitForPane(id, supplied, false) orelse return null;
const c = @as(i32, mapped_hit.col) - config.GUTTER;
@@ -11093,7 +11096,7 @@ pub const Pardes = struct {
return .{ .sel = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v }, .on_tag = false };
};
const tag_col = if (on_tag) p.tagColumn(.pane, id, tag_hit, false) else null;
- const c: i32 = (if (tag_col) |value| @as(i32, value) else @as(i32, col) - @as(i32, r.x + config.GUTTER)) +
+ const c: i32 = (if (tag_col) |value| @as(i32, value) else @as(i32, col) - @as(i32, r.x + (if (on_tag) TAG_TEXT_INSET else config.GUTTER))) +
(if (on_tag) @as(i32, p.panes[id].?.tag_scroll) else 0);
const v: i32 = if (on_tag) 0 else @as(i32, row) - @as(i32, body_y) + @as(i32, BOX_H);
return .{
@@ -11324,7 +11327,7 @@ pub const Pardes = struct {
}
}
if (p.header_drag and m.button == config.select_button and (m.kind == .drag or m.kind == .release)) {
- const x = if (p.header_column) |c| p.col_x[c] + config.GUTTER else 0;
+ const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
const bar = p.headerText();
p.topbar_col = @intCast(@min(bar.len, panes.File.rawAtDisplay(bar, (p.tagColumn(if (p.header_column != null) .column else .workspace, p.header_column orelse 0, p.pointer_tag_hit, p.header_drag) orelse (mcol -| x)) + p.header_scroll)));
if (m.kind == .release) p.header_drag = false;
@@ -11341,13 +11344,13 @@ pub const Pardes = struct {
if (mrow >= TOPBAR_H) {
for (0..p.ncol) |c| if (mcol >= p.col_x[c] and mcol < p.col_x[c] + p.col_w[c]) {
column = c;
- x = p.col_x[c] + config.GUTTER;
+ x = p.col_x[c] + TAG_TEXT_INSET;
if (layout.findPane(p, p.active)) |active_col| {
if (active_col.col != c and p.col_n[c] > 0) p.active = p.col_panes[c][0];
}
break;
};
- if (column == null) return;
+ if (column == null or mcol < x) return;
}
if (p.header_column != column) {
p.rememberHeader();
@@ -11478,8 +11481,9 @@ pub const Pardes = struct {
p.active = id;
pane.scrollBy(-(@as(i32, mrow) - @as(i32, body_y)));
} else if (mrow >= tag_y and mrow < tag_y + BOX_H) {
+ if (mcol < r.x + TAG_TEXT_INSET) return;
p.active = id;
- p.enterTagEdit(pane, if (p.tagColumn(.pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + config.GUTTER));
+ p.enterTagEdit(pane, if (p.tagColumn(.pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + TAG_TEXT_INSET));
pane.tag_anchor = pane.tag_col;
p.drag = .{ .tag = .{ .id = id } };
} else {
@@ -11634,14 +11638,14 @@ pub const Pardes = struct {
else
@as(i32, @min(@max(mrow, r.y), r.y + body_h - 1)) - @as(i32, r.y) + @as(i32, BOX_H);
} else pane.sel[b].r1 = if (pane.sel[b].r0 < BOX_H) 0 else @as(i32, mrow) - @as(i32, r.y);
- if (pane.sel[b].r1 < BOX_H) pane.sel[b].c1 += pane.tag_scroll;
+ if (pane.sel[b].r1 < BOX_H) pane.sel[b].c1 += @as(i32, pane.tag_scroll) - TAG_GAP;
if (comptime pdf_enabled)
panes.Pdf.pointerUpdate(&s.pdf, p, pane, mcol, mrow);
}
},
.tag => |d| {
const pane = p.panes[d.id] orelse return;
- const c = if (p.tagColumn(.pane, d.id, p.pointer_tag_hit, true)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, p.rects[d.id].x + config.GUTTER);
+ const c = if (p.tagColumn(.pane, d.id, p.pointer_tag_hit, true)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, p.rects[d.id].x + TAG_TEXT_INSET);
const text = p.tagText(p.scratch.allocator(), pane) catch return;
pane.tag_col = @intCast(@min(text.len, panes.File.rawAtDisplay(text, @as(usize, @intCast(@max(0, c))) + pane.tag_scroll)));
pane.tag_sel = pane.tag_col != pane.tag_anchor;
@@ -13256,8 +13260,9 @@ pub const Pardes = struct {
};
if (p.columnBarHeight() > 0) for (0..p.ncol) |c| {
const grip = @min(config.GUTTER, p.col_w[c]);
- const x = p.col_x[c] + grip;
- const w = p.col_w[c] - grip;
+ const inset = @min(TAG_TEXT_INSET, p.col_w[c]);
+ const x = p.col_x[c] + inset;
+ const w = p.col_w[c] - inset;
if (p.col_w[c] == 0 or p.col_x[c] >= s.cols) continue;
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;
@@ -13267,6 +13272,7 @@ pub const Pardes = struct {
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 };
s.fill(p.col_x[c], TOPBAR_H, grip, 1, grip_style);
+ s.fill(p.col_x[c] + grip, TOPBAR_H, inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
s.fill(x, TOPBAR_H, w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
const bar_scroll = if (p.topbar_col != null and p.header_column == c) p.header_scroll else 0;
@@ -13291,8 +13297,8 @@ pub const Pardes = struct {
};
if (p.topbar_col) |cur| {
const bar = p.headerText();
- const x = if (p.header_column) |c| p.col_x[c] + config.GUTTER else 0;
- const w = if (p.header_column) |c| p.col_w[c] -| config.GUTTER else s.cols;
+ const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
+ const w = if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else s.cols;
const y: u16 = if (p.header_column != null) TOPBAR_H else 0;
const anchor = p.header_anchor orelse cur;
var col = panes.File.rawDisplayCol(bar, @min(cur, anchor)) -| p.header_scroll;
@@ -13631,8 +13637,10 @@ pub const Pardes = struct {
// row zero in Sel space and starts at its real first text cell.
const scroll: i32 = if (row < BOX_H) pane.tag_scroll else 0;
var col: i32 = if (pane.file != null and row >= BOX_H) @max(c0, @as(i32, panes.File.gutterWidth(pane))) else @max(0, c0 - scroll);
- while (col <= c1 - scroll and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), sy);
+ const row_x = if (row < BOX_H) r.x + @min(TAG_TEXT_INSET, r.w) else tx;
+ const row_width = if (row < BOX_H) r.w -| TAG_TEXT_INSET else tw;
+ while (col <= c1 - scroll and col < row_width) : (col += 1) {
+ const cell = s.at(row_x + @as(u16, @intCast(col)), sy);
cell.default = false;
cell.style.bg = .{ .rgb = bg };
if (fg) |ink| cell.style.fg = .{ .rgb = ink };
@@ -13645,41 +13653,45 @@ pub const Pardes = struct {
for (p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
const r = p.rects[id];
- if (r.w <= config.GUTTER or r.h == 0) continue;
+ if (r.w <= TAG_TEXT_INSET or r.h == 0) continue;
const tag = try p.tagText(arena, pane);
- const columns: u16 = @intCast(@min(std.math.maxInt(u16) - config.GUTTER, @max(p.tagCapacity(r.w - config.GUTTER), panes.File.displayWidth(tag) + 1)));
+ const columns: u16 = @intCast(@min(std.math.maxInt(u16) - TAG_TEXT_INSET, @max(p.tagCapacity(r.w - TAG_TEXT_INSET), panes.File.displayWidth(tag) + 1)));
const layer = &p.surface.tag_layers[id];
if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
- const storage = try arena.alloc(Cell, @as(usize, columns) + config.GUTTER);
+ const storage = try arena.alloc(Cell, @as(usize, columns) + TAG_TEXT_INSET);
@memset(storage, .{});
- var temporary: Surface = .{ .cols = columns + config.GUTTER, .rows = 1, .cells = storage };
+ var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = 1, .cells = storage };
std.mem.swap(Surface, &p.surface, &temporary);
- p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + config.GUTTER, .h = 1 }, id, id == p.active, p.tagCapacity(r.w - config.GUTTER)) catch |err| {
+ p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = 1 }, id, id == p.active, p.tagCapacity(r.w - TAG_TEXT_INSET)) catch |err| {
std.mem.swap(Surface, &p.surface, &temporary);
return err;
};
std.mem.swap(Surface, &p.surface, &temporary);
- @memcpy(layer.cells, storage[config.GUTTER..]);
+ @memcpy(layer.cells, storage[TAG_TEXT_INSET..]);
layer.kind = .pane;
layer.id = @intCast(id);
layer.serial = pane.serial;
- layer.viewport = .{ .x = r.x + config.GUTTER, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - config.GUTTER, .h = BOX_H };
+ layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - TAG_TEXT_INSET, .h = BOX_H };
layer.cols = columns;
layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
- layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - config.GUTTER, .bar = temporary.cursor.?.bar } else null;
+ layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null;
}
var text_buf: [1200]u8 = undefined;
try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg);
if (p.columnBarHeight() > 0) for (0..p.ncol) |column| {
if (p.col_w[column] == 0) continue;
const active = if (layout.findPane(p, p.active)) |found| found.col == column and p.settings.focus_tint else false;
- try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(config.GUTTER, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| config.GUTTER, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
+ try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
};
}
fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: TagKind, id: u16, viewport: Rect, text: []const u8, bg: [3]u8, fg: [3]u8) !void {
_ = arena;
const layer = &p.surface.tag_layers[index];
+ if (viewport.w == 0) {
+ layer.cols = 0;
+ return;
+ }
const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), panes.File.displayWidth(text) + 1)));
if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
layer.kind = kind;
@@ -13815,7 +13827,7 @@ pub const Pardes = struct {
.tty => config.tag_tty,
}, box_style);
- try p.paintPaneTag(arena, pane, r, id, active, p.tagCapacity(tw));
+ try p.paintPaneTag(arena, pane, r, id, active, p.tagCapacity(r.w -| TAG_TEXT_INSET));
// Folded panes retain their body state, but neither body pixels nor
// its cursor belong outside the one-row tag rectangle.
@@ -13840,8 +13852,8 @@ pub const Pardes = struct {
const s = &p.surface;
const th = p.theme();
const chrome = p.chromeTheme();
- const tx = r.x + config.GUTTER;
- const tw = r.w - config.GUTTER;
+ const tx = r.x + @min(TAG_TEXT_INSET, r.w);
+ const tw = r.w -| TAG_TEXT_INSET;
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
const page_rgb = th.bg orelse th.tag_bg;
@@ -13851,6 +13863,7 @@ pub const Pardes = struct {
const tag_style: CellStyle = .{ .fg = .{ .rgb = tag_fg }, .bg = .{ .rgb = tag_bg }, .font_role = .tagline };
// Padding belongs to the same font row but keeps its default ink: a
// role must not manufacture a foreground color for blank cells.
+ s.fill(r.x + config.GUTTER, tag_y, @min(TAG_GAP, r.w -| config.GUTTER), BOX_H, .{ .bg = .{ .rgb = tag_bg }, .font_role = .tagline });
s.fill(tx, tag_y, tw, BOX_H, .{
.bg = .{ .rgb = tag_bg },
.font_role = .tagline,
diff --git a/src/web.zig b/src/web.zig
index 72a7bb05..b5a992ae 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -332,6 +332,10 @@ export fn pardes_cursor_x() i32 {
return if (s.core.surface.cursor) |cursor| cursor.x else -1;
}
+export fn pardes_tag_text_inset() u32 {
+ return pardes.TAG_TEXT_INSET;
+}
+
export fn pardes_grip_columns() u32 {
return @import("config.zig").GUTTER;
}
diff --git a/src/web/app.mjs b/src/web/app.mjs
index 65e1240d..477186a3 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -55,15 +55,16 @@ export class DomRenderer {
const cursorBar = this.wasm.pardes_cursor_bar() !== 0;
const raw = new Array(count);
const tagCovered = new Uint8Array(count);
- const tagGrips = new Map();
+ const tagPrefixBands = new Map();
if (this.wasm.pardes_tag_layer_limit) {
for (let i = 0; i < this.wasm.pardes_tag_layer_limit(); i++) {
const val = field => this.wasm.pardes_tag_layer_value(i, field);
if (!val(0)) continue;
const gripCols = this.wasm.pardes_grip_columns?.() ?? 2;
- if (val(1) !== 0 && val(4) >= gripCols && this.cellHeight) {
+ const textInset = this.wasm.pardes_tag_text_inset?.() ?? gripCols;
+ if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) {
const offset = this.wasm.pardes_tagline_band_offset(val(5),rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256;
- for (let x = val(4)-gripCols; x < val(4); x++) tagGrips.set(val(5)*cols+x,offset);
+ for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(val(5)*cols+x,offset);
}
for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) {
if (val(5) < rows) tagCovered[val(5) * cols + x] = 1;
@@ -83,7 +84,7 @@ export class DomRenderer {
const cursor = x === cursorX && y === cursorY;
const cell = this.cells[index];
cell.style.visibility = tagCovered[index] ? "hidden" : "";
- const gripOffset = tagGrips.get(index);
+ const gripOffset = tagPrefixBands.get(index);
cell.style.height = gripOffset === undefined ? "" : `${this.taglineHeight}px`;
cell.style.lineHeight = gripOffset === undefined ? "" : `${this.taglineHeight}px`;
cell.style.marginTop = gripOffset === undefined ? "" : `${gripOffset}px`;
@@ -121,7 +122,7 @@ export class DomRenderer {
const bytes = new Uint8Array(this.wasm.memory.buffer,ptr,cols*CELL_SIZE);
const offset = this.wasm.pardes_tagline_band_offset(values[5],this.rowCount*bh*256,Math.round(bh*256),Math.round(th*256))/256;
const gripCols = this.wasm.pardes_grip_columns?.() ?? 2;
- const gripX = values[4] - gripCols;
+ const gripX = values[4] - (this.wasm.pardes_tag_text_inset?.() ?? gripCols);
const gripCell = canonical[values[5]*this.cols+gripX];
const grip = values[1] === 2 && gripX >= 0 && gripCell?.text === " " && th >= 6 && tw >= 3;
const geometry = [bw,bh,tw,th,offset,...values,grip,gripCell?.fg].join(":");
diff --git a/test/column_drag.zig b/test/column_drag.zig
index 1c9a344e..be4e33a3 100644
--- a/test/column_drag.zig
+++ b/test/column_drag.zig
@@ -85,7 +85,7 @@ test "column grip drag moves only its interval and keeps tag caret with the colu
try std.testing.expect(p.drag == .column_move);
mouse(p, .drag, target);
_ = try p.render(frame.allocator());
- try std.testing.expectEqual(p.col_w[3] - config.GUTTER - 1, (try tagLayer(p, 3)).viewport.w);
+ try std.testing.expectEqual(p.col_w[3] - pardes.TAG_TEXT_INSET - 1, (try tagLayer(p, 3)).viewport.w);
try std.testing.expectEqual(last_rect, p.rects[p.col_panes[3][0]]);
try std.testing.expectEqual(last_columns, (try tagLayer(p, 3)).cols);
try expectOrder(p, &before, &.{ 0, 1, 2, 3 });
@@ -103,7 +103,7 @@ test "column grip drag moves only its interval and keeps tag caret with the colu
try std.testing.expectEqual(caret, p.topbar_col);
_ = try p.render(frame.allocator());
try std.testing.expectEqual(before[1].serial, (try tagLayer(p, 3)).serial);
- try std.testing.expectEqual(p.col_w[3] - config.GUTTER, (try tagLayer(p, 3)).viewport.w);
+ try std.testing.expectEqual(p.col_w[3] - pardes.TAG_TEXT_INSET, (try tagLayer(p, 3)).viewport.w);
mouse(p, .press, p.col_x[3]);
const back = p.col_x[1] + config.GUTTER;
mouse(p, .drag, back);
@@ -136,13 +136,13 @@ test "column grip is separate from compact text and the resize seam" {
defer frame.deinit();
_ = try p.render(frame.allocator());
const layer = try tagLayer(p, 1);
- try std.testing.expectEqual(p.col_x[1] + config.GUTTER, layer.viewport.x);
+ try std.testing.expectEqual(p.col_x[1] + pardes.TAG_TEXT_INSET, layer.viewport.x);
const surface = &p.surface;
for (0..p.ncol) |column| {
const pane_id = p.col_panes[column][0];
const rect = p.rects[pane_id];
try std.testing.expectEqual(p.col_x[column], rect.x);
- try std.testing.expectEqual(rect.x + config.GUTTER, (try tagLayer(p, column)).viewport.x);
+ try std.testing.expectEqual(rect.x + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x);
for (p.surface.tagLayers()) |pane_layer| {
if (pane_layer.cols > 0 and pane_layer.kind == .pane and pane_layer.id == pane_id)
try std.testing.expectEqual((try tagLayer(p, column)).viewport.x, pane_layer.viewport.x);
@@ -152,9 +152,15 @@ test "column grip is separate from compact text and the resize seam" {
const x = rect.x + @as(u16, @intCast(offset));
try std.testing.expectEqual(surface.at(x, pane_tag_y).style.font_role, surface.at(x, pardes.TOPBAR_H).style.font_role);
}
- try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + config.GUTTER, pardes.TOPBAR_H).style.font_role);
- try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + config.GUTTER, pane_tag_y).style.font_role);
+ try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + pardes.TAG_TEXT_INSET, pardes.TOPBAR_H).style.font_role);
+ try std.testing.expectEqual(pardes.FontRole.tagline, surface.at(rect.x + pardes.TAG_TEXT_INSET, pane_tag_y).style.font_role);
}
+ const gap_x = p.col_x[1] + config.GUTTER;
+ try std.testing.expectEqualStrings(" ", p.surface.at(gap_x, pardes.TOPBAR_H).grapheme());
+ mouse(p, .press, gap_x);
+ try std.testing.expect(p.drag != .column_move);
+ try std.testing.expect(!p.header_drag);
+ mouse(p, .release, gap_x);
p.update(.{ .mouse = headerMouse(layer, 0, .press) });
try std.testing.expect(p.drag != .column_move);
try std.testing.expect(p.header_drag);
@@ -199,7 +205,7 @@ test "column grips stay blank and muted across focus hover and drag" {
try std.testing.expectEqual(muted, cell.style.bg);
try std.testing.expectEqual(pardes.FontRole.tagline, cell.style.font_role);
}
- try std.testing.expectEqual(p.col_x[column] + config.GUTTER, (try tagLayer(p, column)).viewport.x);
+ try std.testing.expectEqual(p.col_x[column] + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x);
}
if (dragging) mouse(p, .release, x);
}
diff --git a/test/mode.zig b/test/mode.zig
index 6d694b42..cc36ac04 100644
--- a/test/mode.zig
+++ b/test/mode.zig
@@ -16,7 +16,7 @@ fn clickModeTag(p: *Pardes) !void {
const rect = p.rects[0];
const row = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
for ([_]pardes.Mouse.Kind{ .press, .release }) |kind|
- p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = rect.x + pardes.config.GUTTER + @as(u16, @intCast(col)), .row = row } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col)), .row = row } });
try std.testing.expect(pane.tag_edit);
}
diff --git a/test/panes.zig b/test/panes.zig
index f3e7034f..da06f692 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -111,9 +111,15 @@ const TagLayerTests = struct {
_ = try p.render(frame.allocator());
const layer = try findLayer(p, .pane, second);
const edge = @as(f32, @floatFromInt(layer.viewport.x)) * 10;
- p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) });
+ const grip_edge = edge - @as(f32, @floatFromInt(pardes.TAG_GAP)) * 10;
+ p.update(.{ .mouse = mouse(layer, grip_edge - 0.1, .press) });
try std.testing.expect(p.drag == .move);
try std.testing.expectEqual(second, p.drag.move.id);
+ p.update(.{ .mouse = mouse(layer, grip_edge - 0.1, .release) });
+ try std.testing.expectEqualStrings(" ", p.surface.at(layer.viewport.x - 1, layer.viewport.y).grapheme());
+ p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) });
+ try std.testing.expect(p.drag != .move);
+ try std.testing.expect(!p.panes[second].?.tag_edit);
p.update(.{ .mouse = mouse(layer, edge - 0.1, .release) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .press) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .release) });
@@ -201,7 +207,7 @@ const TagNavigationTests = struct {
_ = try p.render(frame.allocator());
const rect = p.rects[0];
const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
- ClickCursorTests.click(p, .left, rect.x + config.GUTTER + 6, y);
+ ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + 6, y);
try std.testing.expectEqual(@as(usize, 0), p.active);
try std.testing.expectEqual(@as(u16, 6), first.tag_col);
key(p, Key.escape);
@@ -948,14 +954,14 @@ const TagNameTintTests = struct {
const base = std.mem.lastIndexOfScalar(u8, name, '/') orelse return error.MissingDirectory;
const rect = p.rects[id];
const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
- const left = rect.x + config.GUTTER;
+ const left = rect.x + pardes.TAG_TEXT_INSET;
var visible_name: usize = 0;
var at: usize = 0;
while (at < text.len) : (at = modal.nextGrapheme(text, at)) {
const start: i32 = @as(i32, @intCast(panes.File.rawDisplayCol(text, at))) - pane.tag_scroll;
const end = start + @as(i32, @intCast(panes.File.displayWidth(text[at..modal.nextGrapheme(text, at)])));
var col = @max(0, start);
- while (col < end and col < rect.w - config.GUTTER) : (col += 1) {
+ while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) {
const in_name = at > base and at < name.len;
const fg = if (in_name) name_fg else tag_fg;
try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg);
@@ -1009,7 +1015,7 @@ const TagNameTintTests = struct {
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
const surface = try p.render(arena.allocator());
- const x = p.rects[0].x + config.GUTTER + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll));
+ const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll));
const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y;
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x, y).style.fg);
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x + 1, y).style.fg);
@@ -1032,7 +1038,7 @@ const TagNameTintTests = struct {
for ([_]bool{ false, true }) |active| {
p.settings.focus_tint = active;
const surface = try p.render(arena.allocator());
- const x = p.rects[0].x + config.GUTTER;
+ const x = p.rects[0].x + pardes.TAG_TEXT_INSET;
const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y;
const fg = if (active) active_fg else tag_fg;
try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 1, y).style.fg);
@@ -1139,7 +1145,7 @@ test "middle clicking Save commits the staged filename before issuing a save" {
_ = try p.render(arena.allocator());
const text = try p.tagText(arena.allocator(), pane);
const save_at = std.mem.indexOf(u8, text, "Save").?;
- const x: u16 = @intCast(p.rects[0].x + config.GUTTER + save_at - pane.tag_scroll);
+ const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + save_at - pane.tag_scroll);
const y = p.rects[0].y;
while (p.nextEffect()) |_| {}
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } });
@@ -1211,7 +1217,7 @@ test "long pane tags reveal commands and map visible clicks to whole tag address
const text = try p.tagText(p.gpa, pane);
defer p.gpa.free(text);
const offset = std.mem.indexOf(u8, text, "SyntaxBold").?;
- const x: u16 = @intCast(p.rects[0].x + config.GUTTER + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll);
+ const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll);
const y = p.rects[0].y;
const before = p.settings.syntax_bold;
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } });
diff --git a/test/web-snapshots/touch.mjs b/test/web-snapshots/touch.mjs
index b78cbdc0..1351712e 100644
--- a/test/web-snapshots/touch.mjs
+++ b/test/web-snapshots/touch.mjs
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
function tags(frame) {
// This fixture has workspace and column bars in its first two rows.
- return frame.styleRows.flatMap((cells, y) => y > 1 && /^\s{2}\S/.test(frame.lines[y]) && cells.some((cell) => cell[2] & 128) ? [y] : []);
+ return frame.styleRows.flatMap((cells, y) => y > 1 && /^\s{3}\S/.test(frame.lines[y]) && cells.some((cell) => cell[2] & 128) ? [y] : []);
}
function pane(frame, name) {
@@ -95,7 +95,8 @@ export default async function ({ grid, touch, wait, stable, delay, evaluate, mou
tagValues[10]=0x556677;
renderer.render();
result.tag.bgInvalidates=renderer.tagRoot.firstChild!==tag;
- tagValues[1]=1;tagValues[4]=2;tagValues[5]=0;
+ wasm.pardes_tag_text_inset=()=>3;
+ tagValues[1]=1;tagValues[4]=3;tagValues[5]=0;
bytes[0]=32;data.setUint32(8,0x778899,true);
renderer.render();
const columnGrip=renderer.tagRoot.querySelector('[data-tag-grip]');