summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/gui/gui.zig18
-rw-r--r--src/mouse.zig24
-rw-r--r--src/pardes.zig180
3 files changed, 150 insertions, 72 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 140ca1db..7bf45676 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5508,11 +5508,11 @@ const ResolvedCell = struct {
const topbarPaneBorderPixels = pardes.topbarPaneBorderPixels;
const taglineBandOffset = pardes.taglineBandOffset;
-fn topbarPaneBorderHeight(surface: *const pardes.Surface, cell_h: u32, tagline_h: u32) ?u32 {
- if (surface.rows <= pardes.TOPBAR_H) return null;
+fn topbarPaneBorderHeight(surface: *const pardes.Surface, top: u16, cell_h: u32, tagline_h: u32) ?u32 {
+ if (top == 0 or surface.rows <= top) return null;
const height = topbarPaneBorderPixels(cell_h, tagline_h);
if (height == 0) return null;
- const base = @as(usize, pardes.TOPBAR_H) * surface.cols;
+ const base = @as(usize, top) * surface.cols;
for (surface.cells[base..][0..surface.cols]) |cell|
if (!cell.default and cell.style.font_role == .tagline) return height;
return null;
@@ -5611,7 +5611,7 @@ fn cellInstanceCount(cell: *const pardes.Cell) u32 {
fn taglineBaseRgb(core: *const pardes.Pardes, col: u16, row: u16) [3]u8 {
const chrome = core.chromeTheme();
if (!core.settings.focus_tint) return chrome.tag_bg;
- if (row >= pardes.TOPBAR_H and row < pardes.TOPBAR_H + core.columnBarHeight()) {
+ if (row >= core.topBarHeight() and row < core.topBarHeight() + core.columnBarHeight()) {
for (0..core.ncol) |column| for (core.col_panes[column][0..core.col_n[column]]) |id| {
if (id != core.active) continue;
const x = core.col_x[column];
@@ -7275,8 +7275,10 @@ fn buildOverlay(
const win_h: f32 = @floatFromInt(sh);
var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = win_w, .win_h = win_h };
appendWorkspacePet(g, core, surface, &builder);
- if (topbarPaneBorderHeight(surface, g.cell_h, g.tagline_height)) |height| {
- const y0: f32 = @floatFromInt(g.cell_h - height / 2);
+ // The workspace tag grows a row per line, so the border follows its foot.
+ const top_rows: u16 = if (core) |p| p.topBarHeight() else pardes.TOPBAR_H;
+ if (topbarPaneBorderHeight(surface, top_rows, g.cell_h, g.tagline_height)) |height| {
+ const y0: f32 = @floatFromInt(top_rows * g.cell_h - height / 2);
const rgb = topbar_pane_border_rgb;
builder.addRect(0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), .{
.r = @as(f32, @floatFromInt(rgb[0])) / 255.0,
@@ -7286,7 +7288,7 @@ fn buildOverlay(
});
}
if (core) |p| if (p.columnBarHeight() > 0) {
- const y = @as(f32, @floatFromInt(pardes.TOPBAR_H + p.columnBarHeight())) * layout.h;
+ const y = @as(f32, @floatFromInt(top_rows + p.columnBarHeight())) * layout.h;
builder.addRect(0, y - 1, win_w, y, overlayRgb(p.chromeTheme().border));
};
const grid_bottom = @as(f32, @floatFromInt(surface.rows)) * layout.h;
@@ -7589,7 +7591,7 @@ fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w:
}
fn appendColumnSpines(core: *const pardes.Pardes, rows: u16, layout: CellLayout, builder: *OverlayBuilder) void {
- const y0 = @as(f32, @floatFromInt(pardes.TOPBAR_H)) * layout.h;
+ const y0 = @as(f32, @floatFromInt(core.topBarHeight())) * layout.h;
const grid_y1 = @as(f32, @floatFromInt(rows)) * layout.h;
// Down through the pixels past the last whole row, as tags run right.
const y1 = if (builder.win_h - grid_y1 < layout.h) @max(grid_y1, builder.win_h) else grid_y1;
diff --git a/src/mouse.zig b/src/mouse.zig
index 33646090..4b624dca 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -299,12 +299,15 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
return;
};
const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
- const bar = modal.lineSlice(edit.flatSurface(p, t) catch return, 0);
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tagColumn(p, if (p.header_column != null) .column else .workspace, p.header_column orelse 0, p.pointer_tag_hit, p.header_drag, 0) orelse (mcol -| x)) + p.header_scroll));
+ const text = edit.flatSurface(p, t) catch return;
+ const top = if (p.header_column != null) p.topBarHeight() else 0;
+ const line: u16 = @intCast(@min(mrow -| top, modal.lineCount(text) -| 1, (if (p.header_column != null) p.columnBarHeight() else p.topBarHeight()) -| 1));
+ const bar = modal.lineSlice(text, line);
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tagColumn(p, if (p.header_column != null) .column else .workspace, p.header_column orelse 0, p.pointer_tag_hit, p.header_drag, line) orelse (mcol -| x)) + p.header_scroll));
// the anchor stays where the press put it
- t.cur_row = 0;
+ t.cur_row = line;
t.cur_col = @intCast(at);
- t.vsel.active = t.vsel.col != t.cur_col;
+ t.vsel.active = t.vsel.row != t.cur_row or t.vsel.col != t.cur_col;
if (m.kind == .release) p.header_drag = false;
return;
}
@@ -330,19 +333,22 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
}
const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
const focused = p.header_focus and p.header_column == column;
- const bar = modal.lineSlice(edit.flatSurface(p, t) catch return, 0);
+ const text = edit.flatSurface(p, t) catch return;
+ const line: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0);
+ if (line > 0 and line >= modal.lineCount(text)) return;
+ const bar = modal.lineSlice(text, line);
const scroll = if (focused) p.header_scroll else 0;
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tagColumn(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, 0) orelse (mcol -| x)) + scroll));
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tagColumn(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, line) orelse (mcol -| x)) + scroll));
if (m.button == config.select_button) {
// A clicked header is typed straight into, as a clicked tag is.
if (!focused) p.enterHeader(column);
t.mode = .insert;
- t.cur_row = 0;
+ t.cur_row = line;
t.cur_col = @intCast(at);
- t.vsel = .{ .active = false, .row = 0, .col = @intCast(at) };
+ t.vsel = .{ .active = false, .row = line, .col = @intCast(at) };
p.header_drag = true;
} else {
- const span = if (focused) Pardes.headerSelection(t, bar) else null;
+ const span = if (focused) Pardes.headerSelection(t, text, line) else null;
const selected = if (span) |sp|
(if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at))
else
diff --git a/src/pardes.zig b/src/pardes.zig
index 72a282c2..d07539ba 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -252,10 +252,12 @@ pub const TagKind = @import("tag_layer.zig").Kind;
/// A tag shows at most this many lines; past them it is cut, like a line
/// too long for its width.
pub const MAX_TAG_ROWS = 8;
-pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max + MAX_PANES * (MAX_TAG_ROWS - 1);
+pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max + (MAX_PANES + MAX_COLS + 1) * (MAX_TAG_ROWS - 1);
pub const NOTICE_LAYER_BASE = MAX_PANES + MAX_COLS + 1;
/// Where the layers for a pane tag's second and later lines start.
pub const TAG_LINE_LAYER_BASE = NOTICE_LAYER_BASE + MAX_PANES * Pane.Notices.max;
+/// ...and those of a column's (by index) or the workspace's (after them).
+pub const HEADER_LINE_LAYER_BASE = TAG_LINE_LAYER_BASE + MAX_PANES * (MAX_TAG_ROWS - 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;
@@ -2854,6 +2856,39 @@ test "header clicks beyond text append and long UTF-8 tags reveal their caret" {
try std.testing.expectEqual(@as(usize, 0), p.header_scroll);
}
+test "a column tag with a newline in it takes a row per line and pushes its panes down" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true });
+ defer p.deinit();
+ _ = try p.setTestFile("body\n");
+ p.col_tags[0].own = try p.gpa.dupe(u8, "New");
+ p.acknowledgePanelPresentation(&.{});
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = 40, .row = TOPBAR_H } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = 40, .row = TOPBAR_H } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ p.update(.{ .key = .{ .cp = 'S', .text = "Snarf" } });
+ try std.testing.expectEqualStrings("New\nSnarf", p.columnTag(0));
+ try std.testing.expectEqual(@as(u16, 2), p.columnBarHeight());
+ const frame = try p.render(p.scratch.allocator());
+ try std.testing.expectEqual(TOPBAR_H + 2, p.rects[0].y);
+ try std.testing.expectEqual(TOPBAR_H + 1, frame.cursor.?.y);
+ try std.testing.expectEqual(@as(u8, 'S'), frame.at(TAG_TEXT_INSET, TOPBAR_H + 1).text[0]);
+
+ // A click on the second row lands on the second line.
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ p.acknowledgePanelPresentation(&.{});
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
+ try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_row);
+ try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_col);
+
+ // Taking the newline back out gives the panes their row again.
+ p.update(.{ .key = .{ .cp = Key.home } });
+ p.update(.{ .key = .{ .cp = Key.backspace } });
+ try std.testing.expectEqualStrings("NewSnarf", p.columnTag(0));
+ _ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqual(TOPBAR_H + COLBAR_H, p.rects[0].y);
+}
+
test "tiny screens keep pane space before adding a column tag" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 8, .rows = 2, .tty_only = true });
defer p.deinit();
@@ -4064,16 +4099,24 @@ pub const Pardes = struct {
return p.global_tag.own orelse p.headerDefault(.workspace);
}
- /// The workspace tag row's height: `TOPBAR_H` while the setting keeps
- /// it, zero once a native shell takes its commands over. Layout and hit
- /// testing read this rather than the constant, the way they read
+ /// The workspace tag's height: a row per line of it while the setting
+ /// keeps it, zero once a native shell takes its commands over. Layout and
+ /// hit testing read this rather than the constant, the way they read
/// `columnBarHeight`.
pub fn topBarHeight(p: *const Pardes) u16 {
- return if (p.settings.workspace_tag) TOPBAR_H else 0;
+ if (!p.settings.workspace_tag) return 0;
+ const lines = if (p.global_tag.own) |own| std.mem.count(u8, own, "\n") + 1 else TOPBAR_H;
+ return @intCast(@min(lines, MAX_TAG_ROWS, @max(TOPBAR_H, p.screen_h / 3)));
}
+ /// The column tags' height: a row per line of the tallest one.
pub fn columnBarHeight(p: *const Pardes) u16 {
- return if (p.settings.column_tags and p.screen_h >= 3) COLBAR_H else 0;
+ if (!p.settings.column_tags or p.screen_h < 3) return 0;
+ var lines: usize = COLBAR_H;
+ for (p.col_tags[0..p.ncol]) |t| if (t.own) |own| {
+ lines = @max(lines, std.mem.count(u8, own, "\n") + 1);
+ };
+ return @intCast(@min(lines, MAX_TAG_ROWS, @max(COLBAR_H, p.screen_h / 3)));
}
pub fn columnTag(p: *const Pardes, c: usize) []const u8 {
@@ -4093,7 +4136,7 @@ pub const Pardes = struct {
return;
};
const text = edit.flatSurface(p, t) catch return;
- const line = modal.lineSlice(text, 0);
+ const line = modal.lineSlice(text, @intCast(@max(0, t.cur_row)));
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(line, @min(@as(usize, @intCast(@max(0, t.cur_col))), line.len));
if (col < p.header_scroll) p.header_scroll = col;
@@ -6064,40 +6107,46 @@ pub const Pardes = struct {
}
}
- // global tagbar: full width, top row — unless a native shell has
- // taken its commands over, in which case the row is not ours to draw
+ // global tagbar: full width, top rows — unless a native shell has
+ // taken its commands over, in which case the rows are not ours to draw
p.revealHeader();
- const global_bar = modal.lineSlice(p.topbar(), 0);
+ const global = p.topbar();
+ const global_rows = p.topBarHeight();
const global_scroll = if (p.header_focus and p.header_column == null) p.header_scroll else 0;
if (p.settings.workspace_tag) {
- s.fill(0, 0, s.cols, TOPBAR_H, .{
- .bg = .{ .rgb = chrome.tag_bg },
- .font_role = .tagline,
- });
- _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{
- .fg = .{ .rgb = chrome.tag_fg },
+ s.fill(0, 0, s.cols, global_rows, .{
.bg = .{ .rgb = chrome.tag_bg },
.font_role = .tagline,
});
+ for (0..global_rows) |row| {
+ const line = modal.lineSlice(global, row);
+ _ = s.print(0, @intCast(row), s.cols, line[panes.File.rawAtDisplay(line, global_scroll)..], .{
+ .fg = .{ .rgb = chrome.tag_fg },
+ .bg = .{ .rgb = chrome.tag_bg },
+ .font_role = .tagline,
+ });
+ }
}
- if (p.pointer_inside and p.hover_row < p.topBarHeight()) {
- const bar = global_bar;
+ if (p.pointer_inside and p.hover_row < global_rows) {
+ const bar = modal.lineSlice(global, p.hover_row);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col + global_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| global_scroll;
const hi = panes.File.rawDisplayCol(bar, bounds.hi) -| global_scroll;
while (col < hi and col < s.cols) : (col += 1) {
- const cell = s.at(@intCast(col), 0);
+ const cell = s.at(@intCast(col), p.hover_row);
cell.default = false;
cell.style.bg = .{ .rgb = th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
}
}
}
- if (p.columnBarHeight() > 0) for (0..p.ncol) |c| {
+ const column_rows = p.columnBarHeight();
+ if (column_rows > 0) for (0..p.ncol) |c| {
const grip = @min(config.GUTTER, p.col_w[c]);
const inset = @min(TAG_TEXT_INSET, p.col_w[c]);
const x = p.col_x[c] + inset;
const w = p.col_w[c] - inset;
+ const y0 = global_rows;
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;
@@ -6109,25 +6158,28 @@ pub const Pardes = struct {
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], p.topBarHeight(), grip, 1, grip_style);
- s.fill(p.col_x[c] + grip, p.topBarHeight(), inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ s.fill(p.col_x[c], y0, grip, column_rows, grip_style);
+ s.fill(p.col_x[c] + grip, y0, inset - grip, column_rows, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
- s.fill(x, p.topBarHeight(), w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
+ s.fill(x, y0, w, column_rows, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
const bar_scroll = if (p.header_focus and p.header_column == c) p.header_scroll else 0;
- const tag = modal.lineSlice(p.columnTag(c), 0);
- _ = s.print(x, p.topBarHeight(), w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{
- .fg = .{ .rgb = fg },
- .bg = .{ .rgb = bg },
- .font_role = .tagline,
- });
+ const tag = p.columnTag(c);
+ for (0..column_rows) |row| {
+ const line = modal.lineSlice(tag, row);
+ _ = s.print(x, y0 + @as(u16, @intCast(row)), w, line[panes.File.rawAtDisplay(line, bar_scroll)..], .{
+ .fg = .{ .rgb = fg },
+ .bg = .{ .rgb = bg },
+ .font_role = .tagline,
+ });
+ }
// 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 = tag;
+ if (p.pointer_inside and p.drag != .column_move and p.hover_row >= y0 and p.hover_row < y0 + column_rows and p.hover_col >= x and p.hover_col < x + w) {
+ const bar = modal.lineSlice(tag, p.hover_row - y0);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll;
const hi = panes.File.rawDisplayCol(bar, bounds.hi) -| bar_scroll;
while (col < hi and col < w) : (col += 1) {
- const cell = s.at(x + @as(u16, @intCast(col)), p.topBarHeight());
+ const cell = s.at(x + @as(u16, @intCast(col)), p.hover_row);
cell.style.bg = .{ .rgb = th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
}
@@ -6135,22 +6187,26 @@ pub const Pardes = struct {
}
};
if (p.focusedHeader()) |t| {
- const bar = modal.lineSlice(try edit.flatSurface(p, t), 0);
+ const text = try edit.flatSurface(p, t);
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) p.topBarHeight() else 0;
- if (headerSelection(t, bar)) |span| {
+ const y: u16 = if (p.header_column != null) global_rows else 0;
+ const rows = if (p.header_column != null) column_rows else global_rows;
+ for (0..rows) |row| {
+ const bar = modal.lineSlice(text, row);
+ const span = headerSelection(t, text, row) orelse continue;
var col = panes.File.rawDisplayCol(bar, span.lo) -| p.header_scroll;
const hi = panes.File.rawDisplayCol(bar, span.hi) -| p.header_scroll;
while (col < hi and col < w) : (col += 1) {
- const cell = s.at(x + @as(u16, @intCast(col)), y);
+ const cell = s.at(x + @as(u16, @intCast(col)), y + @as(u16, @intCast(row)));
cell.style.fg = .{ .rgb = th.sel_fg };
cell.style.bg = .{ .rgb = th.sel_bg };
}
}
- if (t.cur_row == 0) {
+ if (t.cur_row >= 0 and t.cur_row < rows) {
+ const bar = modal.lineSlice(text, @intCast(t.cur_row));
const caret = panes.File.rawDisplayCol(bar, @min(@as(usize, @intCast(@max(0, t.cur_col))), bar.len)) -| p.header_scroll;
- if (caret < w) s.cursor = .{ .x = x + @as(u16, @intCast(caret)), .y = y, .bar = t.mode == .insert };
+ if (caret < w) s.cursor = .{ .x = x + @as(u16, @intCast(caret)), .y = y + @as(u16, @intCast(t.cur_row)), .bar = t.mode == .insert };
}
}
@@ -6439,8 +6495,12 @@ pub const Pardes = struct {
}
// With the workspace tag row handed to native chrome there is no
// workspace layer to render; the menu bar carries its commands.
- if (p.settings.workspace_tag)
- try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, modal.lineSlice(p.topbar(), 0), chrome.tag_bg, chrome.tag_fg);
+ if (p.settings.workspace_tag) {
+ for (0..p.topBarHeight()) |line| {
+ const index = if (line == 0) MAX_PANES + MAX_COLS else HEADER_LINE_LAYER_BASE + MAX_COLS * (MAX_TAG_ROWS - 1) + line - 1;
+ try p.renderHeaderLayer(arena, index, .workspace, 0, @intCast(line), .{ .x = 0, .y = @intCast(line), .w = p.screen_w, .h = TOPBAR_H }, modal.lineSlice(p.topbar(), line), chrome.tag_bg, chrome.tag_fg);
+ }
+ }
// Each notice is its own tagline-shaped chip over the top of the body.
// Going through the same layer machinery the pane and column tags use
// is what gives it the tagline's height, its small-font metrics, the
@@ -6483,7 +6543,7 @@ pub const Pardes = struct {
const fade = motion.alpha;
const page = p.theme().bg orelse chrome.tag_bg;
const layer_index = NOTICE_LAYER_BASE + id * Pane.Notices.max + i;
- try p.renderHeaderLayer(arena, layer_index, .notice, @intCast(id), .{
+ try p.renderHeaderLayer(arena, layer_index, .notice, @intCast(id), 0, .{
.x = cx,
.y = first + @as(u16, pane.notices.row[i]),
.w = chip,
@@ -6503,22 +6563,31 @@ 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] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = p.topBarHeight(), .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, modal.lineSlice(p.columnTag(column), 0), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
+ for (0..p.columnBarHeight()) |line| {
+ const index = if (line == 0) MAX_PANES + column else HEADER_LINE_LAYER_BASE + column * (MAX_TAG_ROWS - 1) + line - 1;
+ try p.renderHeaderLayer(arena, index, .column, @intCast(column), @intCast(line), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = p.topBarHeight() + @as(u16, @intCast(line)), .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, modal.lineSlice(p.columnTag(column), line), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
+ }
};
}
- /// The span a header's selection covers on its first line: up to the
- /// caret while typing, and over the cursor's own cell in normal mode,
- /// where a cursor is a cell.
- pub fn headerSelection(t: *const Text, line: []const u8) ?struct { lo: usize, hi: usize } {
- if (!t.vsel.active or t.vsel.row != 0 or t.cur_row != 0) return null;
- const anchor = @min(@as(usize, @intCast(@max(0, t.vsel.col))), line.len);
- const cursor = @min(@as(usize, @intCast(@max(0, t.cur_col))), line.len);
- if (t.mode == .insert) return .{ .lo = @min(anchor, cursor), .hi = @max(anchor, cursor) };
- return .{ .lo = @min(anchor, cursor), .hi = modal.nextGrapheme(line, @max(anchor, cursor)) };
+ /// The span a header's selection covers on line `row` of its text, in
+ /// that line's bytes: up to the caret while typing, and over the
+ /// cursor's own cell too in normal mode, where a cursor is a cell.
+ pub fn headerSelection(t: *const Text, text: []const u8, row: usize) ?struct { lo: usize, hi: usize } {
+ if (!t.vsel.active) return null;
+ const anchor = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) });
+ const cursor = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ const lo = @min(anchor, cursor);
+ const hi = if (t.mode == .insert) @max(anchor, cursor) else modal.nextGrapheme(text, @max(anchor, cursor));
+ const start = modal.lineStartOffset(text, row);
+ const end = modal.lineEndOffset(text, row);
+ if (hi <= start or lo >= end) return null;
+ return .{ .lo = @max(lo, start) - start, .hi = @min(hi, end) - start };
}
- 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 {
+ /// One row of a header or a notice band as a tag layer: `text` is line
+ /// `line` of what it shows.
+ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: TagKind, id: u16, line: 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) {
@@ -6529,6 +6598,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.line = line;
layer.serial = if (kind == .column) p.col_serial[id] else 0;
layer.viewport = viewport;
layer.cols = columns;
@@ -6549,7 +6619,7 @@ pub const Pardes = struct {
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) |t| {
- if (headerSelection(t, text)) |span| {
+ if (headerSelection(t, try edit.flatSurface(p, t), line)) |span| {
var col = panes.File.rawDisplayCol(text, span.lo) -| scroll;
const end = panes.File.rawDisplayCol(text, span.hi) -| scroll;
while (col < end and col < columns) : (col += 1) {
@@ -6558,11 +6628,11 @@ pub const Pardes = struct {
}
}
const caret = panes.File.rawDisplayCol(text, @min(@as(usize, @intCast(@max(0, t.cur_col))), text.len)) -| scroll;
- if (t.cur_row == 0 and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
+ if (t.cur_row == line and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .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)
+ (if (pointer.kind == kind and pointer.id == id and pointer.line == line) 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