summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 01:01:35 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9d2e07e997de155c7dbb894a79e5c11839eab39e (patch)
tree45d09fbe224693b2b9426213e59e56d5b1f35224 /src
parent425d9beb9c977df364ed83743cb2efdef9429478 (diff)
downloadpardes-9d2e07e997de155c7dbb894a79e5c11839eab39e.tar.gz
pardes-9d2e07e997de155c7dbb894a79e5c11839eab39e.zip
Scroll a tag taller than its rows to keep its caret in view
A tag shows at most eight rows, so a longer one (a 9P write of many lines, or `o` in the tag) put its caret on a line that was never drawn. Pane, column and workspace tags now keep a first line on screen that follows the caret while they have the keyboard, and the pointer and the pane text count their rows from it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig56
-rw-r--r--src/edit.zig7
-rw-r--r--src/mouse.zig16
-rw-r--r--src/panes.zig4
-rw-r--r--src/pardes.zig10
-rw-r--r--src/tagline.zig15
6 files changed, 71 insertions, 37 deletions
diff --git a/src/body_layer.zig b/src/body_layer.zig
index ab000795..04c84b62 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -569,13 +569,14 @@ pub fn paintHeaders(p: *Pardes) !void {
const global = tagline.topbar(p);
const global_rows = p.topBarHeight();
const global_scroll = if (p.header_focus and p.header_column == null) p.header_scroll else 0;
+ const global_top = if (p.header_focus and p.header_column == null) p.header_top else 0;
if (p.settings.workspace_tag) {
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);
+ const line = modal.lineSlice(global, row + global_top);
_ = s.print(0, @intCast(row), s.cols, line[panes.File.rawAtDisplay(line, global_scroll)..], .{
.fg = .{ .rgb = chrome.tag_fg },
.bg = .{ .rgb = chrome.tag_bg },
@@ -584,7 +585,7 @@ pub fn paintHeaders(p: *Pardes) !void {
}
}
if (p.pointer_inside and p.hover_row < global_rows) {
- const bar = modal.lineSlice(global, p.hover_row);
+ const bar = modal.lineSlice(global, p.hover_row + global_top);
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;
@@ -619,9 +620,10 @@ pub fn paintHeaders(p: *Pardes) !void {
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 bar_top = if (p.header_focus and p.header_column == c) p.header_top else 0;
const tag = tagline.columnTag(p, c);
for (0..column_rows) |row| {
- const line = modal.lineSlice(tag, row);
+ const line = modal.lineSlice(tag, row + bar_top);
_ = s.print(x, y0 + @as(u16, @intCast(row)), w, line[panes.File.rawAtDisplay(line, bar_scroll)..], .{
.fg = .{ .rgb = fg },
.bg = .{ .rgb = bg },
@@ -630,7 +632,7 @@ pub fn paintHeaders(p: *Pardes) !void {
}
// 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 >= 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);
+ const bar = modal.lineSlice(tag, p.hover_row - y0 + bar_top);
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;
@@ -649,8 +651,8 @@ pub fn paintHeaders(p: *Pardes) !void {
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 = tagline.headerSelection(t, text, row) orelse continue;
+ const bar = modal.lineSlice(text, row + p.header_top);
+ const span = tagline.headerSelection(t, text, row + p.header_top) 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) {
@@ -659,10 +661,11 @@ pub fn paintHeaders(p: *Pardes) !void {
cell.style.bg = .{ .rgb = th.sel_bg };
}
}
- if (t.cur_row >= 0 and t.cur_row < rows) {
- const bar = modal.lineSlice(text, @intCast(t.cur_row));
+ const cur: usize = @intCast(@max(0, t.cur_row));
+ if (cur >= p.header_top and cur < p.header_top + rows) {
+ const bar = modal.lineSlice(text, cur);
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 + @as(u16, @intCast(t.cur_row)), .bar = t.mode == .insert };
+ if (caret < w) s.cursor = .{ .x = x + @as(u16, @intCast(caret)), .y = y + @as(u16, @intCast(cur - p.header_top)), .bar = t.mode == .insert };
}
}
}
@@ -704,6 +707,7 @@ pub fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
// its viewport.
var widest: usize = 0;
var split = std.mem.splitScalar(u8, tag, '\n');
+ for (0..pane.tag_top) |_| _ = split.next();
for (0..rows) |_| widest = @max(widest, panes.File.displayWidth(split.next() orelse ""));
const columns: u16 = @intCast(@min(std.math.maxInt(u16) - TAG_TEXT_INSET, @max(p.tagCapacity(r.w - TAG_TEXT_INSET), widest + 1)));
const pitch = @as(usize, columns) + TAG_TEXT_INSET;
@@ -740,7 +744,8 @@ pub fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
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 renderHeaderLayer(p, arena, index, .workspace, 0, @intCast(line), .{ .x = 0, .y = @intCast(line), .w = p.screen_w, .h = TOPBAR_H }, modal.lineSlice(tagline.topbar(p), line), chrome.tag_bg, chrome.tag_fg);
+ const top = if (p.header_focus and p.header_column == null) p.header_top else 0;
+ try renderHeaderLayer(p, arena, index, .workspace, 0, @intCast(line), .{ .x = 0, .y = @intCast(line), .w = p.screen_w, .h = TOPBAR_H }, modal.lineSlice(tagline.topbar(p), line + top), chrome.tag_bg, chrome.tag_fg);
}
}
// Each notice is its own tagline-shaped chip over the top of the body.
@@ -805,9 +810,10 @@ pub fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
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;
+ const top = if (p.header_focus and p.header_column == column) p.header_top else 0;
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 renderHeaderLayer(p, 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(tagline.columnTag(p, column), line), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
+ try renderHeaderLayer(p, 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(tagline.columnTag(p, column), line + top), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg);
}
};
}
@@ -846,7 +852,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: T
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 (tagline.headerSelection(t, try edit.flatSurface(p, t), line)) |span| {
+ if (tagline.headerSelection(t, try edit.flatSurface(p, t), line + p.header_top)) |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) {
@@ -855,7 +861,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: T
}
}
const caret = panes.File.rawDisplayCol(text, @min(@as(usize, @intCast(@max(0, t.cur_col))), text.len)) -| scroll;
- if (t.cur_row == line and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
+ if (t.cur_row == line + p.header_top 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|
@@ -899,10 +905,12 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
const whole = try tagline.tagText(p, arena, pane);
const tail = tagline.curTail(pane);
const prefix_len = whole.len - tail.len;
- // A row per line: the first carries the prefix, the rest are the
- // tag's own text alone.
+ // A row per line shown, from `tag_top`: the tag's first line carries
+ // the prefix, the rest are the tag's own text alone.
+ const top = pane.tag_top;
var lines: [MAX_TAG_ROWS][]const u8 = @splat("");
var split = std.mem.splitScalar(u8, whole, '\n');
+ for (0..top) |_| _ = split.next();
for (lines[0..rows]) |*line| line.* = split.next() orelse "";
// The keyboard's caret, as a row and a byte offset in that row's
// line: in the tag's own text when the tag has the keyboard, in the
@@ -912,9 +920,9 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
null
else if (pane.prompt == .name)
.{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) }
- else if (pane.focus == .tag and pane.tag.cur_row < rows) caret: {
- const row: usize = @intCast(@max(0, pane.tag.cur_row));
- const lead: usize = if (row == 0) prefix_len else 0;
+ else if (pane.focus == .tag and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: {
+ const row: usize = @intCast(pane.tag.cur_row - top);
+ const lead: usize = if (row + top == 0) prefix_len else 0;
break :caret .{ .row = row, .at = @min(lines[row].len, lead + @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
} else null;
if (caret) |at| {
@@ -936,7 +944,7 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
_ = s.print(tx, tag_y + @as(u16, @intCast(row)), tw, line[start..], tag_style);
}
const tag = lines[0];
- if (tagTintRange(pane, tag, prefix_len)) |name| {
+ if (top == 0) if (tagTintRange(pane, tag, prefix_len)) |name| {
const lo = @max(name.lo, panes.File.rawAtDisplay(tag, pane.tag_scroll));
const hi = @min(name.hi, tag.len);
if (lo < hi) {
@@ -946,7 +954,7 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
while (col < end and col < tw) : (col += 1)
s.at(tx + @as(u16, @intCast(col)), tag_y).style.fg = .{ .rgb = name_fg };
}
- }
+ };
if (p.look_hover_preview) |preview| {
if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < rows) {
const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg));
@@ -964,15 +972,15 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
if (modal.nextGrapheme(tail, lo) >= hi) continue;
const first = modal.positionAt(tail, lo);
const last = modal.positionAt(tail, hi);
- var row = first.row;
- while (row <= last.row and row < rows) : (row += 1) {
- const line = lines[row];
+ var row = @max(first.row, top);
+ while (row <= last.row and row < top + rows) : (row += 1) {
+ const line = lines[row - top];
const lead: usize = if (row == 0) prefix_len else 0;
const from = lead + if (row == first.row) first.col else 0;
const to = @min(line.len, lead + if (row == last.row) last.col else modal.lineSlice(tail, row).len);
var col = panes.File.rawDisplayCol(line, from) -| pane.tag_scroll;
const end = panes.File.rawDisplayCol(line, to);
- const y = tag_y + @as(u16, @intCast(row));
+ const y = tag_y + @as(u16, @intCast(row - top));
while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), y);
cell.default = false;
diff --git a/src/edit.zig b/src/edit.zig
index edc86dd2..42032168 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -131,12 +131,13 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void {
}
/// tag + '\n' + body: the full selectable pane text, a row to a screen row
-/// (the tag's lines first, as many as it shows)
+/// (the tag's lines first, the ones it shows)
pub fn paneText(p: *Pardes, pane: *Pane) ![]u8 {
const arena = p.scratch.allocator();
const tag = try tagline.tagText(p, arena, pane);
- const cut = modal.lineStartOffset(tag, pane.tag_rows);
- const shown = if (cut < tag.len) tag[0 .. cut - 1] else tag;
+ const from = modal.lineStartOffset(tag, pane.tag_top);
+ const cut = modal.lineStartOffset(tag, pane.tag_top + pane.tag_rows);
+ const shown = if (cut < tag.len) tag[from .. cut - 1] else tag[from..];
return std.fmt.allocPrint(arena, "{s}\n{s}", .{ shown, try body_layer.bodyText(p, arena, pane) });
}
diff --git a/src/mouse.zig b/src/mouse.zig
index df657ead..69feea29 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -276,10 +276,13 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
};
const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
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));
+ // `row` is the header's row on screen, `line` the line of its text
+ // shown there.
+ const base = if (p.header_column != null) p.topBarHeight() else 0;
+ const row: u16 = @intCast(@min(mrow -| base, (if (p.header_column != null) p.columnBarHeight() else p.topBarHeight()) -| 1));
+ const line: u16 = @intCast(@min(row + p.header_top, modal.lineCount(text) -| 1));
const bar = modal.lineSlice(text, line);
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tag_layer.columnAt(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));
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tag_layer.columnAt(p, if (p.header_column != null) .column else .workspace, p.header_column orelse 0, p.pointer_tag_hit, p.header_drag, row) orelse (mcol -| x)) + p.header_scroll));
// the anchor stays where the press put it
t.cur_row = line;
t.cur_col = @intCast(at);
@@ -310,11 +313,12 @@ 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 text = edit.flatSurface(p, t) catch return;
- const line: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0);
+ const row: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0);
+ const line: u16 = @intCast(row + if (focused) p.header_top 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, (tag_layer.columnAt(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, line) orelse (mcol -| x)) + scroll));
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (tag_layer.columnAt(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, row) orelse (mcol -| x)) + scroll));
if (m.button == config.select_button) {
// A clicked header is typed straight into, as a clicked tag is.
if (!focused) tagline.enterHeader(p, column);
@@ -662,7 +666,7 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand:
const tag = tagline.tagText(p, p.scratch.allocator(), pane) catch return .{};
const sel = operand.expanded orelse operand.preview orelse return .{};
const lead = if (sel.r0 < sel.r1 or (sel.r0 == sel.r1 and sel.c0 <= sel.c1)) sel else Pane.Sel{ .r0 = sel.r1, .c0 = sel.c1 };
- const row: usize = @intCast(@max(0, lead.r0));
+ const row: usize = @as(usize, @intCast(@max(0, lead.r0))) + pane.tag_top;
const start = modal.lineStartOffset(tag, row);
const at = start + panes.File.rawAtDisplay(modal.lineSlice(tag, row), @intCast(@max(0, lead.c0)));
const q0: u32 = @intCast(@min(at, tag.len));
diff --git a/src/panes.zig b/src/panes.zig
index 3cd66eb7..356f4913 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -125,6 +125,10 @@ pub const Pane = struct {
/// has room for with a body row to spare. Set with the geometry in sync.
/// Pointer rows count from the tag's first, so a body starts at this one.
tag_rows: u16 = 1,
+ /// The tag's first line on screen. A tag with more lines than rows
+ /// scrolls to keep its caret in view while it has the keyboard; pointer
+ /// rows count from this line, not the tag's first.
+ tag_top: u16 = 0,
cwd: Cwd = .none,
hscroll: i32 = 0,
// Visible rows map to source lines and raw/display column origins.
diff --git a/src/pardes.zig b/src/pardes.zig
index c802f0ea..40e49d6a 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -3771,6 +3771,9 @@ pub const Pardes = struct {
/// A mouse sweep is extending the focused header's selection.
header_drag: bool = false,
header_scroll: usize = 0,
+ /// The focused header's first line on screen, which follows its caret
+ /// when it has more lines than rows.
+ header_top: usize = 0,
ov_pinch_scale: f32 = 1.0,
ov_touch_scroll_delta: f32 = 0.0,
/// middle+left chord argument: a kept left selection captured by a left
@@ -5533,6 +5536,13 @@ pub const Pardes = struct {
const lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1;
pane.tag_rows = @intCast(@min(lines, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1)));
if (pane.collapsed) pane.tag_rows = BOX_H;
+ if (pane.focus == .tag and pane.prompt != .name) {
+ const cur: usize = @intCast(@max(0, pane.tag.cur_row));
+ var top: usize = pane.tag_top;
+ if (cur < top) top = cur;
+ if (cur >= top + pane.tag_rows) top = cur + 1 - pane.tag_rows;
+ pane.tag_top = @intCast(@min(top, lines - pane.tag_rows));
+ } else pane.tag_top = 0;
const cols = @max(1, r.w -| config.GUTTER);
const rows = @max(1, r.h -| pane.tag_rows); // the tag steals the top rows
if ((cols != pane.cols or rows != pane.rows) and r.h > pane.tag_rows) {
diff --git a/src/tagline.zig b/src/tagline.zig
index 1021e9db..a918689b 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -81,10 +81,15 @@ pub fn focusedHeader(p: *Pardes) ?*Text {
pub fn revealHeader(p: *Pardes) void {
const t = focusedHeader(p) orelse {
p.header_scroll = 0;
+ p.header_top = 0;
return;
};
const text = edit.flatSurface(p, t) catch return;
- const line = modal.lineSlice(text, @intCast(@max(0, t.cur_row)));
+ const cur: usize = @intCast(@max(0, t.cur_row));
+ const rows: usize = if (p.header_column != null) p.columnBarHeight() else p.topBarHeight();
+ if (cur < p.header_top) p.header_top = cur;
+ if (rows > 0 and cur >= p.header_top + rows) p.header_top = cur + 1 - rows;
+ const line = modal.lineSlice(text, cur);
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;
@@ -187,10 +192,11 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
/// lands in the tag's own text, or null when it is on the prefix, which
/// only the first line has.
pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor {
- const width = if (row == 0) panes.File.displayWidth(tagPrefix(p, pane) catch return null) else 0;
+ const at = row + pane.tag_top;
+ const width = if (at == 0) panes.File.displayWidth(tagPrefix(p, pane) catch return null) else 0;
if (col < width) return null;
- const line = modal.lineSlice(curTail(pane), row);
- return .{ .row = row, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width)) };
+ const line = modal.lineSlice(curTail(pane), at);
+ return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width)) };
}
/// Start a name draft for a file that can take one, its caret at display
@@ -235,6 +241,7 @@ pub fn exitHeader(p: *Pardes) void {
p.header_column = null;
p.header_drag = false;
p.header_scroll = 0;
+ p.header_top = 0;
}
/// Give the keyboard to a column's tag, or the workspace's for null, in