summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 01:26:08 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit09c35b22793153d201fa202b52671a6442e5e2e1 (patch)
tree1eb84c955dc96f4c6bcb75b36f6113ccad8e2f1e /src
parent475cc49ccbe9e81f8ec8aa2371c20fc632d110b9 (diff)
downloadpardes-09c35b22793153d201fa202b52671a6442e5e2e1.tar.gz
pardes-09c35b22793153d201fa202b52671a6442e5e2e1.zip
a builtin to put the pane taglines at the bottom
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig12
-rw-r--r--src/config.zig1
-rw-r--r--src/file_pane.zig7
-rw-r--r--src/gui/gui.zig31
-rw-r--r--src/pardes.zig199
-rw-r--r--src/term_pane.zig3
-rw-r--r--src/web.zig19
7 files changed, 199 insertions, 73 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index b1380203..934b3996 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -191,6 +191,18 @@ pub const Wrap = struct {
}
};
+/// Taglines at the BOTTOM of every pane instead of the top. Global like Wrap
+/// and for the same reason — a layout where some panes label themselves above
+/// and others below is unreadable, so it is one switch for the whole screen.
+/// The transient message row moves the other way to keep out of its way (see
+/// render), which puts the two strips of chrome at the ends they always were,
+/// just swapped.
+pub const Tagbottom = struct {
+ pub fn run(c: Ctx) void {
+ c.p.tag_bottom = !c.p.tag_bottom;
+ }
+};
+
/// One step along the ring. With 228 themes in it this is no longer a way to
/// REACH a theme — ThemeSel is — but it is still the way to browse one, and the
/// browse got better rather than worse: the generated half is sorted by name, so
diff --git a/src/config.zig b/src/config.zig
index 93395406..76d63727 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -121,6 +121,7 @@ pub const leader_path = paths: {
.Debug = "td",
.Colors = "tc",
.Wrap = "tw",
+ .Tagbottom = "tb",
.NextColor = "tn",
.Crt = "tr",
// the theme picker joins the toggles it belongs with; `Theme` itself takes
diff --git a/src/file_pane.zig b/src/file_pane.zig
index 98a72ac4..b06599ae 100644
--- a/src/file_pane.zig
+++ b/src/file_pane.zig
@@ -277,6 +277,8 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod
// the number sits on the first of them, so the whole run lights up — the
// gutter is naming the line you are on, and that is still one line
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.tag_bottom) r.y else r.y + pardes.BOX_H;
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
const row_line: i32 = if (pane.wrap_n == 0)
@@ -285,7 +287,7 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod
const on_cursor = row_line == cur_line;
var c: u16 = 0;
while (c < config.PREFIX_W and c < tw) : (c += 1) {
- const cell = s.at(tx + c, r.y + pardes.BOX_H + vr);
+ const cell = s.at(tx + c, body_y + vr);
cell.default = false; // paints blank gutter rows too
if (on_cursor) {
cell.style.fg = .{ .rgb = ch.tag_fg };
@@ -317,6 +319,7 @@ pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16,
// 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.tag_bottom) r.y else r.y + pardes.BOX_H;
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
// A colour has to land on the byte it belongs to, so this walk reads
@@ -352,7 +355,7 @@ pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16,
const hidx = idx - f.highlight_start;
if (hidx >= f.highlights.len) continue;
const ss = synStyle(p, @enumFromInt(f.highlights[hidx])) orelse continue;
- const cell = s.at(tx + @as(u16, @intCast(config.PREFIX_W + c)), r.y + pardes.BOX_H + vr);
+ const cell = s.at(tx + @as(u16, @intCast(config.PREFIX_W + c)), body_y + vr);
if (cell.default) continue;
cell.style.fg = .{ .rgb = ss.fg };
cell.style.bold = ss.bold;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index e1d5c86d..1f7812e4 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -500,6 +500,12 @@ const WebChromeTarget = struct { col: u16, row: u16 };
/// Fat-finger hit test for web layout chrome. A gesture is classified once at
/// finger-down, then never changes into scrolling. Handle coordinates are
/// snapped onto the core's one-cell resize edge; body rows remain untouched.
+///
+/// The tag row and the stacked-pair seam come from the core's own rule:
+/// Tagbottom puts a pane's tag on its LAST row, which makes its FIRST row an
+/// ordinary scrollable body row and moves the h-handle down to the lower
+/// pane's first. Kept a line-for-line clone of web.zig's chromeTarget — the
+/// two must classify a finger identically.
fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTarget {
if (row < pardes.TOPBAR_H) return .{ .col = col, .row = row };
@@ -508,8 +514,9 @@ fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTar
for (core.panes, 0..) |slot, id| {
if (slot == null) continue;
const rect = core.rects[id];
- if (row == rect.y and col >= rect.x and col < rect.x + rect.w and col < rect.x + config.GUTTER)
- return .{ .col = col, .row = rect.y };
+ const tag = if (core.tag_bottom) rect.y + rect.h -| pardes.BOX_H else rect.y;
+ if (row == tag and col >= rect.x and col < rect.x + rect.w and col < rect.x + config.GUTTER)
+ return .{ .col = col, .row = tag };
}
for (0..core.ncol -| 1) |column| {
@@ -520,7 +527,7 @@ fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTar
if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
for (0..core.col_n[column] -| 1) |index| {
const rect = core.rects[core.col_terms[column][index]];
- const handle = rect.y + rect.h -| 1;
+ const handle = if (core.tag_bottom) rect.y +| rect.h else rect.y + rect.h -| 1;
if (row == handle) return .{ .col = col, .row = handle };
}
}
@@ -529,8 +536,9 @@ fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTar
for (core.panes, 0..) |slot, id| {
if (slot == null) continue;
const rect = core.rects[id];
- if (row == rect.y and col >= rect.x and col < rect.x + rect.w)
- return .{ .col = col, .row = rect.y };
+ const tag = if (core.tag_bottom) rect.y + rect.h -| pardes.BOX_H else rect.y;
+ if (row == tag and col >= rect.x and col < rect.x + rect.w)
+ return .{ .col = col, .row = tag };
}
// One-cell tolerance around thin separators, after exact tag rows have had
@@ -543,7 +551,7 @@ fn webChromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?WebChromeTar
if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
for (0..core.col_n[column] -| 1) |index| {
const rect = core.rects[core.col_terms[column][index]];
- const handle = rect.y + rect.h -| 1;
+ const handle = if (core.tag_bottom) rect.y +| rect.h else rect.y + rect.h -| 1;
if (cellDistance(row, handle) == 1) return .{ .col = col, .row = handle };
}
}
@@ -984,6 +992,7 @@ const Gui = struct {
// scroll_delta the way the wheel arm of dispatch does.
scroll_pane: ?usize = null,
scroll_rect: pardes.Rect = .{ .x = 0, .y = 0, .w = 0, .h = 0 },
+ scroll_body_y: u16 = 0, // that rect's first BODY row (Tagbottom moves it)
scroll_col: u16 = 0, // where the wheel turned: crossed rows are delivered
scroll_row: u16 = 0, // THERE, not wherever the pointer has drifted to since
scroll_delta: f32 = 0, // finite raw distance waiting for the render batch
@@ -2884,6 +2893,10 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
}
g.scroll_lag = st.lag;
g.scroll_rect = r;
+ // the body origin travels with the rect: the two draw sites below have no
+ // core to ask, and with Tagbottom the body starts at r.y, not r.y + BOX_H
+ const body_y = if (core.tag_bottom) r.y else r.y + pardes.BOX_H;
+ g.scroll_body_y = body_y;
if (st.rows != 0) {
// The offset opens a gap at the trailing edge of the travel, and what
// belongs in it is the row that just left the pane: already gone from
@@ -2896,7 +2909,7 @@ fn stepScroll(g: *Gui, core: *pardes.Pardes, gpa: std.mem.Allocator) void {
const bw = r.w - config.GUTTER;
const bh = r.h - pardes.BOX_H;
const k: u16 = @intCast(@min(@abs(st.rows), @as(i32, bh)));
- const srow = r.y + pardes.BOX_H + (if (st.rows > 0) k - 1 else bh - k);
+ const srow = body_y + (if (st.rows > 0) k - 1 else bh - k);
if (srow < s.rows) {
if (g.scroll_edge.len < bw) {
gpa.free(g.scroll_edge);
@@ -2923,7 +2936,7 @@ fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pard
const r = g.scroll_rect;
if (r.w <= config.GUTTER or r.h <= pardes.BOX_H) return 0;
const x0 = r.x + config.GUTTER;
- const y0 = r.y + pardes.BOX_H;
+ const y0 = g.scroll_body_y;
const bw = r.w - config.GUTTER;
const bh = r.h - pardes.BOX_H;
if (x0 + bw > surface.cols or y0 + bh > surface.rows) return 0; // resized under us
@@ -2960,7 +2973,7 @@ fn emitScrollRows(g: *Gui, instances: [*]CellInstance, base: u32, surface: *pard
fn scrollScissor(g: *const Gui, layout: CellLayout, sw: u32, sh: u32) c.SDL_Rect {
const r = g.scroll_rect;
const px = layout.x_off + @as(f32, @floatFromInt(r.x + config.GUTTER)) * layout.w;
- const py = layout.y_off + @as(f32, @floatFromInt(r.y + pardes.BOX_H)) * layout.h;
+ const py = layout.y_off + @as(f32, @floatFromInt(g.scroll_body_y)) * layout.h;
const pw = @as(f32, @floatFromInt(r.w - config.GUTTER)) * layout.w;
const ph = @as(f32, @floatFromInt(r.h - pardes.BOX_H)) * layout.h;
const x0 = std.math.clamp(@as(i32, @intFromFloat(@floor(px))), 0, @as(i32, @intCast(sw)));
diff --git a/src/pardes.zig b/src/pardes.zig
index aa753277..ba80ee22 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -3943,9 +3943,15 @@ fn clampBorderCol(lx: u16, lw: u16, rw: u16, mcol: u16) u16 {
/// Where a horizontal border drag settles: `mrow` clamped so either pane may
/// shrink to just its tag row (BOX_H) but no further. `ay`/`ah` are the upper
/// pane's y and height, `bh` the lower pane's.
-fn clampBorderRow(ay: u16, ah: u16, bh: u16, mrow: u16) u16 {
- const lo = ay + BOX_H - 1;
- const hi = ay +| ah +| bh -| (BOX_H + 1);
+///
+/// The handle is the seam row that is a BODY row, which is the upper pane's
+/// LAST row normally and — with Tagbottom, where that row is the upper pane's
+/// tag — the lower pane's FIRST. That is the same seam one row further down,
+/// so both walls simply move with it.
+fn clampBorderRow(ay: u16, ah: u16, bh: u16, mrow: u16, tag_bottom: bool) u16 {
+ const d: u16 = if (tag_bottom) 1 else 0;
+ const lo = ay + BOX_H - 1 + d;
+ const hi = (ay +| ah +| bh -| (BOX_H + 1)) +| d;
return if (lo <= hi) std.math.clamp(mrow, lo, hi) else mrow;
}
@@ -3967,25 +3973,32 @@ test "a corner drag's two axes clamp independently" {
// each axis alone, at both walls
try std.testing.expectEqual(@as(u16, config.MINW), clampBorderCol(lx, lw, rw, 0));
try std.testing.expectEqual(@as(u16, 100 - config.MINW), clampBorderCol(lx, lw, rw, 999));
- try std.testing.expectEqual(row_lo, clampBorderRow(ay, ah, bh, 0));
- try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999));
+ try std.testing.expectEqual(row_lo, clampBorderRow(ay, ah, bh, 0, false));
+ try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999, false));
// THE corner property: drag off the right edge while staying mid-height —
// x sits at its wall, y still tracks the mouse row exactly
try std.testing.expectEqual(@as(u16, 100 - config.MINW), clampBorderCol(lx, lw, rw, 999));
- try std.testing.expectEqual(@as(u16, 12), clampBorderRow(ay, ah, bh, 12));
+ try std.testing.expectEqual(@as(u16, 12), clampBorderRow(ay, ah, bh, 12, false));
// and the mirror: pinned at the bottom, x still free
- try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999));
+ try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999, false));
try std.testing.expectEqual(@as(u16, 40), clampBorderCol(lx, lw, rw, 40));
// both pinned at once is just both walls, not a stuck gesture
try std.testing.expectEqual(@as(u16, config.MINW), clampBorderCol(lx, lw, rw, 0));
- try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999));
+ try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999, false));
+
+ // Tagbottom moves the handle to the LOWER pane's first row, one further
+ // down, and both walls travel with it — either pane still bottoms out at
+ // its bare tag row and neither can be squeezed away
+ try std.testing.expectEqual(row_lo + 1, clampBorderRow(ay, ah, bh, 0, true));
+ try std.testing.expectEqual(row_hi + 1, clampBorderRow(ay, ah, bh, 999, true));
+ try std.testing.expectEqual(@as(u16, 12), clampBorderRow(ay, ah, bh, 12, true));
// degenerate pairs pass through untouched, each on its own terms: a pane
// pair with no room stays draggable even when the column pair has room
try std.testing.expectEqual(@as(u16, 7), clampBorderCol(0, 5, 5, 7));
try std.testing.expectEqual(@as(u16, 40), clampBorderCol(lx, lw, rw, 40));
- try std.testing.expectEqual(@as(u16, 9), clampBorderRow(TOPBAR_H, 1, 0, 9));
+ try std.testing.expectEqual(@as(u16, 9), clampBorderRow(TOPBAR_H, 1, 0, 9, false));
}
pub const Rect = struct { x: u16, y: u16, w: u16, h: u16 };
@@ -4104,6 +4117,12 @@ pub const Pardes = struct {
/// something. Turned off, every row math in here is the one-row-per-line
/// arithmetic it always was and hscroll comes back.
wrap_on: bool = true,
+ /// every pane's tagline on its LAST row instead of its first (the
+ /// Tagbottom builtin). Render and hit-testing derive two locals from it —
+ /// the tag row and the first body row — and nothing else in here knows;
+ /// the Sel coordinate space is untouched, so row 0 is still the tag and
+ /// the body still starts at BOX_H whichever end they are drawn at.
+ tag_bottom: bool = false,
show_debug: bool = false,
/// the Crt builtin's CRT post-process, rendered by the gui shell only
/// (the tty and web shells never read it)
@@ -8994,13 +9013,23 @@ pub const Pardes = struct {
// pair's h-handle, and the box must stay grabbable to move
if (hovered) |mid| {
const mr = p.rects[mid];
- if (mcol < mr.x + config.GUTTER and mrow < mr.y + BOX_H) {
+ const mtag = if (p.tag_bottom) mr.y + mr.h -| BOX_H else mr.y;
+ if (mcol < mr.x + config.GUTTER and mrow >= mtag and mrow < mtag + BOX_H) {
p.drag = .{ .move = .{ .id = mid, .cur_x = mcol, .cur_y = mrow } };
return;
}
}
// resize handles next: a pane's own trailing edge (v: the
- // left column's last col; h: the upper pane's last row).
+ // left column's last col; h: the seam row between a stacked
+ // pair that is a BODY row — the upper pane's last, or with
+ // Tagbottom, where that one is the upper pane's tag, the
+ // lower pane's first).
+ // Either way the handle EATS one body row of one of the two
+ // panes — a press there resizes instead of placing a cursor
+ // — and it always has. Tagbottom does not change how many
+ // rows that costs, only WHICH pane pays: the lower pane's
+ // first row rather than the upper pane's last. A no-drag
+ // click on it is a no-op in both orientations.
// The v test still wins outright, but it now also asks
// whether this same cell is one of ITS OWN column's h
// handles — that cell is the corner where the two lines
@@ -9011,7 +9040,8 @@ pub const Pardes = struct {
var corner: ?usize = null;
for (0..p.col_n[c] -| 1) |k| {
const r = p.rects[p.col_terms[c][k]];
- if (mrow == r.y + r.h -| 1) {
+ const seam = if (p.tag_bottom) r.y +| r.h else r.y + r.h -| 1;
+ if (mrow == seam) {
corner = k;
break;
}
@@ -9024,7 +9054,8 @@ pub const Pardes = struct {
if (mcol < p.col_x[cc] or mcol >= p.col_x[cc] + p.col_w[cc]) continue;
for (0..p.col_n[cc] -| 1) |k| {
const r = p.rects[p.col_terms[cc][k]];
- if (mrow == r.y + r.h -| 1) {
+ const seam = if (p.tag_bottom) r.y +| r.h else r.y + r.h -| 1;
+ if (mrow == seam) {
p.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } };
return;
}
@@ -9033,11 +9064,18 @@ pub const Pardes = struct {
const id = hovered orelse return;
const r = p.rects[id];
const pane = p.panes[id] orelse return;
+ // the same two rows renderPane painted through (see there):
+ // the tag's, and the body's first. A Sel row is still 0 for
+ // the tag and BOX_H upward for the body whichever end they
+ // are at, so the mapping happens HERE and everything
+ // downstream of a Sel is untouched.
+ const tag_y = if (p.tag_bottom) r.y + r.h -| BOX_H else r.y;
+ const body_y = if (p.tag_bottom) r.y else r.y + BOX_H;
if (mcol < r.x + config.GUTTER) {
// gutter scrollbar: a left click scrolls UP to that row
p.active = id;
- pane.scrollBy(-(@as(i32, mrow) - @as(i32, r.y + BOX_H)));
- } else if (mrow < r.y + BOX_H) {
+ pane.scrollBy(-(@as(i32, mrow) - @as(i32, body_y)));
+ } else if (mrow >= tag_y and mrow < tag_y + BOX_H) {
// left press on the tag row: focus the tag AT that
// column (a rendered-tag column — the mouse and the tag
// agree, no clamp into the tail) and anchor a sweep, so
@@ -9050,7 +9088,7 @@ pub const Pardes = struct {
p.active = id;
exitTagEdit(pane); // clicking the body leaves tag editing
const sc: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
- const v: i32 = @as(i32, mrow) - @as(i32, r.y);
+ const v: i32 = @as(i32, mrow) - @as(i32, body_y) + @as(i32, BOX_H);
pane.sel[sel_slot] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v };
const pdf_native = if (comptime pdf_enabled) p.pdfNativeReady(pane) else false;
if (pdf_native) _ = p.beginPdfDrag(pane, mcol, mrow, false);
@@ -9111,17 +9149,27 @@ pub const Pardes = struct {
const id = hovered orelse return;
const r = p.rects[id];
const pane = p.panes[id] orelse return;
- if (mcol < r.x + config.GUTTER and mrow >= r.y + BOX_H) {
+ // the tag row and the body's first, exactly as the select
+ // arm above derives them
+ const tag_y = if (p.tag_bottom) r.y + r.h -| BOX_H else r.y;
+ const body_y = if (p.tag_bottom) r.y else r.y + BOX_H;
+ const on_tag = mrow >= tag_y and mrow < tag_y + BOX_H;
+ if (mcol < r.x + config.GUTTER and !on_tag) {
// gutter: right scrolls DOWN to here, mirroring left's up;
// middle matches left. Right focuses (look lands you
// there); middle does not.
- const local = @as(i32, mrow) - @as(i32, r.y + BOX_H);
+ const local = @as(i32, mrow) - @as(i32, body_y);
if (m.button == config.look_button) p.active = id;
pane.scrollBy(if (m.button == config.look_button) local else -local);
} else if (mcol >= r.x + config.GUTTER) {
if (m.button == config.look_button) p.active = id;
const c: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
- const v: i32 = @as(i32, mrow) - @as(i32, r.y);
+ // unlike the select button, execute and look sweep the
+ // TAG through this same Sel — that is how middle-clicking
+ // Del on a tagline runs it — so the tag row is row 0 and
+ // everything else counts from BOX_H, wherever the tag is
+ // physically drawn.
+ const v: i32 = if (on_tag) 0 else @as(i32, mrow) - @as(i32, body_y) + @as(i32, BOX_H);
pane.sel[@intFromEnum(m.button)] = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v };
// Once a body gesture starts over a native PDF, keep
// it native even when the point misses a letterbox or
@@ -9129,7 +9177,7 @@ pub const Pardes = struct {
// inert instead of executing hidden projected text.
// Tag clicks remain ordinary text/builtin gestures.
const pdf_native = if (comptime pdf_enabled)
- mrow >= r.y + BOX_H and p.pdfNativeReady(pane)
+ !on_tag and p.pdfNativeReady(pane)
else
false;
if (pdf_native) _ = p.beginPdfDrag(pane, mcol, mrow, true);
@@ -9163,7 +9211,7 @@ pub const Pardes = struct {
if (d.corner_idx) |k| if (k + 1 < p.col_n[c]) {
const a = p.rects[p.col_terms[c][k]];
const b = p.rects[p.col_terms[c][k + 1]];
- d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow);
+ d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow, p.tag_bottom);
} else {
d.cur_y = mrow;
};
@@ -9174,7 +9222,7 @@ pub const Pardes = struct {
if (k + 1 < p.col_n[cc]) {
const a = p.rects[p.col_terms[cc][k]];
const b = p.rects[p.col_terms[cc][k + 1]];
- d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow);
+ d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow, p.tag_bottom);
} else d.cur_y = mrow;
},
.move => |*d| {
@@ -9186,7 +9234,29 @@ pub const Pardes = struct {
const r = p.rects[s.id];
const b = @intFromEnum(s.button);
pane.sel[b].c1 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
- pane.sel[b].r1 = @as(i32, mrow) - @as(i32, r.y);
+ // A Tagbottom drag STAYS IN THE REGION ITS ANCHOR STARTED
+ // IN, which is the one place the two coordinate spaces
+ // genuinely disagree: Sel row 0 is the tag and the body
+ // counts up from BOX_H, but on screen the tag is now BELOW
+ // the body. Sweeping down past the last line onto the
+ // tagline — the ordinary "select to the end" gesture — would
+ // therefore drive r1 to the TOP of Sel space and invert the
+ // selection: the highlight jumps above the anchor, and a 1-2
+ // Cut on it deletes lines nobody swept. So a body-anchored
+ // drag is clamped to the body rows and a tag-anchored one
+ // (execute/look only; the left button's tag sweep is its own
+ // .tag drag) stays on the tag, which is one line anyway.
+ // The cost is that a body sweep can no longer be extended
+ // onto the bottom tagline to pick up the tag text.
+ // With the tag on TOP, Sel order IS screen order and the
+ // gesture is contiguous and correct, so nothing is clamped.
+ if (p.tag_bottom) {
+ const body_h = r.h -| BOX_H;
+ pane.sel[b].r1 = if (pane.sel[b].r0 < BOX_H or body_h == 0)
+ 0
+ else
+ @as(i32, @min(@max(mrow, r.y), r.y + body_h - 1)) - @as(i32, r.y) + @as(i32, BOX_H);
+ } else pane.sel[b].r1 = @as(i32, mrow) - @as(i32, r.y);
if (comptime pdf_enabled) {
if (s.pdf.native) {
if (p.updatePdfSelection(pane, mcol, mrow, false))
@@ -9406,7 +9476,11 @@ pub const Pardes = struct {
const ra = p.rects[p.col_terms[cc][k]];
const rb = p.rects[p.col_terms[cc][k + 1]];
const combined: f32 = @floatFromInt(ra.h + rb.h);
- var nt: f32 = @floatFromInt((cur_y + 1) -| ra.y);
+ // the handle is the upper pane's LAST row (inclusive, hence the +1), or
+ // with Tagbottom the lower pane's FIRST — which is that row's height
+ // already, with nothing to add. Either way a no-drag click hands back
+ // exactly ra.h and nothing moves.
+ var nt: f32 = @floatFromInt(if (p.tag_bottom) cur_y -| ra.y else (cur_y + 1) -| ra.y);
nt = std.math.clamp(nt, @as(f32, BOX_H), @max(@as(f32, BOX_H), combined - BOX_H));
const pair = a.vweight + b.vweight;
a.vweight = pair * (nt / combined);
@@ -10494,9 +10568,10 @@ pub const Pardes = struct {
const r = for (p.panes, 0..) |slot, i| {
if (slot == pane) break p.rects[i];
} else return null;
- if (mrow < r.y + BOX_H or mrow >= r.y + r.h) return null;
+ const body_y = if (p.tag_bottom) r.y else r.y + BOX_H;
+ if (mrow < body_y or mrow >= body_y + (r.h -| BOX_H)) return null;
const local_y = @as(f64, @floatFromInt(
- @as(u32, mrow - (r.y + BOX_H)) * p.cell_pixels.h + p.cell_pixels.h / 2,
+ @as(u32, mrow - body_y) * p.cell_pixels.h + p.cell_pixels.h / 2,
));
const page = pdfPageAtOffset(&pv, pv.document_scroll_y + local_y);
const document_y = pv.document_scroll_y + local_y;
@@ -10528,7 +10603,7 @@ pub const Pardes = struct {
if (slot == pane) break p.rects[i];
} else return null;
const body_x = @as(i64, r.x + config.GUTTER);
- const body_y = @as(i64, r.y + BOX_H);
+ const body_y = @as(i64, if (p.tag_bottom) r.y else r.y + BOX_H);
var px = (@as(i64, mcol) - body_x) * p.cell_pixels.w + p.cell_pixels.w / 2;
var py = (@as(i64, mrow) - body_y) * p.cell_pixels.h + p.cell_pixels.h / 2;
if (clamp_to_page) {
@@ -12049,7 +12124,10 @@ pub const Pardes = struct {
if (text.len == 0 and !leader_here) continue;
const tx = r.x + config.GUTTER;
const tw = r.w - config.GUTTER;
- const row = r.y + r.h - 1;
+ // ...at the OTHER end from the tagline, whichever end that is:
+ // Tagbottom moved the tag onto this row, so the twin moves to the
+ // pane's first. They stay a pair with the body between them.
+ const row = if (p.tag_bottom) r.y else r.y + r.h - 1;
// In the EDITOR's colours, not the tag bar's: this row is the one
// place the program talks back to you about the buffer you are in,
// and it reads as part of that buffer rather than as another strip
@@ -12153,7 +12231,9 @@ pub const Pardes = struct {
if (p.hover_col < p.col_x[cc] or p.hover_col >= p.col_x[cc] + p.col_w[cc]) continue;
for (0..p.col_n[cc] -| 1) |k| {
const r = p.rects[p.col_terms[cc][k]];
- if (p.hover_row == r.y + r.h -| 1) {
+ // the same seam the h-handle hit test picks (see there)
+ const seam = if (p.tag_bottom) r.y +| r.h else r.y + r.h -| 1;
+ if (p.hover_row == seam) {
var col = p.col_x[cc];
while (col < p.col_x[cc] + p.col_w[cc]) : (col += 1) s.overlayDash(col, p.hover_row, "╌");
}
@@ -12227,6 +12307,12 @@ pub const Pardes = struct {
const chrome = p.chromeTheme();
const tx = r.x + config.GUTTER; // text area (tag + body), right of the gutter
const tw = r.w - config.GUTTER;
+ // The pane's two anchor rows, computed once: the tagline's, and the
+ // body's first. The Tagbottom builtin swaps which end each is at and
+ // NOTHING else in here reads r.y — that is the whole of the feature on
+ // the render side. r.h == 0 returned above, so the bottom row exists.
+ const tag_y = if (p.tag_bottom) r.y + r.h - 1 else r.y;
+ const body_y = if (p.tag_bottom) r.y else r.y + BOX_H;
// the pane's own background, for everything that has to read as "no
// chrome here": the body text, and the blank right half of the
// scrollbar's second column. `.default` means the host terminal's own
@@ -12272,9 +12358,9 @@ pub const Pardes = struct {
const box_lum = (@as(u16, box_bg[0]) * 3 + @as(u16, box_bg[1]) * 6 + @as(u16, box_bg[2])) / 10;
const box_ink: [3]u8 = if (box_lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
const box_style: CellStyle = .{ .fg = .{ .rgb = box_ink }, .bg = .{ .rgb = box_bg } };
- s.fill(r.x, r.y, config.GUTTER, BOX_H, box_style);
+ s.fill(r.x, tag_y, config.GUTTER, BOX_H, box_style);
const box_mode = if (pane.tag_edit) pane.tag_mode else pane.mode;
- s.set(r.x, r.y, switch (box_mode) {
+ s.set(r.x, tag_y, switch (box_mode) {
.normal => config.tag_normal,
.insert => config.tag_insert,
.tty => config.tag_tty,
@@ -12282,7 +12368,7 @@ pub const Pardes = struct {
// tag: live prefix (cwd/path) + editable tail, on a dim bar
const tag_style: CellStyle = .{ .fg = .{ .rgb = chrome.tag_fg }, .bg = .{ .rgb = chrome.tag_bg } };
- s.fill(tx, r.y, tw, BOX_H, .{ .bg = .{ .rgb = chrome.tag_bg } });
+ s.fill(tx, tag_y, tw, BOX_H, .{ .bg = .{ .rgb = chrome.tag_bg } });
const tag = try p.tagText(arena, pane);
// An armed input — `/`, Find, Grep, Rename, WsSymbols, Select/Split,
// `|` — is still TYPED INTO the tag tail: same buffer, same offsets,
@@ -12301,7 +12387,7 @@ pub const Pardes = struct {
@min(tag.len, tag.len - @min(tag.len, pane.tag_tail.items.len) + at)
else
tag.len;
- _ = s.print(tx, r.y, tw, tag[0..tag_cut], tag_style);
+ _ = s.print(tx, tag_y, tw, tag[0..tag_cut], tag_style);
// tag char selection highlight (helix v/x, or a tagline sweep),
// inclusive [lo, hi] — rendered-tag columns, so no conversion.
//
@@ -12315,7 +12401,7 @@ pub const Pardes = struct {
var col: usize = b.lo;
const end: usize = b.hi;
while (col <= end and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), r.y);
+ const cell = s.at(tx + @as(u16, @intCast(col)), tag_y);
cell.default = false;
cell.style.bg = .{ .rgb = th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
@@ -12324,7 +12410,7 @@ pub const Pardes = struct {
// cursor while editing the tag: a rendered-tag column, verbatim
if (active and pane.tag_edit) {
// bar while typing, block for `:` normal mode (same rule as a body)
- if (pane.tag_col < tw) s.cursor = .{ .x = tx + pane.tag_col, .y = r.y, .bar = pane.mode == .insert };
+ if (pane.tag_col < tw) s.cursor = .{ .x = tx + pane.tag_col, .y = tag_y, .bar = pane.mode == .insert };
}
// A native PDF page uses the same backend-neutral pixel attachment as
@@ -12332,8 +12418,8 @@ pub const Pardes = struct {
// extracted text projection becomes an ordinary readable body.
if (comptime pdf_enabled) {
if (hasPdf(pane) and p.native_images and p.drawPdf(pane, r, tx, tw)) {
- s.fill(r.x, r.y + BOX_H, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, r.y + BOX_H, 1, r.h -| BOX_H, .{ .bg = pane_bg });
+ s.fill(r.x, body_y, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, body_y, 1, r.h -| BOX_H, .{ .bg = pane_bg });
if (r.h > BOX_H) {
const pv = &pane.pdf.?;
const track_h: usize = r.h - BOX_H;
@@ -12353,7 +12439,7 @@ pub const Pardes = struct {
));
var sy = pos;
while (sy < track_h and sy < pos + len) : (sy += 1)
- s.fill(r.x, r.y + BOX_H + @as(u16, @intCast(sy)), 1, 1, .{ .bg = .{ .rgb = chrome.scroll_thumb } });
+ s.fill(r.x, body_y + @as(u16, @intCast(sy)), 1, 1, .{ .bg = .{ .rgb = chrome.scroll_thumb } });
}
return;
}
@@ -12366,8 +12452,8 @@ pub const Pardes = struct {
p.drawImage(pane, r, tx, tw);
// thumbless, but the same one column as the real scrollbar below —
// that is the whole point of drawing it
- s.fill(r.x, r.y + BOX_H, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, r.y + BOX_H, 1, r.h -| BOX_H, .{ .bg = pane_bg });
+ s.fill(r.x, body_y, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, body_y, 1, r.h -| BOX_H, .{ .bg = pane_bg });
return;
}
@@ -12384,7 +12470,7 @@ pub const Pardes = struct {
var i: u16 = 0;
while (it.next()) |line| : (i += 1) {
if (i >= body_h) break;
- _ = s.print(tx, r.y + BOX_H + i, tw, line, body_style);
+ _ = s.print(tx, body_y + i, tw, line, body_style);
}
// the two file-only passes and the terminal-only one: each is the
@@ -12409,7 +12495,7 @@ pub const Pardes = struct {
var wr: u16 = 0;
while (wr + 1 < pane.wrap_n and wr + 1 < body_h) : (wr += 1) {
if (pane.wrap_line[wr + 1] != pane.wrap_line[wr]) continue;
- s.set(tx + tw - 1, r.y + BOX_H + wr, config.wrap_marker, .{
+ s.set(tx + tw - 1, body_y + wr, config.wrap_marker, .{
.fg = .{ .rgb = chrome.lineno },
.bg = pane_bg,
});
@@ -12454,9 +12540,12 @@ pub const Pardes = struct {
var row: u16 = 0;
while (row < r.h) : (row += 1) {
if (@as(i32, row) < r0 or @as(i32, row) > r1) continue;
+ // a Sel row, so 0 is the tag and BOX_H upward is the body —
+ // the one place the two coordinate spaces meet on the render side
+ const sy = if (row < BOX_H) tag_y else body_y + row - BOX_H;
var col: i32 = if (pane.file != null) @max(c0, @as(i32, config.PREFIX_W)) else c0;
while (col <= c1 and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), r.y + row);
+ const cell = s.at(tx + @as(u16, @intCast(col)), sy);
cell.default = false;
cell.style.bg = .{ .rgb = sel_btn[b] };
cell.style.fg = .{ .rgb = th.sel_fg };
@@ -12476,7 +12565,7 @@ pub const Pardes = struct {
if (ar < lo or ar > hi) continue;
var col: u16 = if (pane.file != null) config.PREFIX_W else 0;
while (col < tw) : (col += 1) {
- const cell = s.at(tx + col, r.y + row);
+ const cell = s.at(tx + col, body_y + row - BOX_H);
cell.default = false;
cell.style.bg = .{ .rgb = th.sel_bg };
cell.style.fg = .{ .rgb = th.sel_fg };
@@ -12531,12 +12620,11 @@ pub const Pardes = struct {
while (vr + @as(i32, BOX_H) < @as(i32, r.h)) : (vr += 1) {
const w = pane.wrapAt(vr);
if (w.line < bnd.lo_row or w.line > bnd.hi_row) continue;
- const prow = vr + @as(i32, BOX_H);
const cstart: i32 = if (w.line == bnd.lo_row) bnd.lo_col - w.at + vpfx else vpfx;
const cend: i32 = if (w.line == bnd.hi_row) bnd.hi_col - w.at + vpfx else @as(i32, tw) - 1;
var col: i32 = @max(cstart, vpfx);
while (col <= cend and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), r.y + @as(u16, @intCast(prow)));
+ const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(vr)));
cell.default = false;
cell.style.bg = .{ .rgb = bg };
cell.style.fg = .{ .rgb = th.sel_fg };
@@ -12547,7 +12635,7 @@ pub const Pardes = struct {
const crow = cw.row + @as(i32, BOX_H);
const ccol = sr.col - cw.at + vpfx;
if (crow >= BOX_H and crow < @as(i32, r.h) and ccol >= vpfx and ccol < tw) {
- const cell = s.at(tx + @as(u16, @intCast(ccol)), r.y + @as(u16, @intCast(crow)));
+ const cell = s.at(tx + @as(u16, @intCast(ccol)), body_y + @as(u16, @intCast(crow - BOX_H)));
cell.default = false;
cell.style.bg = .{ .rgb = th.sel_fg };
cell.style.fg = .{ .rgb = th.sel_bg };
@@ -12574,9 +12662,9 @@ pub const Pardes = struct {
else
ccol;
if (prow >= BOX_H and cx >= 0 and prow < r.h and cx < tw)
- s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y + @as(u16, @intCast(prow)), .bar = pane.mode == .insert };
+ s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - BOX_H)), .bar = pane.mode == .insert };
} else if (cur.y + BOX_H < r.h and cur.x < tw) {
- s.cursor = .{ .x = tx + cur.x, .y = r.y + cur.y + BOX_H, .bar = pane.mode == .insert };
+ s.cursor = .{ .x = tx + cur.x, .y = body_y + cur.y, .bar = pane.mode == .insert };
}
}
@@ -12600,8 +12688,8 @@ pub const Pardes = struct {
// config.GUTTER columns and the click handlers still scroll on any of
// them, so the blank column is still live. Only the ink narrowed.
if (r.h > BOX_H) {
- s.fill(r.x, r.y + BOX_H, 1, r.h - BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, r.y + BOX_H, 1, r.h - BOX_H, .{ .bg = pane_bg });
+ s.fill(r.x, body_y, 1, r.h - BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, body_y, 1, r.h - BOX_H, .{ .bg = pane_bg });
const sb: struct { total: usize, offset: usize, len: usize } = if (pane.file) |*f| .{
.total = file_pane.nlines(p.gpa, f),
.offset = f.scroll,
@@ -12620,7 +12708,7 @@ pub const Pardes = struct {
const pos = (track_h * sb.offset) / total;
var sy: usize = pos;
while (sy < track_h and sy < pos + len) : (sy += 1) {
- s.fill(r.x, r.y + BOX_H + @as(u16, @intCast(sy)), 1, 1, .{ .bg = .{ .rgb = chrome.scroll_thumb } });
+ s.fill(r.x, body_y + @as(u16, @intCast(sy)), 1, 1, .{ .bg = .{ .rgb = chrome.scroll_thumb } });
}
}
}
@@ -12757,7 +12845,7 @@ pub const Pardes = struct {
.pixel_offset_y = placed.pixel_offset_y,
},
.x = tx,
- .y = r.y + BOX_H,
+ .y = if (p.tag_bottom) r.y else r.y + BOX_H,
.w = tw,
.h = r.h - BOX_H,
.rgba = slot.rgba,
@@ -12787,6 +12875,7 @@ pub const Pardes = struct {
if (iv.rgba.len == 0 or r.h <= BOX_H) return;
const body_cols = tw;
const body_rows = r.h - BOX_H;
+ const body_y = if (p.tag_bottom) r.y else r.y + BOX_H;
const use_petscii = iv.petscii or !p.native_images;
if (!use_petscii) {
if (!p.appendImagePlace(.{
@@ -12795,7 +12884,7 @@ pub const Pardes = struct {
} else 0),
.serial = pane.serial,
.x = tx,
- .y = r.y + BOX_H,
+ .y = body_y,
.w = body_cols,
.h = body_rows,
.rgba = iv.rgba,
@@ -12839,7 +12928,7 @@ pub const Pardes = struct {
.terminal => .{ .index = c.bg },
};
const sx = tx + @as(u16, @intCast(offx + cx));
- const sy = r.y + BOX_H + @as(u16, @intCast(offy + cy));
+ const sy = body_y + @as(u16, @intCast(offy + cy));
if (sx < s.cols and sy < s.rows)
s.set(sx, sy, c.glyph[0..c.glen], .{ .fg = fg, .bg = bg });
}
diff --git a/src/term_pane.zig b/src/term_pane.zig
index 1c03afa7..47123ceb 100644
--- a/src/term_pane.zig
+++ b/src/term_pane.zig
@@ -99,6 +99,7 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, off: i32) void {
const s = &p.surface;
const goff: i32 = @intCast(pane.vt.screens.active.pages.scrollbar().offset);
+ const body_y = if (p.tag_bottom) r.y else r.y + pardes.BOX_H;
var vr: u16 = 0;
while (vr < body_h and vr < pane.rows) : (vr += 1) {
const grid = pane.gridRow(off + @as(i32, vr)) - goff;
@@ -110,7 +111,7 @@ pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bo
while (c < tw) : (c += 1) {
const ci = pane.vt.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(c), .y = @intCast(grid) } }) orelse continue;
if (ci.cell.wide == .spacer_tail) continue;
- const cell = s.at(tx + c, r.y + pardes.BOX_H + vr);
+ const cell = s.at(tx + c, body_y + vr);
if (cell.default) continue;
cell.style = cellStyle(p, ci);
}
diff --git a/src/web.zig b/src/web.zig
index 42359329..b8abf27e 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -489,13 +489,19 @@ fn putU32(s: *State, value: u32) void {
const Target = struct { col: u16, row: u16 };
+/// The tag row and the stacked-pair seam, the way the core's own hit test
+/// derives them: Tagbottom puts a pane's tag on its LAST row, which makes its
+/// FIRST row an ordinary scrollable body row and moves the h-handle down to
+/// the lower pane's first. Kept a line-for-line clone of gui.zig's
+/// webChromeTarget — the two must classify a finger identically.
fn chromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?Target {
if (row < pardes.TOPBAR_H) return .{ .col = col, .row = row };
for (core.panes, 0..) |slot, id| {
if (slot == null) continue;
const rect = core.rects[id];
- if (row == rect.y and col >= rect.x and col < rect.x + rect.w and col < rect.x + pardes.config.GUTTER)
- return .{ .col = col, .row = rect.y };
+ const tag = if (core.tag_bottom) rect.y + rect.h -| pardes.BOX_H else rect.y;
+ if (row == tag and col >= rect.x and col < rect.x + rect.w and col < rect.x + pardes.config.GUTTER)
+ return .{ .col = col, .row = tag };
}
for (0..core.ncol -| 1) |column| {
const handle = core.col_x[column] + core.col_w[column] -| 1;
@@ -505,15 +511,16 @@ fn chromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?Target {
if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
for (0..core.col_n[column] -| 1) |index| {
const rect = core.rects[core.col_terms[column][index]];
- const handle = rect.y + rect.h -| 1;
+ const handle = if (core.tag_bottom) rect.y +| rect.h else rect.y + rect.h -| 1;
if (row == handle) return .{ .col = col, .row = handle };
}
}
for (core.panes, 0..) |slot, id| {
if (slot == null) continue;
const rect = core.rects[id];
- if (row == rect.y and col >= rect.x and col < rect.x + rect.w)
- return .{ .col = col, .row = rect.y };
+ const tag = if (core.tag_bottom) rect.y + rect.h -| pardes.BOX_H else rect.y;
+ if (row == tag and col >= rect.x and col < rect.x + rect.w)
+ return .{ .col = col, .row = tag };
}
for (0..core.ncol -| 1) |column| {
const handle = core.col_x[column] + core.col_w[column] -| 1;
@@ -523,7 +530,7 @@ fn chromeTarget(core: *const pardes.Pardes, col: u16, row: u16) ?Target {
if (col < core.col_x[column] or col >= core.col_x[column] + core.col_w[column]) continue;
for (0..core.col_n[column] -| 1) |index| {
const rect = core.rects[core.col_terms[column][index]];
- const handle = rect.y + rect.h -| 1;
+ const handle = if (core.tag_bottom) rect.y +| rect.h else rect.y + rect.h -| 1;
if (distance(row, handle) == 1) return .{ .col = col, .row = handle };
}
}