diff options
| -rw-r--r-- | features.txt | 3 | ||||
| -rw-r--r-- | src/builtins.zig | 3 | ||||
| -rw-r--r-- | src/panes.zig | 28 | ||||
| -rw-r--r-- | src/pardes.zig | 102 |
4 files changed, 102 insertions, 34 deletions
diff --git a/features.txt b/features.txt index 7515d455..e05f6f07 100644 --- a/features.txt +++ b/features.txt @@ -136,3 +136,6 @@ gamepad subsystem. Two fields and a branch for nothing, so it went back out. Conclusion for both platforms: nothing in pardes's own logic is slow at startup. The cost is the Debug build's allocator tracing and the GPU/driver stack coming up, and the only lever on either is the build mode. + +The Last builtin should fall back to a heuristic when the jumplist has nothing to offer, so the bare tty layout -- a shell with an unfocused text pane under it -- +alternates with Last even though that text pane was never focused and is not on the jumplist. diff --git a/src/builtins.zig b/src/builtins.zig index aa2b2d6d..30a5a50f 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -701,6 +701,9 @@ pub const Last = struct { // Restore the cursor without recentering the pane's retained view. if (j.pane != c.id) return c.p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }, .keep); } + // Nothing on the jumplist to go back to, which is the ordinary case + // for a pane that opened beside this one and was never focused. + if (c.p.neighbourPane(c.id)) |next| c.p.focusPaneLine(next, .{ .line = 0, .col = 0 }, .keep); } }; diff --git a/src/panes.zig b/src/panes.zig index 9a6802d0..e41698bb 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -183,6 +183,34 @@ pub const Pane = struct { tty_filter: bool = false, msg: [256]u8 = undefined, msg_len: u16 = 0, + /// The transient lines this pane shows between its body and its tagline: + /// a builtin's message, the pending leader chord, a prompt waiting for + /// input. Rebuilt every frame from the state that owns each one, so a + /// line that goes away is simply not added again and the rest close the + /// gap -- there is no second copy of the truth to fall out of step. The + /// body layer reserves exactly `len` rows for them, the way it already + /// reserves rows for sticky context headers. + notices: Notices = .{}, + + pub const Notices = struct { + /// Painted bottom-up in this order, so a prompt keeps the row nearest + /// the tagline that it has always had, and its cursor with it. + pub const Kind = enum { message, leader, prompt }; + pub const max = @typeInfo(Kind).@"enum".fields.len; + + kinds: [max]Kind = undefined, + len: u8 = 0, + + pub fn push(n: *Notices, kind: Kind) void { + if (n.len >= max) return; + n.kinds[n.len] = kind; + n.len += 1; + } + + pub fn slice(n: *const Notices) []const Kind { + return n.kinds[0..n.len]; + } + }; pub fn clearPointerSelection(pane: *Pane, slot: usize) void { if (pane.pointer_selections[slot]) |selection| { 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. |
