summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig102
1 files changed, 68 insertions, 34 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 7e18db59..51442034 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -13090,6 +13090,22 @@ pub const Pardes = struct {
};
}
+ /// The pane Last should fall back to when the jumplist has nothing: the
+ /// one below in this column, wrapping to the top, and any other live pane
+ /// failing that. A pane that opened beside this one and was never focused
+ /// -- the text pane the bare tty layout puts under the shell -- is not on
+ /// the jumplist, and alternating with it is exactly what Last is for.
+ pub fn neighbourPane(p: *Pardes, from: usize) ?usize {
+ if (layout.findPane(p, from)) |at| {
+ const n = p.col_n[at.col];
+ if (n > 1) return p.col_panes[at.col][(at.idx + 1) % n];
+ }
+ for (p.panes, 0..) |slot, id| {
+ if (slot != null and id != from) return id;
+ }
+ return null;
+ }
+
pub fn prevFocus(p: *Pardes, closing: usize) ?usize {
var i = p.njumps;
while (i > 0) {
@@ -13359,50 +13375,54 @@ pub const Pardes = struct {
pane.body_visible_rows = 0;
pane.context_row_limit = null;
s.body_layers[id].rows = 0;
+ p.collectNotices(pane, p.rects[id], id);
try p.renderPane(arena, pane, p.rects[id], id, id == p.active);
try p.renderBodyLayer(arena, pane, p.rects[id], id);
}
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
+ if (pane.notices.len == 0) continue;
const r = p.rects[id];
- if (r.w <= config.GUTTER or r.h <= BOX_H) continue;
- const prompt_at = pane.promptAt();
- const text = if (prompt_at) |at|
- pane.tagSlice()[@min(at, pane.tag_tail_len)..]
- else
- pane.msg[0..pane.msg_len];
- // The pending SPC path is the third thing that wants this row, and
- // it can want it while the row is otherwise empty.
- const leader_here = id == p.active and p.leader_on;
- if (text.len == 0 and !leader_here) continue;
const tx = r.x + config.GUTTER;
const tw = r.w - config.GUTTER;
- const row = if (p.settings.tag_bottom) r.y + r.h - 1 - BOX_H else r.y + r.h - 1;
const msg_style: CellStyle = .{
.fg = if (th.fg) |c| .{ .rgb = c } else .default,
.bg = if (th.bg) |c| .{ .rgb = c } else .default,
};
- s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg });
- if (text.len > 0) _ = s.print(tx, row, tw, text, msg_style);
- if (leader_here) {
- var ibuf: [16]u8 = @splat(' ');
- @memcpy(ibuf[1..4], "SPC");
- var iw: usize = 4;
- for (p.leader_keys[0..p.leader_n]) |ch| {
- ibuf[iw + 1] = ch;
- iw += 2;
+ // The stack sits directly above the tagline, one line to a row, in
+ // the order collectNotices chose. A single notice therefore lands
+ // on exactly the row the message used to have to itself.
+ const bottom = if (p.settings.tag_bottom) r.y + r.h - 1 - BOX_H else r.y + r.h - 1;
+ for (pane.notices.slice(), 0..) |kind, i| {
+ const row = bottom - @as(u16, @intCast(pane.notices.len - 1 - i));
+ s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg });
+ switch (kind) {
+ .message => _ = s.print(tx, row, tw, pane.msg[0..pane.msg_len], msg_style),
+ .leader => {
+ var ibuf: [16]u8 = @splat(' ');
+ @memcpy(ibuf[1..4], "SPC");
+ var iw: usize = 4;
+ for (p.leader_keys[0..p.leader_n]) |ch| {
+ ibuf[iw + 1] = ch;
+ iw += 2;
+ }
+ const w: u16 = @intCast(iw);
+ if (w < tw) _ = s.print(tx + tw - w, row, w, ibuf[0..iw], msg_style);
+ },
+ .prompt => {
+ const at = pane.promptAt() orelse continue;
+ const text = pane.tagSlice()[@min(at, pane.tag_tail_len)..];
+ _ = s.print(tx, row, tw, text, msg_style);
+ if (id != p.active) continue;
+ const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
+ const col = @as(usize, pane.tag_col);
+ if (col >= prompt0) {
+ const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
+ if (prompt_col < tw)
+ s.cursor = .{ .x = tx + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
+ }
+ },
}
- const w: u16 = @intCast(iw);
- if (w < tw) _ = s.print(tx + tw - w, row, w, ibuf[0..iw], msg_style);
- }
- if (id != p.active) continue;
- const at = prompt_at orelse continue;
- const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
- const col = @as(usize, pane.tag_col);
- if (col >= prompt0) {
- const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
- if (prompt_col < tw)
- s.cursor = .{ .x = tx + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
}
}
@@ -13921,12 +13941,26 @@ pub const Pardes = struct {
}
}
+ /// The one place that decides which transient lines a pane is showing and
+ /// in what order. Everything downstream reads the answer: the body layer
+ /// so it does not paint under them, and the paint pass so it can stack
+ /// them instead of letting the last one win the row.
+ fn collectNotices(p: *Pardes, pane: *Pane, r: Rect, id: usize) void {
+ pane.notices.len = 0;
+ if (r.w <= config.GUTTER or r.h <= BOX_H) return;
+ if (pane.msg_len > 0) pane.notices.push(.message);
+ if (id == p.active and p.leader_on) pane.notices.push(.leader);
+ if (pane.promptAt()) |at| {
+ if (pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0) pane.notices.push(.prompt);
+ }
+ // Never claim so many rows that the body has none left.
+ pane.notices.len = @min(pane.notices.len, r.h - BOX_H -| 1);
+ }
+
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;
- const message = if (pane.promptAt()) |at| pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0 else pane.msg_len > 0;
- const reserve: u16 = if (message or (id == p.active and p.leader_on)) 1 else 0;
- const viewport_rows = r.h - BOX_H -| reserve;
+ const viewport_rows = r.h - BOX_H -| @as(u16, pane.notices.len);
if (viewport_rows == 0) return;
// The canonical fixed grid remains available to terminal clients. The
// separate body layer reserves enough rows for any compact font size.