summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-22 02:21:03 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit43b9d696f9e25ad7589527b91a74666e71ff0472 (patch)
tree52ed8573c53a7a6f6ca956b4f473035126a15354 /src/panes.zig
parent760448928186382da4ef9d942af1a2b7546ef54c (diff)
downloadpardes-43b9d696f9e25ad7589527b91a74666e71ff0472.tar.gz
pardes-43b9d696f9e25ad7589527b91a74666e71ff0472.zip
Notices become tagline bands at the top of the body
A message, a leader chord and a prompt used to share one row of body text at the bottom of a pane, wearing the tagline font and nothing else about a tagline. Now each one is a TagLayer of its own, emitted through the same renderHeaderLayer the pane and column tags go through, so it gets the tagline height, the small-font metrics, the band offset and the border for free -- none of which a body-grid row can have by changing its font role. The text is right aligned. The prompt stays on the canonical grid because it owns a cursor, and a cursor has to sit on a real cell. The body starts BELOW the bands rather than under them, the way tree-sitter context rows already worked. Pane.body_offset is how many rows they took and Pardes.bodyTop(pane, rect) is the one place that answers "where does the body begin" -- replacing fifteen copies of `if (tag_bottom) r.y else r.y + BOX_H` spread across the paint, hit-test, scroll, PDF and image paths, which is what let the bands and the text under them come adrift. Every notice is painted on the grid as well, because the grid is what a terminal client draws and a band it cannot see is a message it never gets; the GUI skips grid cells a tag layer covers, so nothing is drawn twice. Three bugs the bands exposed, fixed here: - a prompt band flush with the right edge put its caret one column past the pane, which the detached wire refuses -- so every frame was dropped for as long as the prompt was open. The band now reserves that column. - a click on a band mapped to Sel row 0, which is the TAG row: clicking chrome expanded a word out of the tagline and ran it as a builtin. - a watched file reloading under the editor changed the core without going through update, so needs_frame was never set and the reload was never drawn. Pardes.invalidate() is the name for that, and the file and theme reloads call it. A session can now drive its own 9P namespace instead of being refused one: ownMountSuffix answers what a path names inside this editors own tree and resolve, readLimit and write serve it from memory rather than making the syscall that never returns. The match is anchored to whole components under the registrys 9p/pardes/<name>, because a bare /pardes/<name> anywhere in a string would claim ~/src/pardes/<name>/README -- and, before write learned the same trick, write the trees bytes over the real file. readFileLimit and writeFile refuse instead, having no core to answer from. A toggle setting SETS when given `on` or `off` and only flips when it is bare, so the report LocationsConfig prints can be fed back as configuration and mean what it says. Snapshots: 97/98, from 0/98. The goldens were several commits stale and 17 scripts had stopped running; `config <line>` is a new script command that appends to the per-script startup config, so a script that clicks body coordinates pins `Verbose off` instead of counting the rows an announcement moves. nested-optout is left failing on purpose: two levels of nesting prepend vaxis F3 codepoints to typed lines, which is a real bug and is written down in docs/divergences.md with a repro. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'src/panes.zig')
-rw-r--r--src/panes.zig42
1 files changed, 25 insertions, 17 deletions
diff --git a/src/panes.zig b/src/panes.zig
index e41698bb..4b5c6721 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -191,6 +191,11 @@ pub const Pane = struct {
/// body layer reserves exactly `len` rows for them, the way it already
/// reserves rows for sticky context headers.
notices: Notices = .{},
+ /// Rows the notice bands take off the TOP of the body, the way tree-sitter
+ /// context rows do: the body starts below them rather than being painted
+ /// under them. Zero while the body LAYER renders, which paints into a
+ /// surface of its own that carries no bands.
+ body_offset: u16 = 0,
pub const Notices = struct {
/// Painted bottom-up in this order, so a prompt keeps the row nearest
@@ -2036,7 +2041,7 @@ pub const File = struct {
const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, gcur.y) + goff;
const cur_line: i32 = if (active and !pane.tag_edit) gcrow else std.math.minInt(i32);
// the body's first row, the way renderPane derives it (Tagbottom)
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = p.bodyTop(pane, r);
const context_bg = p.theme().tag_bg;
for (0..@min(pane.context_rows, body_h)) |context_row| {
for (0..tw) |col| {
@@ -2090,7 +2095,7 @@ pub const File = struct {
// indexed start, same as bodyText — an empty tail simply paints nothing
var flines = std.mem.splitScalar(u8, f.content[lineStart(p.gpa, f, f.scroll)..], '\n');
const total = nlines(p.gpa, f);
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = p.bodyTop(pane, r);
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
var base: usize = undefined;
@@ -2166,7 +2171,7 @@ pub const File = struct {
pane_bg: pardes.Color,
) void {
if (tw <= gutterWidth(pane) + 1) return;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = p.bodyTop(pane, r);
const marker_fg = p.chromeTheme().lineno;
var row: u16 = 0;
while (row + 1 < pane.wrap_n and row + 1 < body_h) : (row += 1) {
@@ -2190,9 +2195,9 @@ pub const File = struct {
const tx = r.x + config.GUTTER;
const tw = r.w - config.GUTTER;
const prefix_width = gutterWidth(pane);
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = p.bodyTop(pane, r);
var vr: i32 = 0;
- while (vr + @as(i32, pardes.BOX_H) < @as(i32, r.h)) : (vr += 1) {
+ while (vr + @as(i32, pardes.BOX_H) < @as(i32, r.h -| pane.body_offset)) : (vr += 1) {
const here = pane.wrapAt(vr);
if (here.line != row) continue;
var hi = word_hi;
@@ -5477,7 +5482,7 @@ pub const Pdf = struct {
if (slot == pane) break core.rects[id];
} else return null;
const cols = rect.w -| config.GUTTER;
- const rows = rect.h -| pardes.BOX_H;
+ const rows = (rect.h -| pardes.BOX_H) -| pane.body_offset;
if (cols == 0 or rows == 0) return null;
return .{
.pixel_w = @as(u32, cols) * @as(u32, core.cell_pixels.w),
@@ -5515,8 +5520,8 @@ pub const Pdf = struct {
const rect = for (core.panes, 0..) |slot, id| {
if (slot == pane) break core.rects[id];
} else return null;
- const body_y = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
- if (row < body_y or row >= body_y + (rect.h -| pardes.BOX_H)) return null;
+ const body_y = core.bodyTop(pane, rect);
+ if (row < body_y or row >= body_y + ((rect.h -| pardes.BOX_H) -| pane.body_offset)) return null;
const local_y = @as(f64, @floatFromInt(
@as(u32, row - body_y) * core.cell_pixels.h + core.cell_pixels.h / 2,
));
@@ -5572,7 +5577,7 @@ pub const Pdf = struct {
if (slot == pane) break core.rects[id];
} else return null;
const body_x = @as(i64, rect.x + config.GUTTER);
- const body_y = @as(i64, if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H);
+ const body_y = @as(i64, core.bodyTop(pane, rect));
const px = (@as(i64, col) - body_x) * core.cell_pixels.w + core.cell_pixels.w / 2;
const py = (@as(i64, row) - body_y) * core.cell_pixels.h + core.cell_pixels.h / 2;
return pointAtPage(state, view, page, px, py, clamp_to_page);
@@ -5855,7 +5860,7 @@ pub const Pdf = struct {
text_width: u16,
) bool {
if (comptime !enabled) return false;
- if (!core.native_images or rect.h <= pardes.BOX_H) return false;
+ if (!core.native_images or rect.h <= pardes.BOX_H + pane.body_offset) return false;
const state = &(pane.pdf orelse return false);
const view = paneViewport(core, pane) orelse return false;
const key = TintKey{ .mode = state.tint, .colors = tintColors(core) };
@@ -5892,9 +5897,9 @@ pub const Pdf = struct {
null,
},
.x = text_x,
- .y = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H,
+ .y = core.bodyTop(pane, rect),
.w = text_width,
- .h = rect.h - pardes.BOX_H,
+ .h = (rect.h -| pardes.BOX_H) -| pane.body_offset,
.rgba = placed.rgba,
.iw = placed.width,
// The texture contains the retained band, not the full page.
@@ -5906,14 +5911,17 @@ pub const Pdf = struct {
// This frame has spent the motion used to choose its raster band.
state.scroll_travel = 0;
- const body_y = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const body_y = core.bodyTop(pane, rect);
+ const body_h = (rect.h -| pardes.BOX_H) -| pane.body_offset;
const chrome = core.chromeTheme();
const theme = core.theme();
const pane_bg: pardes.Color = if (theme.bg) |color| .{ .rgb = color } else .default;
- core.surface.fill(rect.x, body_y, 1, rect.h -| pardes.BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- core.surface.fill(rect.x + 1, body_y, 1, rect.h -| pardes.BOX_H, .{ .bg = pane_bg });
+ // The rail runs past the notice bands; only the thumb tracks the text.
+ const rail_y = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ core.surface.fill(rect.x, rail_y, 1, rect.h -| pardes.BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ core.surface.fill(rect.x + 1, rail_y, 1, rect.h -| pardes.BOX_H, .{ .bg = pane_bg });
- const track_h: usize = rect.h - pardes.BOX_H;
+ const track_h: usize = body_h;
const total = @max(@as(u64, 1), state.document_height);
const len = @max(
@as(usize, 1),
@@ -7571,7 +7579,7 @@ pub const Terminal = struct {
if (comptime !enabled) return;
const s = &p.surface;
if (pane.terminal == null) return;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = p.bodyTop(pane, r);
var filtered_storage: FilteredColors = undefined;
const filtered: ?*FilteredColors = if (pane.tty_filter) blk: {
const tz_filter = tracy.zone(@src(), "filterInit");