summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig128
1 files changed, 106 insertions, 22 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 25f4e18e..90658589 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2948,6 +2948,9 @@ pub const Theme = struct {
tag_name_fg: ?[3]u8 = null,
/// Active tint; absence preserves the shared name tint or active tag ink.
tag_active_name_fg: ?[3]u8 = null,
+ /// Column grips stay distinct from pane grips in both focus states.
+ column_box: ?[3]u8 = null,
+ column_box_dim: ?[3]u8 = null,
border: ?[3]u8 = null,
search_bg: ?[3]u8 = null,
search_fg: ?[3]u8 = null,
@@ -3175,15 +3178,46 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" {
try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_active_bg);
try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_name_fg);
try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_active_name_fg);
+ try std.testing.expectEqual(@as(?[3]u8, null), parsed.column_box);
+ try std.testing.expectEqual(@as(?[3]u8, null), parsed.column_box_dim);
try std.testing.expectEqual(@as(?[3]u8, null), parsed.search_bg);
const chrome = ChromeTheme.fromTheme(&parsed);
try std.testing.expectEqual(parsed.tag_bg, chrome.tag_active_bg);
try std.testing.expectEqual(parsed.tag_fg, chrome.tag_name_fg);
try std.testing.expectEqual(chrome.tag_active_fg, chrome.tag_active_name_fg);
+ try std.testing.expectEqual(parsed.num, chrome.column_box);
+ try std.testing.expectEqual(mix(parsed.num, parsed.tag_bg), chrome.column_box_dim);
try std.testing.expectEqual(parsed.sel_bg, chrome.search_bg);
try std.testing.expectEqual(parsed.fg.?, chrome.diagnostic_error);
}
+test "column grip colors remain distinct from pane grips across native themes" {
+ for (themes[0..native_theme_count]) |th| {
+ try std.testing.expect(th.column_box != null and th.column_box_dim != null);
+ const chrome = ChromeTheme.fromTheme(&th);
+ for ([_][2][3]u8{
+ .{ chrome.box, chrome.column_box },
+ .{ chrome.box_dim, chrome.column_box_dim },
+ }) |pair| {
+ var distance: u32 = 0;
+ for (pair[0], pair[1]) |a, b| {
+ const delta = @as(i32, a) - b;
+ distance += @intCast(delta * delta);
+ }
+ try std.testing.expect(distance >= 30 * 30);
+ }
+ }
+ var custom = themes[0];
+ custom.column_box = .{ 90, 130, 170 };
+ custom.column_box_dim = null;
+ var chrome = ChromeTheme.fromTheme(&custom);
+ try std.testing.expectEqual(custom.column_box.?, chrome.column_box);
+ try std.testing.expectEqual(mix(custom.column_box.?, custom.tag_bg), chrome.column_box_dim);
+ custom.column_box_dim = .{ 40, 60, 80 };
+ chrome = ChromeTheme.fromTheme(&custom);
+ try std.testing.expectEqual(custom.column_box_dim.?, chrome.column_box_dim);
+}
+
test "custom filename tints preserve optional active fallback precedence" {
var theme = themes[0];
theme.tag_name_fg = null;
@@ -3272,6 +3306,8 @@ pub const ChromeTheme = struct {
diagnostic_hint: [3]u8,
box: [3]u8,
box_dim: [3]u8,
+ column_box: [3]u8,
+ column_box_dim: [3]u8,
lineno: [3]u8,
scroll_track: [3]u8,
scroll_thumb: [3]u8,
@@ -3293,6 +3329,8 @@ pub const ChromeTheme = struct {
.diagnostic_hint = th.diagnostic_hint orelse (th.fg orelse th.tag_fg),
.box = th.box,
.box_dim = th.box_dim,
+ .column_box = th.column_box orelse th.num,
+ .column_box_dim = th.column_box_dim orelse mix(th.column_box orelse th.num, th.tag_bg),
.lineno = th.lineno,
.scroll_track = th.scroll_track,
.scroll_thumb = th.scroll_thumb,
@@ -4327,8 +4365,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 = 0, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = 0, .row = TOPBAR_H } });
+ 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(.{ .key = .{ .cp = 'é', .text = "é" } });
try std.testing.expect(std.mem.startsWith(u8, p.columnTag(0), "éNew"));
p.update(.{ .key = .{ .cp = Key.backspace } });
@@ -4373,8 +4411,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];
- const width = p.col_w[column];
+ const x = p.col_x[column] + config.GUTTER;
+ const width = p.col_w[column] - config.GUTTER;
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]);
@@ -4395,7 +4433,7 @@ test "column tags render edit hover and scroll from the column edge" {
try std.testing.expect(std.mem.startsWith(u8, p.columnTag(column), "Help Tty"));
const edited = try p.render(frame.allocator());
try std.testing.expectEqual(x + 4, edited.cursor.?.x);
- // The two reclaimed cells belong to text and caret reveal, too.
+ // Only the text viewport participates in caret reveal.
const long: [80]u8 = @splat('a');
try std.testing.expect(p.col_tags[column].set(long[0 .. width - 1]));
p.update(.{ .key = .{ .cp = Key.end } });
@@ -4427,8 +4465,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], .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = p.col_x[column], .row = TOPBAR_H } });
+ 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 } });
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);
@@ -4758,6 +4796,7 @@ const Drag = union(enum) {
border_v: struct { left_col: usize, cur_x: u16, corner: ?struct { col: usize, idx: usize } = null, cur_y: u16 = 0 },
border_h: struct { col: usize, top_idx: usize, cur_y: u16 },
move: struct { id: usize, cur_x: u16, cur_y: u16 },
+ column_move: struct { column: usize, serial: u32, cur_x: u16 },
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
@@ -4774,7 +4813,7 @@ const Drag = union(enum) {
fn button(drag: Drag) ?Mouse.Button {
return switch (drag) {
.none => null,
- .border_v, .border_h, .move, .tag => config.select_button,
+ .border_v, .border_h, .move, .column_move, .tag => config.select_button,
.select => |selection| selection.button,
};
}
@@ -5891,6 +5930,8 @@ pub const Pardes = struct {
col_panes: [MAX_COLS][MAX_PANES]usize = undefined,
col_n: [MAX_COLS]usize = @splat(0),
col_tags: [MAX_COLS]TagLine = @splat(.{}),
+ col_serial: [MAX_COLS]u32 = @splat(0),
+ next_column_serial: u32 = 1,
global_tag: TagLine = .{},
// derived each sync
rects: [MAX_PANES]Rect = @splat(.{}),
@@ -6217,7 +6258,7 @@ pub const Pardes = struct {
return;
};
const text = p.headerText();
- const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] else p.screen_w);
+ const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] -| config.GUTTER 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);
@@ -11231,8 +11272,18 @@ pub const Pardes = struct {
p.chord_arg = null;
}
}
+ if (mrow >= TOPBAR_H and mrow < TOPBAR_H + 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;
+ if (m.button == config.select_button) {
+ p.header_drag = false;
+ p.drag = .{ .column_move = .{ .column = column, .serial = p.col_serial[column], .cur_x = mcol } };
+ }
+ return;
+ }
+ }
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] else 0;
+ const x = if (p.header_column) |c| p.col_x[c] + config.GUTTER 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;
@@ -11249,7 +11300,7 @@ 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];
+ x = p.col_x[c] + config.GUTTER;
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];
}
@@ -11413,7 +11464,7 @@ pub const Pardes = struct {
},
.drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
.release => {
- if ((p.pointer_body_hit != null or p.pointer_tag_hit != null) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
+ if ((p.pointer_body_hit != null or p.pointer_tag_hit != null or p.drag == .column_move) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
p.dragRelease(config.select_button);
},
.motion => {},
@@ -11473,7 +11524,7 @@ pub const Pardes = struct {
},
.drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
.release => {
- if ((p.pointer_body_hit != null or p.pointer_tag_hit != null) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
+ if ((p.pointer_body_hit != null or p.pointer_tag_hit != null or p.drag == .column_move) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
p.dragRelease(m.button);
},
.motion => {},
@@ -11514,6 +11565,7 @@ pub const Pardes = struct {
d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow, p.settings.tag_bottom);
} else d.cur_y = mrow;
},
+ .column_move => |*d| d.cur_x = mcol,
.move => |*d| {
d.cur_x = mcol;
d.cur_y = mrow;
@@ -11701,6 +11753,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|
+ layout.reorderColumn(p, column, layout.columnMoveTarget(p, column, d.cur_x));
+ },
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
// a file moved into the left column evicts a lone unused shell
@@ -12661,6 +12717,7 @@ pub const Pardes = struct {
pane.collapsed = src.collapsed;
}
p.ncol = @min(st.columns.len, MAX_COLS);
+ p.col_serial = @splat(0);
if (st.topbar_custom) |bar| _ = p.global_tag.set(bar);
for (st.columns[0..p.ncol], 0..) |col, c| {
if (col.tag) |bar| _ = p.col_tags[c].set(bar);
@@ -13017,6 +13074,7 @@ pub const Pardes = struct {
const pane = p.panes[layer.id] orelse return null;
if (pane.serial != layer.serial) return null;
}
+ if (layer.kind == .column and (layer.id >= p.ncol or p.col_serial[layer.id] != layer.serial)) return null;
return layer.hitAt(tag_hit.pixel_x, tag_hit.pixel_y, @floatFromInt(tag_hit.metrics.body_w), @floatFromInt(tag_hit.metrics.body_h), @floatFromInt(tag_hit.metrics.tagline_w), @floatFromInt(tag_hit.metrics.tagline_h));
}
return null;
@@ -13156,12 +13214,19 @@ pub const Pardes = struct {
if (col < s.cols) s.cursor = .{ .x = @intCast(col), .y = 0, .bar = p.header_insert };
};
if (p.columnBarHeight() > 0) for (0..p.ncol) |c| {
- const x = p.col_x[c];
- const w = p.col_w[c];
- if (w == 0 or x >= s.cols) continue;
+ const grip = @min(config.GUTTER, p.col_w[c]);
+ const x = p.col_x[c] + grip;
+ const w = p.col_w[c] - grip;
+ 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;
const fg = if (active_col and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg;
+ const grip_bg = if (active_col) 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 };
+ s.fill(p.col_x[c], TOPBAR_H, grip, 1, grip_style);
+
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;
const tag = p.columnTag(c);
@@ -13185,8 +13250,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] else 0;
- const w = if (p.header_column) |c| p.col_w[c] else s.cols;
+ 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 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;
@@ -13218,6 +13283,26 @@ 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 >= TOPBAR_H) {
+ layer.viewport.w -|= 1;
+ };
+ }
+ var row: u16 = TOPBAR_H;
+ while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
+ }
+ },
.move => |d| if (d.cur_x < s.cols) {
if (layout.movePlacement(p, d.id, d.cur_x, d.cur_y)) |placement| {
var col: u16 = p.col_x[placement.preview_col];
@@ -13547,7 +13632,7 @@ pub const Pardes = struct {
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], .y = TOPBAR_H, .w = p.col_w[column], .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(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);
};
}
@@ -13558,7 +13643,7 @@ pub const Pardes = struct {
if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
layer.kind = kind;
layer.id = id;
- layer.serial = 0;
+ layer.serial = if (kind == .column) p.col_serial[id] else 0;
layer.viewport = viewport;
layer.cols = columns;
layer.bg = bg;
@@ -13678,8 +13763,7 @@ pub const Pardes = struct {
const box_style: CellStyle = .{
.fg = .{ .rgb = box_ink },
.bg = .{ .rgb = box_bg },
- // The mode badge is the left end of the tagline, not body text.
- // Carry the same role so every GUI backend scales the whole row.
+ // Grips keep physical grid dimensions, independently of tag text.
.font_role = .tagline,
};
s.fill(r.x, tag_y, config.GUTTER, BOX_H, box_style);