summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 22:37:36 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit297e14cfc36e4613a8c1cb3b995597d0a9c2873b (patch)
treead2ff7bd7a5869b2e45fe6098825a8ac35598703 /src/pardes.zig
parent9070942b29bd10dddcdecdb0e88ba0fb40608467 (diff)
parent44ce573e15c773cf5bb0d42941a143b63e44d900 (diff)
downloadpardes-297e14cfc36e4613a8c1cb3b995597d0a9c2873b.tar.gz
pardes-297e14cfc36e4613a8c1cb3b995597d0a9c2873b.zip
Merge the macOS first-class-host work
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig149
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|