diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-09 01:26:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 09c35b22793153d201fa202b52671a6442e5e2e1 (patch) | |
| tree | 1eb84c955dc96f4c6bcb75b36f6113ccad8e2f1e | |
| parent | 475cc49ccbe9e81f8ec8aa2371c20fc632d110b9 (diff) | |
| download | pardes-09c35b22793153d201fa202b52671a6442e5e2e1.tar.gz pardes-09c35b22793153d201fa202b52671a6442e5e2e1.zip | |
a builtin to put the pane taglines at the bottom
| -rw-r--r-- | src/builtins.zig | 12 | ||||
| -rw-r--r-- | src/config.zig | 1 | ||||
| -rw-r--r-- | src/file_pane.zig | 7 | ||||
| -rw-r--r-- | src/gui/gui.zig | 31 | ||||
| -rw-r--r-- | src/pardes.zig | 199 | ||||
| -rw-r--r-- | src/term_pane.zig | 3 | ||||
| -rw-r--r-- | src/web.zig | 19 | ||||
| -rw-r--r-- | test/snapshots/builtins.golden | 84 | ||||
| -rw-r--r-- | test/snapshots/leader.golden | 26 | ||||
| -rw-r--r-- | test/snapshots/leader.snap | 7 | ||||
| -rw-r--r-- | test/snapshots/tagbottom.golden | 549 | ||||
| -rw-r--r-- | test/snapshots/tagbottom.snap | 154 |
12 files changed, 961 insertions, 131 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 }; } } diff --git a/test/snapshots/builtins.golden b/test/snapshots/builtins.golden index afee3f3f..7e4b3366 100644 --- a/test/snapshots/builtins.golden +++ b/test/snapshots/builtins.golden @@ -153,7 +153,6 @@ | | | /tmp/pardes-snap/builtins/cwd/+Help New Del -| 24 SPC l i Lspinfo | 25 SPC l k Hover | 26 SPC l r Rename | 27 SPC l s Symbols @@ -161,26 +160,27 @@ | 29 SPC s d Dump topbar | 30 SPC s r Restore | 31 SPC t a Ascii -| 32 SPC t c Colors -| 33 SPC t d Debug topbar -| 34 SPC t i PdfTint -| 35 SPC t l Palette -| 36 SPC t n NextColor topbar -| 37 SPC t p Petscii -| 38 SPC t r Crt -| 39 SPC t s PdfSections -| 40 SPC t t ThemeSel -| 41 SPC t w Wrap -| 42 SPC t z PdfFit -| 43 SPC w h Left C-w h, C-w left -| 44 SPC w j Down C-w j, C-w down -| 45 SPC w k Up C-w k, C-w up -| 46 SPC w l Right C-w l, C-w right -| 47 Look enter, right-click -| 48 Exec tab, middle-click -| 49 Theme -| 50 Shell -| 51 +| 32 SPC t b Tagbottom +| 33 SPC t c Colors +| 34 SPC t d Debug topbar +| 35 SPC t i PdfTint +| 36 SPC t l Palette +| 37 SPC t n NextColor topbar +| 38 SPC t p Petscii +| 39 SPC t r Crt +| 40 SPC t s PdfSections +| 41 SPC t t ThemeSel +| 42 SPC t w Wrap +| 43 SPC t z PdfFit +| 44 SPC w h Left C-w h, C-w left +| 45 SPC w j Down C-w j, C-w down +| 46 SPC w k Up C-w k, C-w up +| 47 SPC w l Right C-w l, C-w right +| 48 Look enter, right-click +| 49 Exec tab, middle-click +| 50 Theme +| 51 Shell +| 52 == snap not-a-picker grid=120x60 cursor=7,58 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/builtins/cwd/notes.txt Save New Del @@ -214,7 +214,6 @@ | | | /tmp/pardes-snap/builtins/cwd/+Help New Del -| 24 SPC l i Lspinfo | 25 SPC l k Hover | 26 SPC l r Rename | 27 SPC l s Symbols @@ -222,26 +221,27 @@ | 29 SPC s d Dump topbar | 30 SPC s r Restore | 31 SPC t a Ascii -| 32 SPC t c Colors -| 33 SPC t d Debug topbar -| 34 SPC t i PdfTint -| 35 SPC t l Palette -| 36 SPC t n NextColor topbar -| 37 SPC t p Petscii -| 38 SPC t r Crt -| 39 SPC t s PdfSections -| 40 SPC t t ThemeSel -| 41 SPC t w Wrap -| 42 SPC t z PdfFit -| 43 SPC w h Left C-w h, C-w left -| 44 SPC w j Down C-w j, C-w down -| 45 SPC w k Up C-w k, C-w up -| 46 SPC w l Right C-w l, C-w right -| 47 Look enter, right-click -| 48 Exec tab, middle-click -| 49 Theme -| 50 Shell -| 51 +| 32 SPC t b Tagbottom +| 33 SPC t c Colors +| 34 SPC t d Debug topbar +| 35 SPC t i PdfTint +| 36 SPC t l Palette +| 37 SPC t n NextColor topbar +| 38 SPC t p Petscii +| 39 SPC t r Crt +| 40 SPC t s PdfSections +| 41 SPC t t ThemeSel +| 42 SPC t w Wrap +| 43 SPC t z PdfFit +| 44 SPC w h Left C-w h, C-w left +| 45 SPC w j Down C-w j, C-w down +| 46 SPC w k Up C-w k, C-w up +| 47 SPC w l Right C-w l, C-w right +| 48 Look enter, right-click +| 49 Exec tab, middle-click +| 50 Theme +| 51 Shell +| 52 == snap window-group grid=120x60 cursor=7,32 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/builtins/cwd/notes.txt Save New Del diff --git a/test/snapshots/leader.golden b/test/snapshots/leader.golden index 9398b158..74719f13 100644 --- a/test/snapshots/leader.golden +++ b/test/snapshots/leader.golden @@ -393,19 +393,19 @@ | 1 pardes builtins under SPC t | 2 | 3 SPC t a Ascii -| 4 SPC t c Colors -| 5 SPC t d Debug topbar -| 6 SPC t i PdfTint -| 7 SPC t l Palette -| 8 SPC t n NextColor topbar -| 9 SPC t p Petscii -| 10 SPC t r Crt -| 11 SPC t s PdfSections -| 12 SPC t t ThemeSel -| 13 SPC t w Wrap -| 14 SPC t z PdfFit -| 15 -| +| 4 SPC t b Tagbottom +| 5 SPC t c Colors +| 6 SPC t d Debug topbar +| 7 SPC t i PdfTint +| 8 SPC t l Palette +| 9 SPC t n NextColor topbar +| 10 SPC t p Petscii +| 11 SPC t r Crt +| 12 SPC t s PdfSections +| 13 SPC t t ThemeSel +| 14 SPC t w Wrap +| 15 SPC t z PdfFit +| 16 | | == snap help-window grid=100x40 cursor=7,22 diff --git a/test/snapshots/leader.snap b/test/snapshots/leader.snap index c71aaee6..db212631 100644 --- a/test/snapshots/leader.snap +++ b/test/snapshots/leader.snap @@ -66,9 +66,10 @@ key ? wait 10000 SPC c n stable 700 15000 snap help-all -# `SPC t ?`: the toggle group, seven of them now that the image renderer -# switches are builtins instead of words in the image tag — Crt sits past this -# pane's last row in the full listing above, and only a group filter shows it +# `SPC t ?`: the toggle group, twelve of them now that the image renderer +# switches and the PDF ones are builtins instead of words in the image tag — +# Crt sits past this pane's last row in the full listing above, and only a +# group filter shows it key space key t key ? diff --git a/test/snapshots/tagbottom.golden b/test/snapshots/tagbottom.golden new file mode 100644 index 00000000..cc402fd1 --- /dev/null +++ b/test/snapshots/tagbottom.golden @@ -0,0 +1,549 @@ +== snap boot grid=100x30 cursor=7,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/tagbottom/cwd/top.txt Save New Del +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| 15 ooo +| 16 ppp +| 17 qqq +| 18 rrr +| 19 sss +| 20 ttt +| 21 uuu +| 22 vvv +| 23 www +| 24 xxx +| 25 yyy +| 26 zzz +| +| +== snap on grid=100x30 cursor=7,1 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| 15 ooo +| 16 ppp +| 17 qqq +| 18 rrr +| 19 sss +| 20 ttt +| 21 uuu +| 22 vvv +| 23 www +| 24 xxx +| 25 yyy +| 26 zzz +| +| +| /tmp/pardes-snap/tagbottom/cwd/top.txt Save New Del +== snap off grid=100x30 cursor=7,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/tagbottom/cwd/top.txt Save New Del +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| 15 ooo +| 16 ppp +| 17 qqq +| 18 rrr +| 19 sss +| 20 ttt +| 21 uuu +| 22 vvv +| 23 www +| 24 xxx +| 25 yyy +| 26 zzz +| +| +== snap tag-typed grid=100x30 cursor=60,29 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| 15 ooo +| 16 ppp +| 17 qqq +| 18 rrr +| 19 sss +| 20 ttt +| 21 uuu +| 22 vvv +| 23 www +| 24 xxx +| 25 yyy +| 26 zzz +| +| +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +== snap look-bottom-tag grid=100x30 cursor=7,16 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap body-cursor grid=100x30 cursor=9,5 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap seam-noop grid=100x30 cursor=9,5 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| 14 nnn +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap seam-dragged grid=100x30 cursor=9,5 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap wheel-body grid=100x30 cursor=9,1 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| 13 mmm +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap wheel-gutter grid=100x30 cursor=9,2 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| 11 kkk +| 12 lll +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap gutter-click grid=100x30 cursor=9,4 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap search-prompt grid=100x30 cursor=6,11 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| /ZZ +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== snap leader grid=100x30 cursor=9,13 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 2 bbb +| 3 ccc +| 4 ddd +| 5 eee +| 6 fff +| 7 ggg +| 8 hhh +| 9 iii +| 10 jjj +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| SPC +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== style sweep-down +|0: 0-56 #949494,#121212, 57-99 d,#121212, +|1: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-9 #bdbdbd,#080808, 10-99 d,#080808, +|2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-9 #bdbdbd,#080808, 10-99 d,#080808, +|3: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|4: 0-0 d,#121212, 1-1 d,#080808, 2-6 #949494,#121212, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|5: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|6: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|7: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|8: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|9: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-7 #080808,#9399f9, 8-9 #bdbdbd,#080808, 10-99 d,#080808, +|10: 0-1 #ffffff,#626262, 2-90 #949494,#121212, 91-99 d,#121212, +|11: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-11 #bdbdbd,#080808, 12-99 d,#080808, +|12: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-11 #bdbdbd,#080808, 12-99 d,#080808, +|13: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|14: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|15: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|16: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|17: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|19: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|24: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|25: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|26: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|27: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|28: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-99 d,#080808, +|29: 0-1 #ffffff,#262626, 2-79 #949494,#121212, 80-99 d,#121212, +== snap cut-down grid=100x30 cursor=7,4 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 jj +| 5 kkk +| 6 lll +| 7 mmm +| 8 nnn +| 9 ooo +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 ZZONE +| 2 ZZTWO +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save New Del +== file cut-buffer +aaa +bbb +ccc +jj +kkk +lll +mmm +nnn +ooo +ppp +qqq +rrr +sss +ttt +uuu +vvv +www +xxx +yyy +zzz +== end file +== snap del-bottom-tag grid=100x30 cursor=7,4 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| 1 aaa +| 2 bbb +| 3 ccc +| 4 jj +| 5 kkk +| 6 lll +| 7 mmm +| 8 nnn +| 9 ooo +| 10 ppp +| 11 qqq +| 12 rrr +| 13 sss +| 14 ttt +| 15 uuu +| 16 vvv +| 17 www +| 18 xxx +| 19 yyy +| 20 zzz +| +| +| +| +| +| +| +| +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +== snap off-again grid=100x30 cursor=7,5 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/tagbottom/cwd/top.txt bottom.txt Save New Del +| 1 aaa +| 2 bbb +| 3 ccc +| 4 jj +| 5 kkk +| 6 lll +| 7 mmm +| 8 nnn +| 9 ooo +| 10 ppp +| 11 qqq +| 12 rrr +| 13 sss +| 14 ttt +| 15 uuu +| 16 vvv +| 17 www +| 18 xxx +| 19 yyy +| 20 zzz +| +| +| +| +| +| +| +| diff --git a/test/snapshots/tagbottom.snap b/test/snapshots/tagbottom.snap new file mode 100644 index 00000000..801df1a8 --- /dev/null +++ b/test/snapshots/tagbottom.snap @@ -0,0 +1,154 @@ +# Tagbottom: every pane's tagline on its LAST row instead of its first, and +# the transient message row (prompts, messages, the SPC indicator) at the other +# end with it. One global bool, so the whole screen flips at once. +# +# The two things worth pinning here are not pixels. The first is that a click +# still lands where it looks like it lands: the Sel coordinate space is +# UNCHANGED (row 0 is the tag, the body starts at BOX_H) and the mouse maps a +# screen row into it, so a look or an execute on a bottom tagline acts on the +# word under the pointer instead of silently sweeping whatever body row shares +# that number. Every mouse assertion below is therefore a CONSEQUENCE — a pane +# appearing, a pane going away, a view scrolling by a counted number of rows — +# and not a picture of a highlight. The tagline-execute pair is what caught the +# bug this script exists for: before the tag row got its own arm in the +# execute/look press, both ran a blank body row and a golden recorded the +# no-op as if it had passed. +# The second is that turning it back off restores the top-tagline layout +# exactly: `off` is byte-for-byte `boot`, cursor included. +file top.txt aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz +file bottom.txt ZZONE\nZZTWO +start 30 100 top.txt +wait 8000 New Newcol +wait 8000 top.txt +stable 700 20000 +snap boot +# SPC t b. The tag leaves row 1 for row 29, the body slides up one row, and +# the cursor goes with it (7,2 -> 7,1) because it is on the same file line. +key space t b +stable 700 15000 +snap on +# ...and straight back. This grid must equal `boot`'s, to the cell. +key space t b +stable 700 15000 +snap off +key space t b +stable 700 15000 +# CLICK the bottom tagline (col 50 of row 30, the pane's last row) and type +# into it. A clicked tag is insert mode, so the text lands as text, and the +# cursor is down on the tagline with it. Col 50 is past the end of the path, +# which is read-only — a click inside it parks the cursor there and the typing +# goes nowhere. +press left 50 30 +release left 50 30 +stable 400 5000 +send \x20bottom.txt +stable 600 8000 +stable 400 5000 +snap tag-typed +# ...and LOOK the word just typed, from that same bottom tagline: bottom.txt +# opens as a second pane below. It only can if the press was mapped to the tag +# — mapped as a body row it lands on blank line 29 and looks nothing at all. +press right 55 30 +release right 55 30 +wait 10000 ZZONE +stable 700 15000 +snap look-bottom-tag +# a body click in the UPPER pane: the cursor lands on the row under the +# pointer, counted from the pane's first row rather than from a tag that is no +# longer there. Row 6 is grid row 5, the fifth body row, file line 5. +press left 10 6 +release left 10 6 +stable 400 8000 +snap body-cursor +# The h-resize seam. With top taglines it is the upper pane's LAST row; here +# that row is the upper pane's TAG, so the handle is the lower pane's FIRST row +# (grid 16, click row 17). A no-drag click on it must leave both sizes alone. +press left 40 17 +release left 40 17 +motion 40 25 +stable 500 8000 +snap seam-noop +# ...and the real drag, five rows up: the upper pane keeps 10 rows, the lower +# grows to 19. Both taglines follow their own pane's bottom. +press left 40 17 +drag left 40 12 +release left 40 12 +motion 40 25 +stable 500 8000 +snap seam-dragged +# the wheel over the BODY of the upper pane (nine body rows now): four rows +# down, so the view starts at line 5 and the pinned cursor rides up to the +# pane's first row +wheel down 40 4 +wheel down 40 4 +wheel down 40 4 +wheel down 40 4 +stable 400 8000 +snap wheel-body +# ...and one row back up with the wheel over the GUTTER: the same pane, found +# through its gutter column. View at line 4. +wheel up 1 4 +stable 400 8000 +snap wheel-gutter +# a left click in the gutter scrolls UP to the clicked row, and the row it +# counts from is the BODY's first, not r.y + BOX_H. Row 4 is grid row 3, the +# third body row: two rows up, to line 2. Counted from the wrong end it would +# be one row up, to line 3. +press left 1 4 +release left 1 4 +stable 400 8000 +snap gutter-click +# `/` in the lower pane's body arms the search input. The prompt is CUT off the +# tagline and drawn on the message row with the cursor — and with Tagbottom +# those two are at opposite ends of the pane: the tail keeps its builtins on +# row 29 while `/ZZ` and the cursor sit up on row 11, the lower pane's first. +press left 10 14 +release left 10 14 +stable 400 5000 +key / +send ZZ +stable 600 8000 +stable 400 5000 +snap search-prompt +key esc +settle 100 +stable 400 5000 +# the SPC leader indicator lives on that same message row, right-aligned +key space +stable 400 5000 +snap leader +key esc +settle 100 +stable 400 5000 +# THE INVERSION. Sweeping DOWN past the last body row is the ordinary "select +# to the end" gesture, and with the tag drawn BELOW the body it lands on the +# tagline — Sel row 0. Unclamped that drives the selection's far end to the TOP +# of Sel space: the highlight jumps ABOVE the anchor and a 1-2 Cut deletes lines +# nobody swept. Anchored on grid row 3 (file line 4) and dragged onto the upper +# pane's tag row, the sweep must cover lines 4..10 downward, not 2..4 upward. +press left 8 4 +drag left 8 11 +stable 400 5000 +snapstyle sweep-down +# ...and the proof is the BUFFER, not the highlight: 1-2 Cut the sweep, save, +# and read the file back. aaa/bbb/ccc survive and kkk..zzz survive; unclamped +# this file lost bbb/ccc/ddd instead and kept eee onward. +press middle 8 11 +release middle 8 11 +release left 8 11 +stable 400 5000 +snap cut-down +key space f s +stable 700 15000 +snapfile cut-buffer /tmp/pardes-snap/tagbottom/cwd/top.txt +# EXECUTE `Del` on the lower pane's bottom tagline: the pane goes away and the +# upper one takes the whole column back. The middle button's half of the same +# tag mapping the look used above. +press middle 78 30 +release middle 78 30 +stable 700 15000 +snap del-bottom-tag +# and off again, one pane, its tagline back on row 1 with the body under it +key space t b +stable 700 15000 +snap off-again |
