diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 91 |
1 files changed, 52 insertions, 39 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index bd7c43e9..d115bf2d 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -84,6 +84,8 @@ pub const TOPBAR_H: u16 = 1; pub const COLBAR_H: u16 = 1; pub const TagLine = @import("tag_line.zig").Line; pub const BOX_H: u16 = 1; +pub const TAG_GAP: u16 = 1; +pub const TAG_TEXT_INSET: u16 = config.GUTTER + TAG_GAP; pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 { const spare = cell_h -| tagline_h; @@ -1005,7 +1007,7 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" { const pane = p.panes[0].?; try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(pane)); - const tag_x = p.rects[0].x + config.GUTTER; + const tag_x = p.rects[0].x + TAG_TEXT_INSET; const tag_y = p.rects[0].y; const rendered = try p.tagText(p.scratch.allocator(), pane); @@ -1222,7 +1224,7 @@ test "Togglettymode builtin and keyboard chord share mode transitions including p.acknowledgePanelPresentation(&.{}); const tag = try p.tagText(p.scratch.allocator(), pane); - const x = p.rects[0].x + config.GUTTER + @as(u16, @intCast(std.mem.indexOf(u8, tag, "Togglettymode").?)); + const x = p.rects[0].x + TAG_TEXT_INSET + @as(u16, @intCast(std.mem.indexOf(u8, tag, "Togglettymode").?)); const y = p.rects[0].y; p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = x, .row = y } }); @@ -4381,8 +4383,8 @@ test "editable workspace and column tags replace selections and persist" { p.update(.{ .key = .{ .cp = 'H', .text = "Help" } }); try std.testing.expect(std.mem.startsWith(u8, p.global_tag.slice(""), "Help Joincol")); p.update(.{ .key = .{ .cp = Key.escape } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); p.update(.{ .key = .{ .cp = 'é', .text = "é" } }); try std.testing.expect(std.mem.startsWith(u8, p.columnTag(0), "éNew")); p.update(.{ .key = .{ .cp = Key.backspace } }); @@ -4427,8 +4429,8 @@ test "column tags render edit hover and scroll from the column edge" { var frame = std.heap.ArenaAllocator.init(std.testing.allocator); defer frame.deinit(); for (0..p.ncol) |column| { - const x = p.col_x[column] + config.GUTTER; - const width = p.col_w[column] - config.GUTTER; + const x = p.col_x[column] + TAG_TEXT_INSET; + const width = p.col_w[column] - TAG_TEXT_INSET; const initial = try p.render(frame.allocator()); for ("New", 0..) |byte, offset| try std.testing.expectEqual(byte, initial.at(x + @as(u16, @intCast(offset)), TOPBAR_H).text[0]); @@ -4481,8 +4483,8 @@ test "column tag first-cell execution keeps New local to the clicked column" { p.active = p.col_panes[1 - column][0]; p.acknowledgePanelPresentation(&.{}); const before = p.col_n; - p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = p.col_x[column] + config.GUTTER, .row = TOPBAR_H } }); - p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = p.col_x[column] + config.GUTTER, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = p.col_x[column] + TAG_TEXT_INSET, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = p.col_x[column] + TAG_TEXT_INSET, .row = TOPBAR_H } }); try std.testing.expectEqual(before[column] + 1, p.col_n[column]); try std.testing.expectEqual(before[1 - column], p.col_n[1 - column]); try std.testing.expectEqual(column, layout.findPane(p, p.active).?.col); @@ -4527,8 +4529,8 @@ test "ColumnTags reclaims the row and releases hidden header focus without losin p.acknowledgePanelPresentation(&.{}); const shown = p.rects[0]; try std.testing.expectEqual(TOPBAR_H + COLBAR_H, shown.y); - p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); try std.testing.expect(p.header_column != null and p.topbar_col != null); p.update(.{ .command = "ColumnTags" }); @@ -4544,8 +4546,8 @@ test "ColumnTags reclaims the row and releases hidden header focus without losin try std.testing.expect(p.settings.column_tags); try std.testing.expectEqual(shown, p.rects[0]); p.acknowledgePanelPresentation(&.{}); - p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = config.GUTTER, .row = TOPBAR_H } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = config.GUTTER, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } }); try std.testing.expect(p.header_column != null); p.update(.{ .resize = .{ .cols = 60, .rows = 2 } }); try std.testing.expect(p.settings.column_tags); @@ -6274,7 +6276,7 @@ pub const Pardes = struct { return; }; const text = p.headerText(); - const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] -| config.GUTTER else p.screen_w); + const width = p.tagCapacity(if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else p.screen_w); const col = panes.File.rawDisplayCol(text, @min(cur, text.len)); if (col < p.header_scroll) p.header_scroll = col; if (col >= p.header_scroll + width) p.header_scroll = col -| (width -| 1); @@ -7377,7 +7379,7 @@ pub const Pardes = struct { pane.tag_col = @intCast(@min(edit0 + lead, end)); } if (pane.tag_saved_col == null) if (p.paneIdOf(pane)) |id| { - const width = p.tagCapacity(p.rects[id].w -| config.GUTTER); + const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET); // Reveal the suffix together with its commands, not merely the // first command letter at the viewport's far right edge. pane.tag_scroll = @intCast(panes.File.displayWidth(text) + 1 -| width); @@ -11086,6 +11088,7 @@ pub const Pardes = struct { const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y; const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H; const on_tag = row >= tag_y and row < tag_y + BOX_H; + if (on_tag and col < r.x + TAG_TEXT_INSET) return null; if (!on_tag) if (body_hit) |supplied| { const mapped_hit = p.bodyHitForPane(id, supplied, false) orelse return null; const c = @as(i32, mapped_hit.col) - config.GUTTER; @@ -11093,7 +11096,7 @@ pub const Pardes = struct { return .{ .sel = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v }, .on_tag = false }; }; const tag_col = if (on_tag) p.tagColumn(.pane, id, tag_hit, false) else null; - const c: i32 = (if (tag_col) |value| @as(i32, value) else @as(i32, col) - @as(i32, r.x + config.GUTTER)) + + const c: i32 = (if (tag_col) |value| @as(i32, value) else @as(i32, col) - @as(i32, r.x + (if (on_tag) TAG_TEXT_INSET else config.GUTTER))) + (if (on_tag) @as(i32, p.panes[id].?.tag_scroll) else 0); const v: i32 = if (on_tag) 0 else @as(i32, row) - @as(i32, body_y) + @as(i32, BOX_H); return .{ @@ -11324,7 +11327,7 @@ pub const Pardes = struct { } } if (p.header_drag and m.button == config.select_button and (m.kind == .drag or m.kind == .release)) { - const x = if (p.header_column) |c| p.col_x[c] + config.GUTTER else 0; + const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0; const bar = p.headerText(); p.topbar_col = @intCast(@min(bar.len, panes.File.rawAtDisplay(bar, (p.tagColumn(if (p.header_column != null) .column else .workspace, p.header_column orelse 0, p.pointer_tag_hit, p.header_drag) orelse (mcol -| x)) + p.header_scroll))); if (m.kind == .release) p.header_drag = false; @@ -11341,13 +11344,13 @@ pub const Pardes = struct { if (mrow >= TOPBAR_H) { for (0..p.ncol) |c| if (mcol >= p.col_x[c] and mcol < p.col_x[c] + p.col_w[c]) { column = c; - x = p.col_x[c] + config.GUTTER; + x = p.col_x[c] + TAG_TEXT_INSET; if (layout.findPane(p, p.active)) |active_col| { if (active_col.col != c and p.col_n[c] > 0) p.active = p.col_panes[c][0]; } break; }; - if (column == null) return; + if (column == null or mcol < x) return; } if (p.header_column != column) { p.rememberHeader(); @@ -11478,8 +11481,9 @@ pub const Pardes = struct { p.active = id; pane.scrollBy(-(@as(i32, mrow) - @as(i32, body_y))); } else if (mrow >= tag_y and mrow < tag_y + BOX_H) { + if (mcol < r.x + TAG_TEXT_INSET) return; p.active = id; - p.enterTagEdit(pane, if (p.tagColumn(.pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + config.GUTTER)); + p.enterTagEdit(pane, if (p.tagColumn(.pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + TAG_TEXT_INSET)); pane.tag_anchor = pane.tag_col; p.drag = .{ .tag = .{ .id = id } }; } else { @@ -11634,14 +11638,14 @@ pub const Pardes = struct { 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 = if (pane.sel[b].r0 < BOX_H) 0 else @as(i32, mrow) - @as(i32, r.y); - if (pane.sel[b].r1 < BOX_H) pane.sel[b].c1 += pane.tag_scroll; + if (pane.sel[b].r1 < BOX_H) pane.sel[b].c1 += @as(i32, pane.tag_scroll) - TAG_GAP; if (comptime pdf_enabled) panes.Pdf.pointerUpdate(&s.pdf, p, pane, mcol, mrow); } }, .tag => |d| { const pane = p.panes[d.id] orelse return; - const c = if (p.tagColumn(.pane, d.id, p.pointer_tag_hit, true)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, p.rects[d.id].x + config.GUTTER); + const c = if (p.tagColumn(.pane, d.id, p.pointer_tag_hit, true)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, p.rects[d.id].x + TAG_TEXT_INSET); const text = p.tagText(p.scratch.allocator(), pane) catch return; pane.tag_col = @intCast(@min(text.len, panes.File.rawAtDisplay(text, @as(usize, @intCast(@max(0, c))) + pane.tag_scroll))); pane.tag_sel = pane.tag_col != pane.tag_anchor; @@ -13256,8 +13260,9 @@ pub const Pardes = struct { }; if (p.columnBarHeight() > 0) for (0..p.ncol) |c| { const grip = @min(config.GUTTER, p.col_w[c]); - const x = p.col_x[c] + grip; - const w = p.col_w[c] - grip; + const inset = @min(TAG_TEXT_INSET, p.col_w[c]); + const x = p.col_x[c] + inset; + const w = p.col_w[c] - inset; if (p.col_w[c] == 0 or p.col_x[c] >= s.cols) continue; const active_col = if (layout.findPane(p, p.active)) |f| f.col == c else false; const bg = if (active_col and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg; @@ -13267,6 +13272,7 @@ pub const Pardes = struct { const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 }; const grip_style: CellStyle = .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline }; s.fill(p.col_x[c], TOPBAR_H, grip, 1, grip_style); + s.fill(p.col_x[c] + grip, TOPBAR_H, inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); s.fill(x, TOPBAR_H, w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); const bar_scroll = if (p.topbar_col != null and p.header_column == c) p.header_scroll else 0; @@ -13291,8 +13297,8 @@ pub const Pardes = struct { }; if (p.topbar_col) |cur| { const bar = p.headerText(); - const x = if (p.header_column) |c| p.col_x[c] + config.GUTTER else 0; - const w = if (p.header_column) |c| p.col_w[c] -| config.GUTTER else s.cols; + const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0; + const w = if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else s.cols; const y: u16 = if (p.header_column != null) TOPBAR_H else 0; const anchor = p.header_anchor orelse cur; var col = panes.File.rawDisplayCol(bar, @min(cur, anchor)) -| p.header_scroll; @@ -13631,8 +13637,10 @@ pub const Pardes = struct { // row zero in Sel space and starts at its real first text cell. const scroll: i32 = if (row < BOX_H) pane.tag_scroll else 0; var col: i32 = if (pane.file != null and row >= BOX_H) @max(c0, @as(i32, panes.File.gutterWidth(pane))) else @max(0, c0 - scroll); - while (col <= c1 - scroll and col < tw) : (col += 1) { - const cell = s.at(tx + @as(u16, @intCast(col)), sy); + const row_x = if (row < BOX_H) r.x + @min(TAG_TEXT_INSET, r.w) else tx; + const row_width = if (row < BOX_H) r.w -| TAG_TEXT_INSET else tw; + while (col <= c1 - scroll and col < row_width) : (col += 1) { + const cell = s.at(row_x + @as(u16, @intCast(col)), sy); cell.default = false; cell.style.bg = .{ .rgb = bg }; if (fg) |ink| cell.style.fg = .{ .rgb = ink }; @@ -13645,41 +13653,45 @@ pub const Pardes = struct { for (p.panes, 0..) |slot, id| { const pane = slot orelse continue; const r = p.rects[id]; - if (r.w <= config.GUTTER or r.h == 0) continue; + if (r.w <= TAG_TEXT_INSET or r.h == 0) continue; const tag = try p.tagText(arena, pane); - const columns: u16 = @intCast(@min(std.math.maxInt(u16) - config.GUTTER, @max(p.tagCapacity(r.w - config.GUTTER), panes.File.displayWidth(tag) + 1))); + const columns: u16 = @intCast(@min(std.math.maxInt(u16) - TAG_TEXT_INSET, @max(p.tagCapacity(r.w - TAG_TEXT_INSET), panes.File.displayWidth(tag) + 1))); const layer = &p.surface.tag_layers[id]; if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns); - const storage = try arena.alloc(Cell, @as(usize, columns) + config.GUTTER); + const storage = try arena.alloc(Cell, @as(usize, columns) + TAG_TEXT_INSET); @memset(storage, .{}); - var temporary: Surface = .{ .cols = columns + config.GUTTER, .rows = 1, .cells = storage }; + var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = 1, .cells = storage }; std.mem.swap(Surface, &p.surface, &temporary); - p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + config.GUTTER, .h = 1 }, id, id == p.active, p.tagCapacity(r.w - config.GUTTER)) catch |err| { + p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = 1 }, id, id == p.active, p.tagCapacity(r.w - TAG_TEXT_INSET)) catch |err| { std.mem.swap(Surface, &p.surface, &temporary); return err; }; std.mem.swap(Surface, &p.surface, &temporary); - @memcpy(layer.cells, storage[config.GUTTER..]); + @memcpy(layer.cells, storage[TAG_TEXT_INSET..]); layer.kind = .pane; layer.id = @intCast(id); layer.serial = pane.serial; - layer.viewport = .{ .x = r.x + config.GUTTER, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - config.GUTTER, .h = BOX_H }; + layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - TAG_TEXT_INSET, .h = BOX_H }; layer.cols = columns; layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg; - layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - config.GUTTER, .bar = temporary.cursor.?.bar } else null; + layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null; } var text_buf: [1200]u8 = undefined; try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg); if (p.columnBarHeight() > 0) for (0..p.ncol) |column| { if (p.col_w[column] == 0) continue; const active = if (layout.findPane(p, p.active)) |found| found.col == column and p.settings.focus_tint else false; - try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(config.GUTTER, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| config.GUTTER, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg); + try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg); }; } fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: TagKind, id: u16, viewport: Rect, text: []const u8, bg: [3]u8, fg: [3]u8) !void { _ = arena; const layer = &p.surface.tag_layers[index]; + if (viewport.w == 0) { + layer.cols = 0; + return; + } const columns: u16 = @intCast(@min(std.math.maxInt(u16), @max(p.tagCapacity(viewport.w), panes.File.displayWidth(text) + 1))); if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns); layer.kind = kind; @@ -13815,7 +13827,7 @@ pub const Pardes = struct { .tty => config.tag_tty, }, box_style); - try p.paintPaneTag(arena, pane, r, id, active, p.tagCapacity(tw)); + try p.paintPaneTag(arena, pane, r, id, active, p.tagCapacity(r.w -| TAG_TEXT_INSET)); // Folded panes retain their body state, but neither body pixels nor // its cursor belong outside the one-row tag rectangle. @@ -13840,8 +13852,8 @@ pub const Pardes = struct { const s = &p.surface; const th = p.theme(); const chrome = p.chromeTheme(); - const tx = r.x + config.GUTTER; - const tw = r.w - config.GUTTER; + const tx = r.x + @min(TAG_TEXT_INSET, r.w); + const tw = r.w -| TAG_TEXT_INSET; const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y; const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H; const page_rgb = th.bg orelse th.tag_bg; @@ -13851,6 +13863,7 @@ pub const Pardes = struct { const tag_style: CellStyle = .{ .fg = .{ .rgb = tag_fg }, .bg = .{ .rgb = tag_bg }, .font_role = .tagline }; // Padding belongs to the same font row but keeps its default ink: a // role must not manufacture a foreground color for blank cells. + s.fill(r.x + config.GUTTER, tag_y, @min(TAG_GAP, r.w -| config.GUTTER), BOX_H, .{ .bg = .{ .rgb = tag_bg }, .font_role = .tagline }); s.fill(tx, tag_y, tw, BOX_H, .{ .bg = .{ .rgb = tag_bg }, .font_role = .tagline, |
