diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 16:50:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 9ce11bfeff73b96704e521b41cffe4625b02f103 (patch) | |
| tree | 3aedcf15b6adc9c1ef4d1acff2146b7c352e6ba6 /src/gui/gui.zig | |
| parent | 96725011b43d7bf56ac02e710ea4ef4a76f05351 (diff) | |
| download | pardes-9ce11bfeff73b96704e521b41cffe4625b02f103.tar.gz pardes-9ce11bfeff73b96704e521b41cffe4625b02f103.zip | |
Reorder columns using aligned and distinctly colored tag grips
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 123 |
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" { |
