diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 102 |
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. |
