diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 149 |
1 files changed, 97 insertions, 52 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index c66688f5..7e18db59 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -88,12 +88,19 @@ 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 { +pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32, workspace: bool) u32 { const spare = cell_h -| tagline_h; const border = topbarPaneBorderPixels(cell_h, tagline_h); + const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h)))))); + if (!workspace) { + // No workspace row: nothing above row 0 to share a rule with, so + // every band centers in its cell the way the middle rows always + // have. The anchor boxes this lets the hosts draw stay symmetric. + if (@as(u32, row) + 1 >= rows) return spare; + return spare / 2; + } if (row == 0) return spare - border / 2; if (row == TOPBAR_H) return border - border / 2; - const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h)))))); if (@as(u32, row) + 1 >= rows) return spare; return spare / 2; } @@ -104,8 +111,8 @@ pub fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 { } pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?layout.Track) f32 { - if (row < TOPBAR_H) return 0; - if (row < TOPBAR_H + p.columnBarHeight()) { + if (row < p.topBarHeight()) return 0; + if (row < p.topBarHeight() + p.columnBarHeight()) { for (p.col_x[0..p.ncol], p.col_w[0..p.ncol]) |x, w| if (col >= x and col - x < w) return @floatFromInt(x); } @@ -164,8 +171,8 @@ test "paint order is moving, then opening, then closing tombstones on top" { pub fn gridColAt(p: ?*const Pardes, x: f32, row: u16, body_w: f32, tagline_w: f32) u16 { const body = @max(body_w, 1); const tag = @max(tagline_w, 1); - if (row < TOPBAR_H) return colFromSpan(x, tag); - if (p) |core| if (row < TOPBAR_H + core.columnBarHeight()) { + if (row < (if (p) |core| core.topBarHeight() else TOPBAR_H)) return colFromSpan(x, tag); + if (p) |core| if (row < core.topBarHeight() + core.columnBarHeight()) { for (core.col_x[0..core.ncol], core.col_w[0..core.ncol]) |origin, width| { if (width == 0) continue; const left = @as(f32, @floatFromInt(origin)) * body; @@ -3403,6 +3410,12 @@ pub const Cell = struct { len: u8 = 1, style: CellStyle = .{}, default: bool = true, + /// The look-hover affordance is on this cell: the word under the pointer + /// is a real Look or Exec operand, and hosts that draw the quiet tint as + /// more than a flat fill — macOS renders it as a glass overlay — read + /// this instead of guessing from the background colour, which a real + /// selection shares. + hover: bool = false, pub fn grapheme(c: *const Cell) []const u8 { return c.text[0..c.len]; @@ -3678,8 +3691,12 @@ test "pixel attachment fit and pan do not invalidate cached pixels" { /// The canonical interface: what a frame of pardes IS. The tty shell writes /// these cells to vaxis one-to-one; the SDL shells rasterize them. pub const Surface = struct { - pub const PointerShape = enum(u8) { arrow, link }; + pub const PointerShape = enum(u8) { arrow, link, target }; pointer_shape: PointerShape = .arrow, + /// Set only for the duration of the look-hover paint, so the cells that + /// paint the quiet affordance can carry the hover bit through the ABI + /// without every selection painter learning a new parameter. + mark_hover: bool = false, body_layers: [MAX_PANES]BodyLayer = @splat(.{}), tag_layers: [MAX_TAG_LAYERS]TagLayer = @splat(.{}), cols: u16 = 0, @@ -3725,7 +3742,11 @@ pub const Surface = struct { pub fn at(s: *Surface, x: u16, y: u16) *Cell { std.debug.assert(x < s.cols and y < s.rows); - return &s.cells[@as(usize, y) * s.cols + x]; + const cell = &s.cells[@as(usize, y) * s.cols + x]; + // The look-hover paint runs with mark_hover set, and its cells are + // exactly the ones this marks — see render's preview block. + if (s.mark_hover) cell.hover = true; + return cell; } pub fn set(s: *Surface, x: u16, y: u16, text: []const u8, style: CellStyle) void { @@ -6321,6 +6342,14 @@ pub const Pardes = struct { return config.topbar_str; } + /// The workspace tag row's height: `TOPBAR_H` while the setting keeps + /// it, zero once a native shell takes its commands over. Layout and hit + /// testing read this rather than the constant, the way they read + /// `columnBarHeight`. + pub fn topBarHeight(p: *const Pardes) u16 { + return if (p.settings.workspace_tag) TOPBAR_H else 0; + } + pub fn columnBarHeight(p: *const Pardes) u16 { return if (p.settings.column_tags and p.screen_h >= 3) COLBAR_H else 0; } @@ -7834,7 +7863,7 @@ pub const Pardes = struct { } var dest: ?usize = null; for (p.panes, 0..) |slot, i| { - if (slot == null or p.rects[i].y != TOPBAR_H + p.columnBarHeight()) continue; + if (slot == null or p.rects[i].y != p.topBarHeight() + p.columnBarHeight()) continue; if (p.header_column) |c| if (p.rects[i].x != p.col_x[c]) continue; if (i == p.active) { dest = i; @@ -11099,7 +11128,7 @@ pub const Pardes = struct { pub const ChromeTarget = struct { col: u16, row: u16 }; pub fn chromeTarget(p: *const Pardes, col: u16, row: u16) ?ChromeTarget { - if (row < TOPBAR_H + p.columnBarHeight()) return .{ .col = col, .row = row }; + if (row < p.topBarHeight() + p.columnBarHeight()) return .{ .col = col, .row = row }; for (p.panes, 0..) |slot, id| { if (slot == null) continue; const rect = p.rects[id]; @@ -11414,7 +11443,7 @@ pub const Pardes = struct { p.chord_arg = null; } } - if (mrow >= TOPBAR_H and mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press) { + if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) { for (0..p.ncol) |column| { if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue; if (m.button == config.select_button) { @@ -11431,7 +11460,7 @@ pub const Pardes = struct { if (m.kind == .release) p.header_drag = false; return; } - if (mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press and + if (mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press and (m.button == config.select_button or m.button == config.exec_button or m.button == config.look_button)) { if (m.button == config.select_button) if (p.panes[p.active]) |pane| { @@ -11439,7 +11468,7 @@ pub const Pardes = struct { }; var column: ?usize = null; var x: u16 = 0; - if (mrow >= TOPBAR_H) { + if (mrow >= p.topBarHeight()) { 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] + TAG_TEXT_INSET; @@ -13302,6 +13331,15 @@ pub const Pardes = struct { } } }; + // A look-hover preview means the pointed word is a real Look or Exec + // operand: the same plumbing a click would act on. Hosts that model + // the pointer per-text (the macOS I-beam) read this as "target" and + // afford it the arrow instead. + if (p.look_hover_preview != null) s.pointer_shape = .target; + // Stale hover bits must not survive into a frame whose pointer has + // moved on: the ordinary content paint does not clear fields it + // never set. + for (s.cells) |*c| c.hover = false; for (&s.body_layers) |*layer| layer.rows = 0; for (&s.tag_layers) |*layer| layer.cols = 0; s.nimages = 0; @@ -13368,21 +13406,24 @@ pub const Pardes = struct { } } - // global tagbar: full width, top row + // global tagbar: full width, top row — unless a native shell has + // taken its commands over, in which case the row is not ours to draw p.revealHeader(); - s.fill(0, 0, s.cols, TOPBAR_H, .{ - .bg = .{ .rgb = chrome.tag_bg }, - .font_role = .tagline, - }); var tb_buf: [1200]u8 = undefined; const global_bar = p.topbar(&tb_buf); const global_scroll = if (p.topbar_col != null and p.header_column == null) p.header_scroll else 0; - _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{ - .fg = .{ .rgb = chrome.tag_fg }, - .bg = .{ .rgb = chrome.tag_bg }, - .font_role = .tagline, - }); - if (p.pointer_inside and p.hover_row < TOPBAR_H) { + if (p.settings.workspace_tag) { + s.fill(0, 0, s.cols, TOPBAR_H, .{ + .bg = .{ .rgb = chrome.tag_bg }, + .font_role = .tagline, + }); + _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{ + .fg = .{ .rgb = chrome.tag_fg }, + .bg = .{ .rgb = chrome.tag_bg }, + .font_role = .tagline, + }); + } + if (p.pointer_inside and p.hover_row < p.topBarHeight()) { const bar = p.topbar(&tb_buf); if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col + global_scroll))) |bounds| { var col = panes.File.rawDisplayCol(bar, bounds.lo) -| global_scroll; @@ -13412,24 +13453,24 @@ pub const Pardes = struct { const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10; 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(p.col_x[c], p.topBarHeight(), grip, 1, grip_style); + s.fill(p.col_x[c] + grip, p.topBarHeight(), inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); - s.fill(x, TOPBAR_H, w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); + s.fill(x, p.topBarHeight(), 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; const tag = p.columnTag(c); - _ = s.print(x, TOPBAR_H, w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{ + _ = s.print(x, p.topBarHeight(), w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline, }); - if (p.pointer_inside and p.hover_row == TOPBAR_H and p.hover_col >= x and p.hover_col < x + w) { + if (p.pointer_inside and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) { const bar = p.columnTag(c); if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| { var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll; const hi = panes.File.rawDisplayCol(bar, bounds.hi) -| bar_scroll; while (col < hi and col < w) : (col += 1) { - const cell = s.at(x + @as(u16, @intCast(col)), TOPBAR_H); + const cell = s.at(x + @as(u16, @intCast(col)), p.topBarHeight()); cell.style.bg = .{ .rgb = th.sel_bg }; cell.style.fg = .{ .rgb = th.sel_fg }; } @@ -13440,7 +13481,7 @@ pub const Pardes = struct { const bar = p.headerText(); 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 y: u16 = if (p.header_column != null) p.topBarHeight() else 0; const anchor = p.header_anchor orelse cur; var col = panes.File.rawDisplayCol(bar, @min(cur, anchor)) -| p.header_scroll; const hi = panes.File.rawDisplayCol(bar, @max(cur, anchor)) -| p.header_scroll; @@ -13458,7 +13499,7 @@ pub const Pardes = struct { switch (p.drag) { .border_v => |d| { if (d.cur_x < s.cols) { - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎"); } if (d.corner) |k| if (d.cur_y < s.rows) { @@ -13483,11 +13524,11 @@ pub const Pardes = struct { for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) { layer.viewport.w -|= 1; }; - for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= TOPBAR_H) { + for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) { layer.viewport.w -|= 1; }; } - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎"); } }, @@ -13505,7 +13546,7 @@ pub const Pardes = struct { var c: usize = 0; while (c + 1 < p.ncol) : (c += 1) { if (p.hover_col == p.col_x[c] + p.col_w[c] -| 1) { - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(p.hover_col, row, "╎"); } } @@ -13559,27 +13600,27 @@ pub const Pardes = struct { const ow: u16 = 38; const oh: u16 = 10; const ox: u16 = if (s.cols > ow) s.cols - ow else 0; - if (ox + ow <= s.cols and TOPBAR_H + oh <= s.rows) { + if (ox + ow <= s.cols and p.topBarHeight() + oh <= s.rows) { // vaxis single_rounded border ring, default style - s.set(ox, TOPBAR_H, "╭", .{}); - s.set(ox + ow - 1, TOPBAR_H, "╮", .{}); - s.set(ox, TOPBAR_H + oh - 1, "╰", .{}); - s.set(ox + ow - 1, TOPBAR_H + oh - 1, "╯", .{}); + s.set(ox, p.topBarHeight(), "╭", .{}); + s.set(ox + ow - 1, p.topBarHeight(), "╮", .{}); + s.set(ox, p.topBarHeight() + oh - 1, "╰", .{}); + s.set(ox + ow - 1, p.topBarHeight() + oh - 1, "╯", .{}); var bx = ox + 1; while (bx < ox + ow - 1) : (bx += 1) { - s.set(bx, TOPBAR_H, "─", .{}); - s.set(bx, TOPBAR_H + oh - 1, "─", .{}); + s.set(bx, p.topBarHeight(), "─", .{}); + s.set(bx, p.topBarHeight() + oh - 1, "─", .{}); } - var by = TOPBAR_H + 1; - while (by < TOPBAR_H + oh - 1) : (by += 1) { + var by = p.topBarHeight() + 1; + while (by < p.topBarHeight() + oh - 1) : (by += 1) { s.set(ox, by, "│", .{}); s.set(ox + ow - 1, by, "│", .{}); } - s.fill(ox + 1, TOPBAR_H + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } }); + s.fill(ox + 1, p.topBarHeight() + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } }); var lines = std.mem.splitScalar(u8, text, '\n'); - var ly: u16 = TOPBAR_H + 1; + var ly: u16 = p.topBarHeight() + 1; while (lines.next()) |line| : (ly += 1) { - if (ly >= TOPBAR_H + oh - 1) break; + if (ly >= p.topBarHeight() + oh - 1) break; _ = s.print(ox + 1, ly, ow - 2, line, .{ .fg = .{ .rgb = chrome.tag_fg } }); } } @@ -13818,11 +13859,14 @@ pub const Pardes = struct { 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); + // With the workspace tag row handed to native chrome there is no + // workspace layer to render; the menu bar carries its commands. + if (p.settings.workspace_tag) + 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(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); + try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = p.topBarHeight(), .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); }; } @@ -14126,13 +14170,14 @@ pub const Pardes = struct { if (p.look_hover_preview) |preview| { if (preview.pane == id and preview.serial == pane.serial) { // Preserve body ink, then let every real user selection below - // win over this quiet affordance. + // win over this quiet affordance. mark_hover carries the + // cells this paints out to the hosts as the affordance rect — + // macOS composes its glass overlay over exactly these. const preview_bg = mix(page_rgb, mix(page_rgb, th.sel_bg)); if (preview.file_word) |word| panes.File.paintWordSelection(p, pane, r, word.row, word.lo, word.hi, preview_bg) else if (preview.sel) |sel| - if (@max(sel.r0, sel.r1) >= BOX_H) - p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null); + p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null); } } const modal_hover = if (p.look_hover_preview) |preview| |
