summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 16:28:57 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit1ab4c071be58033d5452f69797ca8e2c32240cff (patch)
treeafa108a1c372276da1d2c62b6c05d8f167cc81b8 /src/pardes.zig
parentc6ab9db0fdbcb8d16f666749194bb111218b96a8 (diff)
downloadpardes-1ab4c071be58033d5452f69797ca8e2c32240cff.tar.gz
pardes-1ab4c071be58033d5452f69797ca8e2c32240cff.zip
Separate tag text geometry from physical pane grips
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig263
1 files changed, 225 insertions, 38 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 294e6812..56f95040 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -71,6 +71,10 @@ pub const Pane = panes.Pane;
pub const MAX_PANES = 16;
pub const BodyLayer = @import("body_layer.zig").Layer;
pub const RowMetrics = @import("body_layer.zig").Metrics;
+pub const TagLayer = @import("tag_layer.zig").Layer;
+pub const TagHit = @import("tag_layer.zig").Hit;
+pub const TagKind = @import("tag_layer.zig").Kind;
+pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1;
pub const PDF_PAGE_GAP_PX = panes.Pdf.page_gap_px;
pub const MAX_COLS = 6;
const column_weight_unit = layout.column_weight_unit;
@@ -257,6 +261,7 @@ pub const Loc = struct {
test {
_ = @import("body_layer.zig");
+ _ = @import("tag_layer.zig");
}
const pane_tail = " " ++ config.pane_builtins_str;
@@ -3609,6 +3614,7 @@ pub const Surface = struct {
pub const PointerShape = enum(u8) { arrow, link };
pointer_shape: PointerShape = .arrow,
body_layers: [MAX_PANES]BodyLayer = @splat(.{}),
+ tag_layers: [MAX_TAG_LAYERS]TagLayer = @splat(.{}),
cols: u16 = 0,
rows: u16 = 0,
cells: []Cell = &.{},
@@ -3620,8 +3626,13 @@ pub const Surface = struct {
npanel_tracks: usize = 0,
previous_cells: []const Cell = &.{},
previous_body_layers: []const BodyLayer = &.{},
+ previous_tag_layers: []const TagLayer = &.{},
cell_diffs: []const PanelCellDiff = &.{},
+ pub fn tagLayers(s: *const Surface) []const TagLayer {
+ return &s.tag_layers;
+ }
+
pub fn bodyLayers(s: *const Surface) []const BodyLayer {
return &s.body_layers;
}
@@ -4525,6 +4536,7 @@ pub const Mouse = struct {
row: u16,
ctrl: bool = false,
body_hit: ?BodyHit = null,
+ tag_hit: ?TagHit = null,
};
pub const Key = struct {
@@ -4708,6 +4720,7 @@ const LookHoverWait = struct {
serial: u32,
frames: u16 = 0,
body_hit: ?Mouse.BodyHit = null,
+ tag_hit: ?TagHit = null,
};
const LookHoverPreview = struct {
@@ -4718,6 +4731,7 @@ const LookHoverPreview = struct {
sel: ?Pane.Sel,
file_word: ?FileWordSpan = null,
body_hit: ?Mouse.BodyHit = null,
+ tag_hit: ?TagHit = null,
};
const FileWordSpan = struct {
@@ -5913,6 +5927,7 @@ pub const Pardes = struct {
pointer_raw_col: u16 = 0,
pointer_raw_row: u16 = 0,
pointer_body_hit: ?Mouse.BodyHit = null,
+ pointer_tag_hit: ?TagHit = null,
raw_hover_intent: bool = false,
pointer_inside: bool = false,
look_hover_wait: ?LookHoverWait = null,
@@ -6144,6 +6159,7 @@ pub const Pardes = struct {
p.frame_arena.deinit();
gpa.free(p.surface.cells);
for (&p.surface.body_layers) |*layer| gpa.free(layer.cells);
+ for (&p.surface.tag_layers) |*layer| gpa.free(layer.cells);
p.presentation.deinit(gpa);
if (p.surface.images.len > 0) gpa.free(p.surface.images);
gpa.destroy(p);
@@ -6201,7 +6217,7 @@ pub const Pardes = struct {
return;
};
const text = p.headerText();
- const width = 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] 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);
@@ -7072,6 +7088,7 @@ pub const Pardes = struct {
.pointer_leave => {
p.pointer_inside = false;
p.pointer_body_hit = null;
+ p.pointer_tag_hit = null;
},
.tick => {
p.chrome_animation.advance();
@@ -7303,7 +7320,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 = @max(1, p.rects[id].w -| config.GUTTER);
+ const width = p.tagCapacity(p.rects[id].w -| config.GUTTER);
// 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);
@@ -10980,7 +10997,7 @@ pub const Pardes = struct {
return p.reprojectBodyHit(mapped_hit);
}
- fn pointerTextSelection(p: *const Pardes, id: usize, col: u16, row: u16, body_hit: ?Mouse.BodyHit) ?PointerTextSelection {
+ fn pointerTextSelection(p: *const Pardes, id: usize, col: u16, row: u16, body_hit: ?Mouse.BodyHit, tag_hit: ?TagHit) ?PointerTextSelection {
if (p.panes[id] == null) return null;
const r = p.rects[id];
if (col < r.x + config.GUTTER or col >= r.x + r.w or row < r.y or row >= r.y + r.h) return null;
@@ -10993,7 +11010,8 @@ pub const Pardes = struct {
const v = @as(i32, mapped_hit.row) + BOX_H;
return .{ .sel = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v }, .on_tag = false };
};
- const c: i32 = @as(i32, col) - @as(i32, r.x + config.GUTTER) +
+ 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)) +
(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 .{
@@ -11022,28 +11040,50 @@ pub const Pardes = struct {
return null;
}
+ fn sameTagCell(a: ?TagHit, b: ?TagHit) bool {
+ const first = a orelse return b == null;
+ const second = b orelse return false;
+ return first.kind == second.kind and first.id == second.id and first.serial == second.serial and first.col == second.col;
+ }
+
+ fn tagColumn(p: *const Pardes, kind: TagKind, id: usize, supplied: ?TagHit, clamp: bool) ?u16 {
+ var point = supplied orelse return null;
+ if (point.kind != kind or point.id != id) return null;
+ if (clamp) for (p.surface.tagLayers()) |*layer| {
+ if (layer.cols == 0 or layer.kind != kind or layer.id != id or layer.serial != point.serial) continue;
+ const bw: f32 = @floatFromInt(point.metrics.body_w);
+ const bh: f32 = @floatFromInt(point.metrics.body_h);
+ const left = @as(f32, @floatFromInt(layer.viewport.x)) * bw;
+ const right = @as(f32, @floatFromInt(layer.viewport.x + layer.viewport.w)) * bw;
+ point.pixel_x = std.math.clamp(point.pixel_x, left, @max(left, right - 0.001));
+ point.pixel_y = (@as(f32, @floatFromInt(layer.viewport.y)) + 0.5) * bh;
+ break;
+ };
+ return if (p.reprojectTagHit(point)) |mapped| mapped.col else null;
+ }
+
fn sameBodyCell(a: ?Mouse.BodyHit, b: ?Mouse.BodyHit) bool {
const first = a orelse return b == null;
const second = b orelse return false;
return first.serial == second.serial and first.row == second.row and first.col == second.col;
}
- fn noteLookHover(p: *Pardes, col: u16, row: u16, body_hit: ?Mouse.BodyHit) void {
+ fn noteLookHover(p: *Pardes, col: u16, row: u16, body_hit: ?Mouse.BodyHit, tag_hit: ?TagHit) void {
const delay = config.look_preview_delay_frames orelse return;
_ = delay;
const id = p.paneAt(col, row) orelse return p.cancelLookHover();
const pane = p.panes[id] orelse return p.cancelLookHover();
- _ = p.pointerTextSelection(id, col, row, body_hit) orelse return p.cancelLookHover();
+ _ = p.pointerTextSelection(id, col, row, body_hit, tag_hit) orelse return p.cancelLookHover();
if (comptime pdf_enabled) if (p.pdf_hover_preview) |shown|
if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial) return;
if (p.look_hover_preview) |shown|
- if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial and sameBodyCell(shown.body_hit, body_hit)) return;
+ if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial and sameBodyCell(shown.body_hit, body_hit) and sameTagCell(shown.tag_hit, tag_hit)) return;
if (p.look_hover_wait) |waiting|
- if (waiting.col == col and waiting.row == row and waiting.pane == id and waiting.serial == pane.serial and sameBodyCell(waiting.body_hit, body_hit)) return;
+ if (waiting.col == col and waiting.row == row and waiting.pane == id and waiting.serial == pane.serial and sameBodyCell(waiting.body_hit, body_hit) and sameTagCell(waiting.tag_hit, tag_hit)) return;
p.cancelLookHover();
- p.look_hover_wait = .{ .col = col, .row = row, .pane = id, .serial = pane.serial, .body_hit = body_hit };
+ p.look_hover_wait = .{ .col = col, .row = row, .pane = id, .serial = pane.serial, .body_hit = body_hit, .tag_hit = tag_hit };
}
fn refreshLookHoverFromRaw(p: *Pardes) void {
@@ -11056,7 +11096,7 @@ pub const Pardes = struct {
p.bodyHitForPane(p.paneAt(mapped.col, mapped.row) orelse return p.cancelLookHover(), mapped_hit, false) orelse return p.cancelLookHover()
else
null;
- p.noteLookHover(mapped.col, mapped.row, body_hit);
+ p.noteLookHover(mapped.col, mapped.row, body_hit, p.pointer_tag_hit);
}
fn advanceLookHover(p: *Pardes) void {
@@ -11066,7 +11106,7 @@ pub const Pardes = struct {
if (waiting.frames < delay) return;
const pane = p.panes[waiting.pane] orelse return p.cancelLookHover();
if (pane.serial != waiting.serial) return p.cancelLookHover();
- const pointed = p.pointerTextSelection(waiting.pane, waiting.col, waiting.row, waiting.body_hit) orelse
+ const pointed = p.pointerTextSelection(waiting.pane, waiting.col, waiting.row, waiting.body_hit, waiting.tag_hit) orelse
return p.cancelLookHover();
if (comptime pdf_enabled) if (!pointed.on_tag and panes.Pdf.paneNativeReady(p, pane)) {
const probe = panes.Pdf.probeAt(p, pane, waiting.col, waiting.row) orelse
@@ -11095,6 +11135,7 @@ pub const Pardes = struct {
.sel = preview,
.file_word = operand.file_word,
.body_hit = waiting.body_hit,
+ .tag_hit = waiting.tag_hit,
};
p.look_hover_wait = null;
}
@@ -11133,6 +11174,24 @@ pub const Pardes = struct {
p.pointer_raw_row = raw_row;
const previous_hit = p.pointer_body_hit;
p.pointer_body_hit = m.body_hit;
+ 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 == .tag or p.drag == .select or p.header_drag)) {
+ 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));
+ continued.pixel_y = (@as(f32, @floatFromInt(raw_row)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_h));
+ p.pointer_tag_hit = continued;
+ }
+ }
+ if ((m.kind == .drag or m.kind == .release) and (p.drag == .tag or p.drag == .select or p.header_drag)) {
+ if (previous_tag_hit) |previous| if (p.pointer_tag_hit) |*current| {
+ current.kind = previous.kind;
+ current.id = previous.id;
+ current.serial = previous.serial;
+ };
+ }
// A drag can leave the compact text viewport. Preserve its metrics so
// the endpoint still clamps to that body's last visible logical row.
if (m.body_hit == null and (m.kind == .drag or m.kind == .release) and p.drag == .select) {
@@ -11175,7 +11234,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] else 0;
const bar = p.headerText();
- p.topbar_col = @intCast(@min(bar.len, panes.File.rawAtDisplay(bar, (mcol -| x) + p.header_scroll)));
+ 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;
return;
}
@@ -11206,7 +11265,7 @@ pub const Pardes = struct {
}
p.header_column = column;
const bar = p.headerText();
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (mcol -| x) + p.header_scroll));
+ const at = @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.button == config.select_button) {
p.topbar_col = @intCast(at);
p.header_anchor = @intCast(at);
@@ -11328,13 +11387,13 @@ pub const Pardes = struct {
pane.scrollBy(-(@as(i32, mrow) - @as(i32, body_y)));
} else if (mrow >= tag_y and mrow < tag_y + BOX_H) {
p.active = id;
- p.enterTagEdit(pane, @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 + config.GUTTER));
pane.tag_anchor = pane.tag_col;
p.drag = .{ .tag = .{ .id = id } };
} else {
p.active = id;
if (pane.tag_edit) exitTagEdit(pane);
- const pointed = p.pointerTextSelection(id, mcol, mrow, m.body_hit) orelse return;
+ const pointed = p.pointerTextSelection(id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return;
pane.clearPointerSelection(sel_slot);
pane.sel[sel_slot] = pointed.sel;
p.drag = .{ .select = .{
@@ -11354,7 +11413,7 @@ pub const Pardes = struct {
},
.drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
.release => {
- if (p.pointer_body_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) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
p.dragRelease(config.select_button);
},
.motion => {},
@@ -11390,7 +11449,7 @@ pub const Pardes = struct {
const local = @as(i32, mrow) - @as(i32, body_y);
if (m.button == config.look_button) p.active = id;
pane.scrollBy(if (m.button == config.look_button) local else -local);
- } else if (p.pointerTextSelection(id, mcol, mrow, m.body_hit)) |pointed| {
+ } else if (p.pointerTextSelection(id, mcol, mrow, m.body_hit, p.pointer_tag_hit)) |pointed| {
if (m.button == config.look_button) p.active = id;
pane.clearPointerSelection(@intFromEnum(m.button));
pane.sel[@intFromEnum(m.button)] = pointed.sel;
@@ -11414,7 +11473,7 @@ pub const Pardes = struct {
},
.drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
.release => {
- if (p.pointer_body_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) and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
p.dragRelease(m.button);
},
.motion => {},
@@ -11469,6 +11528,11 @@ pub const Pardes = struct {
pane.sel[b].r1 = @as(i32, mapped_hit.row) + BOX_H;
return;
};
+ if (pane.sel[b].r0 < BOX_H) if (p.tagColumn(.pane, s.id, p.pointer_tag_hit, true)) |value| {
+ pane.sel[b].c1 = @as(i32, value) + pane.tag_scroll;
+ pane.sel[b].r1 = 0;
+ return;
+ };
pane.sel[b].c1 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
if (p.settings.tag_bottom) {
const body_h = r.h -| BOX_H;
@@ -11484,7 +11548,7 @@ pub const Pardes = struct {
},
.tag => |d| {
const pane = p.panes[d.id] orelse return;
- const c = @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 + config.GUTTER);
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;
@@ -12945,6 +13009,24 @@ pub const Pardes = struct {
p.invalidateThemeDependentRasters();
}
+ pub fn reprojectTagHit(p: *const Pardes, tag_hit: TagHit) ?TagHit {
+ for (p.surface.tagLayers()) |*layer| {
+ if (layer.cols == 0 or layer.kind != tag_hit.kind or layer.id != tag_hit.id or layer.serial != tag_hit.serial) continue;
+ if (layer.kind == .pane) {
+ if (layer.id >= p.panes.len) return null;
+ const pane = p.panes[layer.id] orelse return null;
+ if (pane.serial != 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;
+ }
+
+ fn tagCapacity(p: *const Pardes, physical_width: u16) u16 {
+ const layer: TagLayer = .{ .viewport = .{ .x = 0, .y = 0, .w = physical_width, .h = 1 } };
+ return layer.capacity(p.row_metrics);
+ }
+
pub fn reprojectBodyHit(p: *const Pardes, body_hit: Mouse.BodyHit) ?Mouse.BodyHit {
for (&p.surface.body_layers) |*layer| {
if (layer.rows == 0 or layer.serial != body_hit.serial) continue;
@@ -12977,10 +13059,12 @@ pub const Pardes = struct {
}
};
for (&s.body_layers) |*layer| layer.rows = 0;
+ for (&s.tag_layers) |*layer| layer.cols = 0;
s.nimages = 0;
s.npanel_tracks = 0;
s.previous_cells = &.{};
s.previous_body_layers = &.{};
+ s.previous_tag_layers = &.{};
s.cell_diffs = &.{};
const chrome = p.chromeTheme();
const th = p.theme(); // the message row paints in the editor's colours
@@ -13116,6 +13200,8 @@ pub const Pardes = struct {
if (caret < w) s.cursor = .{ .x = x + @as(u16, @intCast(caret)), .y = y, .bar = p.header_insert };
}
+ try p.renderTagLayers(arena);
+
switch (p.drag) {
.border_v => |d| {
if (d.cur_x < s.cols) {
@@ -13428,6 +13514,90 @@ pub const Pardes = struct {
}
}
+ fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
+ const chrome = p.chromeTheme();
+ 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;
+ 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 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);
+ @memset(storage, .{});
+ var temporary: Surface = .{ .cols = columns + config.GUTTER, .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| {
+ std.mem.swap(Surface, &p.surface, &temporary);
+ return err;
+ };
+ std.mem.swap(Surface, &p.surface, &temporary);
+ @memcpy(layer.cells, storage[config.GUTTER..]);
+ 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.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;
+ }
+ 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], .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);
+ };
+ }
+
+ 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];
+ 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;
+ layer.id = id;
+ layer.serial = 0;
+ layer.viewport = viewport;
+ layer.cols = columns;
+ layer.bg = bg;
+ layer.cursor = null;
+ var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells };
+ const editing = p.topbar_col != null and (if (kind == .workspace) p.header_column == null else p.header_column == id);
+ const scroll = if (editing) p.header_scroll else 0;
+ surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ _ = surface.print(0, 0, columns, text[panes.File.rawAtDisplay(text, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
+ if (editing) {
+ const cursor = p.topbar_col.?;
+ const anchor = p.header_anchor orelse cursor;
+ var col = panes.File.rawDisplayCol(text, @min(cursor, anchor)) -| scroll;
+ const end = panes.File.rawDisplayCol(text, @max(cursor, anchor)) -| scroll;
+ while (col < end and col < columns) : (col += 1) {
+ surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
+ const caret = panes.File.rawDisplayCol(text, cursor) -| scroll;
+ if (caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = p.header_insert };
+ } else if (p.pointer_inside) {
+ const tag_hit = p.pointer_tag_hit;
+ const col: ?usize = if (tag_hit) |pointer|
+ (if (pointer.kind == kind and pointer.id == id) pointer.col else null)
+ else if (p.hover_row == viewport.y and p.hover_col >= viewport.x and p.hover_col - viewport.x < viewport.w)
+ p.hover_col - viewport.x
+ else
+ null;
+ if (col) |hover| if (wordBoundsAtCol(text, panes.File.rawAtDisplay(text, hover + scroll))) |bounds| {
+ var cell = panes.File.rawDisplayCol(text, bounds.lo) -| scroll;
+ const end = panes.File.rawDisplayCol(text, bounds.hi) -| scroll;
+ while (cell < end and cell < columns) : (cell += 1) {
+ surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg };
+ surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg };
+ }
+ };
+ }
+ }
+
fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize) !void {
if (!p.settings.tree_context_tag_style or pane.context_rows == 0 or pane.file == null or pane.collapsed) return;
if (r.w <= config.GUTTER or r.h <= BOX_H) return;
@@ -13491,7 +13661,6 @@ 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 pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
- const page_rgb = th.bg orelse th.tag_bg;
// text area resets to terminal-default cells (vaxis clear semantics);
// light themes paint the page over it.
@@ -13521,6 +13690,36 @@ pub const Pardes = struct {
.tty => config.tag_tty,
}, box_style);
+ try p.paintPaneTag(arena, pane, r, id, active, p.tagCapacity(tw));
+
+ // Folded panes retain their body state, but neither body pixels nor
+ // its cursor belong outside the one-row tag rectangle.
+ if (pane.collapsed) return;
+
+ if (comptime pdf_enabled)
+ if (pane.hasPdf() and panes.Pdf.draw(p, pane, r, id, tx, tw)) return;
+
+ if (pane.image) |*iv| {
+ panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y, tw, r.h -| BOX_H);
+ // thumbless, but the same one column as the real scrollbar below —
+ // that is the whole point of drawing it
+ s.fill(r.x, body_y, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, body_y, 1, r.h -| BOX_H, .{ .bg = pane_bg });
+ return;
+ }
+
+ try p.renderBody(arena, pane, r, id, active, true);
+ }
+
+ fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, capacity: u16) !void {
+ 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 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;
// Focus is visible along the tag without changing its live text grid.
const tag_bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
const tag_fg = if (active and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg;
@@ -13537,10 +13736,10 @@ pub const Pardes = struct {
const caret = panes.File.rawDisplayCol(tag, pane.tag_col);
var scroll: usize = pane.tag_scroll;
if (caret < scroll) scroll = caret;
- if (caret >= scroll + tw) scroll = caret - tw + 1;
+ if (caret >= scroll + capacity) scroll = caret - capacity + 1;
var raw = panes.File.rawAtDisplay(tag, scroll);
scroll = panes.File.rawDisplayCol(tag, raw);
- if (caret >= scroll + tw) {
+ if (caret >= scroll + capacity) {
raw = modal.nextGrapheme(tag, raw);
scroll = panes.File.rawDisplayCol(tag, raw);
}
@@ -13588,23 +13787,11 @@ pub const Pardes = struct {
if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = pane.mode == .insert };
}
- // Folded panes retain their body state, but neither body pixels nor
- // its cursor belong outside the one-row tag rectangle.
- if (pane.collapsed) return;
-
- if (comptime pdf_enabled)
- if (pane.hasPdf() and panes.Pdf.draw(p, pane, r, id, tx, tw)) return;
-
- if (pane.image) |*iv| {
- panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y, tw, r.h -| BOX_H);
- // thumbless, but the same one column as the real scrollbar below —
- // that is the whole point of drawing it
- s.fill(r.x, body_y, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, body_y, 1, r.h -| BOX_H, .{ .bg = pane_bg });
- return;
+ for (pane.sel, 0..) |selection, button| {
+ if (selection.state == .none or (selection.state == .done and button != 0)) continue;
+ if (@min(selection.r0, selection.r1) >= BOX_H) continue;
+ p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, selection, th.sel_bg, th.sel_fg);
}
-
- try p.renderBody(arena, pane, r, id, active, true);
}
fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, scrollbar: bool) !void {