summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 16:50:50 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit9ce11bfeff73b96704e521b41cffe4625b02f103 (patch)
tree3aedcf15b6adc9c1ef4d1acff2146b7c352e6ba6 /src/gui
parent96725011b43d7bf56ac02e710ea4ef4a76f05351 (diff)
downloadpardes-9ce11bfeff73b96704e521b41cffe4625b02f103.tar.gz
pardes-9ce11bfeff73b96704e521b41cffe4625b02f103.zip
Reorder columns using aligned and distinctly colored tag grips
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig123
1 files changed, 80 insertions, 43 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index f4c87ed1..10ac839e 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -4936,9 +4936,9 @@ fn renderFrame(
const data_diff = data_effect and surface.panelCellChanged(col, row);
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
const instance_count = if (data_diff)
- cellInstanceCount(core, &surface.previous_cells[logical_idx], row) + cellInstanceCount(core, surface.at(col, row), row)
+ cellInstanceCount(&surface.previous_cells[logical_idx]) + cellInstanceCount(surface.at(col, row))
else
- cellInstanceCount(core, surface.at(col, row), row);
+ cellInstanceCount(surface.at(col, row));
paint_plan.batches[batch_index].cell_count = std.math.add(
u32,
paint_plan.batches[batch_index].cell_count,
@@ -4961,7 +4961,7 @@ fn renderFrame(
destination.cell_count = std.math.add(
u32,
destination.cell_count,
- cellInstanceCount(core, &surface.previous_cells[logical_idx], row),
+ cellInstanceCount(&surface.previous_cells[logical_idx]),
) catch return error.GpuCapacity;
}
}
@@ -5359,14 +5359,8 @@ test "concealed terminal text emits no ink on opaque or transparent ground" {
}
}
-fn drawnFontRole(core: ?*const pardes.Pardes, cell: *const pardes.Cell, row: u16) pardes.FontRole {
- const role = cellFontRole(cell);
- if (role != .tagline or core != null or row < pardes.TOPBAR_H) return role;
- return .body;
-}
-
-fn cellInstanceCount(core: ?*const pardes.Pardes, cell: *const pardes.Cell, row: u16) u32 {
- return if (drawnFontRole(core, cell, row) == .tagline) 2 else 1;
+fn cellInstanceCount(cell: *const pardes.Cell) u32 {
+ return if (cellFontRole(cell) == .tagline) 2 else 1;
}
fn taglineBaseRgb(core: *const pardes.Pardes, col: u16, row: u16) [3]u8 {
@@ -5427,13 +5421,7 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
for (0..tagLayerCellCount(g, layer)) |col| {
const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.col == col else false;
emitInstance(g, instances, next.*, @intCast(col), layer.viewport.y, .{ .w = tw, .h = bh, .x_off = left, .y_off = 0 }, win_w, win_h, track, .tagline, &layer.cells[col], old_layer, cursor, page);
- const instance = &instances[next.*];
- const edge = right / win_w * 2 - 1;
- if (instance.x1 > edge) {
- const fraction = (edge - instance.x0) / (instance.x1 - instance.x0);
- instance.u1 = instance.u0 + (instance.u1 - instance.u0) * fraction;
- instance.x1 = edge;
- }
+ clipInstanceRight(&instances[next.*], right, win_w);
next.* += 1;
}
}
@@ -5450,6 +5438,14 @@ fn bodyLayerIn(layers: []const pardes.BodyLayer, col: u16, row: u16) ?*const par
return null;
}
+fn clipInstanceRight(instance: *CellInstance, right: f32, win_w: f32) void {
+ const edge = right / win_w * 2 - 1;
+ if (instance.x1 <= edge) return;
+ const fraction = std.math.clamp((edge - instance.x0) / (instance.x1 - instance.x0), 0, 1);
+ instance.u1 = instance.u0 + (instance.u1 - instance.u0) * fraction;
+ instance.x1 = @max(instance.x0, edge);
+}
+
fn clipBodyInstance(instance: *CellInstance, bottom: f32, win_h: f32) void {
const limit = 1 - bottom / win_h * 2;
if (instance.y1 >= limit) return;
@@ -5464,6 +5460,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
const tw: f32 = @floatFromInt(g.tagline_width);
const th: f32 = @floatFromInt(g.tagline_height);
const left = @as(f32, @floatFromInt(layer.viewport.x)) * bw;
+ const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * bw;
const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * bh;
for (0..layer.rows) |index| {
const row: u16 = @intCast(index);
@@ -5478,14 +5475,17 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
background.style.font_role = .body;
emitInstance(g, instances, next.*, 0, 0, .{ .w = @as(f32, @floatFromInt(layer.viewport.w)) * bw, .h = height, .x_off = left, .y_off = top }, win_w, win_h, track, .body, &background, old_layer, false, page);
clipBodyInstance(&instances[next.*], bottom, win_h);
+ clipInstanceRight(&instances[next.*], right, win_w);
next.* += 1;
}
for (0..layer.cols) |col| {
+ if (left + @as(f32, @floatFromInt(col)) * width >= right) break;
const cell = &layer.cells[index * layer.cols + col];
const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == row else false;
const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height))) else 0);
emitInstance(g, instances, next.*, 0, 0, .{ .w = width, .h = height, .x_off = left + @as(f32, @floatFromInt(col)) * width, .y_off = glyph_top }, win_w, win_h, track, role, cell, old_layer, cursor, page);
clipBodyInstance(&instances[next.*], bottom, win_h);
+ clipInstanceRight(&instances[next.*], right, win_w);
next.* += 1;
}
if (layer.hasContextBorderAfter(row)) {
@@ -5497,6 +5497,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
.y_off = top + height - 1,
}, win_w, win_h, track, .body, &border, old_layer, false, page);
clipBodyInstance(&instances[next.*], bottom, win_h);
+ clipInstanceRight(&instances[next.*], right, win_w);
next.* += 1;
}
}
@@ -5519,7 +5520,7 @@ fn emitSurfaceCell(
is_cursor: bool,
page: Ground,
) void {
- const role = drawnFontRole(core, cell, row);
+ const role = cellFontRole(cell);
if (role == .tagline) {
var band_base = tagline_base.*;
if (core) |p| band_base.style.bg = .{ .rgb = taglineBaseRgb(p, col, row) };
@@ -5528,8 +5529,9 @@ fn emitSurfaceCell(
// adjacent workspace, column and pane tags at TaglineSize < 100.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, &band_base, old_layer, false, page);
next.* += 1;
- const tag_layout = taglineLayoutForCell(g, col);
- emitInstance(g, instances, next.*, col, row, tag_layout, win_w, win_h, track, .tagline, cell, old_layer, is_cursor, page);
+ // Tag glyphs keep their font size inside a full physical-width grip.
+ emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, cell, old_layer, is_cursor, page);
+ instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w;
next.* += 1;
return;
}
@@ -5556,8 +5558,14 @@ test "small tagline fonts leave no background gutters between adjacent tags" {
try std.testing.expectApproxEqAbs(@as(f32, 20.0 / 480.0 * 2), base.y0 - base.y1, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, 16.0 / 480.0 * 2), ink.y0 - ink.y1, 0.0001);
try std.testing.expect(ink.y0 <= base.y0 and ink.y1 >= base.y1);
+ try std.testing.expectApproxEqAbs(@as(f32, 10.0 / 800.0 * 2), ink.x1 - ink.x0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, 10) / atlas_w, ink.u1 - ink.u0, 0.0001);
if (row > 0) try std.testing.expectApproxEqAbs(instances[(row - 1) * 2].y1, base.y0, 0.0001);
}
+ next = 0;
+ emitSurfaceCell(&g, null, &instances, &next, 0, 2, fixedCellLayout(&g), 800, 480, null, &cell, &cell, true, false, Ground.opaqueRgb(bg_default));
+ try std.testing.expectEqual(@as(u32, 2), next);
+ try std.testing.expectApproxEqAbs(@as(f32, 16.0 / 480.0 * 2), instances[1].y0 - instances[1].y1, 0.0001);
}
fn emitInstance(
@@ -6335,11 +6343,12 @@ fn buildOverlay(
});
}
if (transient_on) {
- if (core) |p| appendPaneChrome(g, p, surface, layout, &builder);
+ if (core) |p| appendPaneChrome(p, surface, layout, &builder);
+ appendTagGrips(g, surface, layout, &builder);
if (surface.cursor) |cursor| {
if (cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null and tagLayerIn(surface.tagLayers(), cursor.x, cursor.y) == null) {
const cell = surface.cells[@as(usize, cursor.y) * surface.cols + cursor.x];
- const role = drawnFontRole(core, &cell, cursor.y);
+ const role = cellFontRole(&cell);
const visual_height: f32 = if (role == .tagline)
@floatFromInt(g.tagline_height)
else
@@ -6504,7 +6513,6 @@ fn cellBackgroundIs(cell: *const pardes.Cell, rgb: [3]u8) bool {
// The canonical surface still owns scrollbar positions and hit targets. Only
// their pixel ink is inset here; no layout, selection or mouse coordinates move.
fn appendPaneChrome(
- g: *const Gui,
core: *const pardes.Pardes,
surface: *const pardes.Surface,
layout: CellLayout,
@@ -6520,31 +6528,13 @@ fn appendPaneChrome(
const body_end = body_y + r.h - pardes.BOX_H;
const x0 = @as(f32, @floatFromInt(r.x)) * layout.w;
const x1 = x0 + @as(f32, @floatFromInt(r.w)) * layout.w;
- const tag_cell = &surface.cells[@as(usize, tag_y) * surface.cols + r.x];
- const tag_cells = surface.cells[@as(usize, tag_y) * surface.cols + r.x ..][0..r.w];
+ const tag_cells = surface.cells[@as(usize, tag_y) * surface.cols + r.x + config.GUTTER ..][0 .. r.w - config.GUTTER];
const tag_visible = for (tag_cells) |*cell| {
if (cellFontRole(cell) != .tagline) break false;
} else true;
if (tag_visible) {
const rule_y = @as(f32, @floatFromInt(tag_y + @as(u16, if (core.settings.tag_bottom) 0 else 1))) * layout.h;
builder.addRect(x0, rule_y - @as(f32, if (core.settings.tag_bottom) 0 else 1), x1, rule_y + @as(f32, if (core.settings.tag_bottom) 1 else 0), overlayRgb(chrome.border));
-
- // A quiet grip gives the normal-mode move box a purpose at rest.
- // Insert and terminal badges retain their existing visible glyphs.
- if (tag_cell.printableAscii() == ' ' and g.tagline_height >= 6 and g.tagline_width >= 3) {
- const tag_layout = fixedCellLayout(g);
- const center_x = tag_layout.x_off + (@as(f32, @floatFromInt(r.x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * tag_layout.w;
- const center_y = @as(f32, @floatFromInt(tag_y)) * layout.h +
- @as(f32, @floatFromInt(taglineBandOffset(tag_y, builder.win_h, g.cell_h, g.tagline_height))) +
- @as(f32, @floatFromInt(g.tagline_height)) / 2;
- const half_w = @max(1, @min(3, tag_layout.w - 2));
- const ink = overlayRgb(switch (tag_cell.style.fg) {
- .rgb => |rgb| rgb,
- else => chrome.tag_fg,
- });
- builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), ink);
- builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), ink);
- }
}
// Do not paint over resize previews or debug text occupying the rail.
@@ -6573,6 +6563,46 @@ fn appendPaneChrome(
appendColumnSpines(core, surface.rows, layout, builder);
}
+// Tag handles use the canonical physical cells, including detached frames.
+// Their tag text begins at the layer viewport and may use a narrower font.
+fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout, builder: *OverlayBuilder) void {
+ if (g.tagline_height < 6 or g.tagline_width < 3) return;
+ for (surface.tagLayers()) |layer| {
+ if (layer.cols == 0 or layer.kind == .workspace or layer.viewport.x < config.GUTTER) continue;
+ const x = layer.viewport.x - config.GUTTER;
+ const y = layer.viewport.y;
+ if (x >= surface.cols or y >= surface.rows) continue;
+ const cell = &surface.cells[@as(usize, y) * surface.cols + x];
+ if (cell.default or cell.printableAscii() != ' ') continue;
+ const rgb = switch (cell.style.fg) {
+ .rgb => |rgb| rgb,
+ else => continue,
+ };
+ const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w;
+ const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
+ const half_w = @max(1, @min(3, layout.w - 2));
+ builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
+ builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
+ }
+}
+
+test "column and pane grip marks share physical centering" {
+ var g: Gui = undefined;
+ g.tagline_height = 12;
+ g.tagline_width = 6;
+ g.cell_h = 20;
+ var cells: [80]pardes.Cell = @splat(.{ .default = false, .style = .{ .fg = .{ .rgb = .{ 100, 120, 140 } } } });
+ var surface: pardes.Surface = .{ .cols = 20, .rows = 4, .cells = &cells };
+ surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 7, .y = 1, .w = 13, .h = 1 }, .cols = 20 };
+ surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 7, .y = 3, .w = 13, .h = 1 }, .cols = 20 };
+ var vertices: [24]OverlayVertex = undefined;
+ var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 200, .win_h = 80 };
+ appendTagGrips(&g, &surface, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
+ try std.testing.expectEqual(@as(usize, 24), builder.len);
+ for (0..12) |i| try std.testing.expectEqual(vertices[i].x, vertices[i + 12].x);
+ try std.testing.expectApproxEqAbs(@as(f32, 57.0 / 200.0 * 2.0 - 1.0), vertices[0].x, 0.0001);
+}
+
fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w: f32, thumb: bool, color: OverlayColor) void {
const width = @min(cell_w, if (thumb) @max(2, @floor(cell_w * 0.5)) else 2);
builder.addRect(x, y0, x + width, y1, color);
@@ -6955,6 +6985,13 @@ test "compact body layer rendering fills recovered rows and clips the final glyp
try std.testing.expectApproxEqAbs(@as(f32, 0.12), instances[14].y0, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, -0.4), instances[19].y1, 0.0001);
try std.testing.expectApproxEqAbs(instances[19].v0 + @as(f32, 12) / atlas_h, instances[19].v1, 0.0001);
+ // A drag preview reserves the last physical column without changing cells.
+ layer.viewport.w = 1;
+ for ([_]bool{ false, true }) |old| {
+ next = 0;
+ emitBodyLayer(&g, &instances, &next, &layer, 200, 200, null, Ground.opaqueRgb(bg_default), false, old, .{ 80, 90, 100 });
+ for (instances[0..next]) |instance| try std.testing.expect(instance.x1 <= -0.6 + 0.0001);
+ }
}
test "compact tag layers fill available width and clip at their physical edge" {