diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-23 02:00:00 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | f6738c7edfff2249e0b13f2a76962f45d02c86bb (patch) | |
| tree | 3f255da736fb0a6f4b926c72054d83091f941d21 /src/gui | |
| parent | 9695cb098c784792af79c22d46a17f7bc7247d33 (diff) | |
| download | pardes-f6738c7edfff2249e0b13f2a76962f45d02c86bb.tar.gz pardes-f6738c7edfff2249e0b13f2a76962f45d02c86bb.zip | |
Center tagline bands, frame anchors evenly, fill to the window edge, and resize columns from their grip
Every tag band (workspace, column, pane) is centred in its row, so text sits
at one baseline offset. Anchors are inset from the column rule by that same
margin, so they are square and the band frames them on the left, top and bottom;
the pane mark moves to stay centred. When the window is not a whole number of
cells, the bands, rules, spines and scroll thumbs at the right and bottom edges
run on through the leftover pixels.
A column grip dropped short of another column's place now moves the column's
left edge, with a dashed rail preview, sharing setColumnPairWidth with the
border drag. A release still on the grip changes nothing, and a pair too
narrow for two MINW columns is left alone.
PARDES_TEST_PAD adds leftover pixels to a test-mode window and capture.
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 88 |
1 files changed, 60 insertions, 28 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 7f28124c..48087d0b 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -1809,6 +1809,8 @@ const Gui = struct { pointer_cell: ?MouseCell = null, capture: bool = false, + /// Pixels the capture holds past the grid (PARDES_TEST_PAD). + capture_pad: u32 = 0, capture_dir: []const u8 = "", capture_tex: ?*c.SDL_GPUTexture = null, capture_tex_w: u32 = 0, @@ -2196,7 +2198,9 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u if (test_mode) { const cols = envU16(env, "PARDES_TEST_COLS") orelse 80; const rows = envU16(env, "PARDES_TEST_ROWS") orelse 24; - _ = c.SDL_SetWindowSize(window, @intCast(cols * cell_w), @intCast(rows * cell_h)); + // PARDES_TEST_PAD: extra pixels past the grid, as a real window has. + const pad = envU16(env, "PARDES_TEST_PAD") orelse 0; + _ = c.SDL_SetWindowSize(window, @intCast(cols * cell_w + pad), @intCast(rows * cell_h + pad)); _ = c.SDL_SyncWindow(window); } @@ -2284,6 +2288,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u .atlas_stage = atlas_stage, .glyphs = std.AutoHashMap(GlyphKey, Slot).init(gpa), .capture = test_mode, + .capture_pad = if (test_mode) envU16(env, "PARDES_TEST_PAD") orelse 0 else 0, .capture_dir = capture_dir orelse "", .soft_present = soft_present, .transparent = config.gui_transparent, @@ -4909,8 +4914,8 @@ fn renderFrame( var sh: u32 = 0; var target: *c.SDL_GPUTexture = undefined; if (g.capture) { - sw = @as(u32, surface.cols) * g.cell_w; - sh = @as(u32, surface.rows) * g.cell_h; + sw = @as(u32, surface.cols) * g.cell_w + g.capture_pad; + sh = @as(u32, surface.rows) * g.cell_h + g.capture_pad; if (sw == 0 or sh == 0) { command_consumed = true; _ = c.SDL_SubmitGPUCommandBuffer(cmd); @@ -5360,18 +5365,14 @@ fn bottomTaglinePresent(surface: *const pardes.Surface) bool { return false; } -test "tagline bands face the topbar rule and Tagbottom faces the window edge" { +test "tagline bands center in their cells and Tagbottom faces the window edge" { const cell_h: u32 = 20; const tagline_h: u32 = 16; const canvas_h: f32 = 200; - const border = topbarPaneBorderPixels(cell_h, tagline_h); - try std.testing.expectEqual(@as(u32, 1), border); - try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h, true)); - try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(0, canvas_h, cell_h, tagline_h, false)); - try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h, true)); - try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(1, canvas_h, cell_h, tagline_h, false)); - try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h, true)); - try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h, true)); + // Workspace, column and pane rows alike: text lines up across bands. + for ([_]u16{ 0, 1, 2, 5, 8 }) |row| + try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(row, canvas_h, cell_h, tagline_h)); + try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h)); } fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell { @@ -5502,7 +5503,10 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p // it comes out from under whatever is above it. const slide = layer.slide * bh; const top = @as(f32, @floatFromInt(layer.viewport.y)) * bh; - emitInstance(g, instances, next.*, 0, layer.viewport.y, .{ .w = right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, &background, old_layer, false, page); + // A window is rarely a whole number of cells wide: a band that ends at + // the last column runs on through the leftover pixels to the edge. + const band_right = if (win_w - right < bw) @max(right, win_w) else right; + emitInstance(g, instances, next.*, 0, layer.viewport.y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, &background, old_layer, false, page); if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h); next.* += 1; for (0..tagLayerCellCount(g, layer)) |col| { @@ -5578,7 +5582,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const 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, true))) else 0); + 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); @@ -5628,6 +5632,10 @@ fn paneGripCell(layers: []const pardes.TagLayer, cells: []const pardes.Cell, col return null; } +fn anchorInset(g: *const Gui, row: u16, win_h: f32) u32 { + return taglineBandOffset(row, win_h, g.cell_h, g.tagline_height); +} + fn emitSurfaceCell( g: *Gui, core: ?*const pardes.Pardes, @@ -5657,19 +5665,27 @@ fn emitSurfaceCell( next.* += 1; // 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); + // An anchor starts at its column's rule: its left edge is inset by + // its top margin so the band frames it on the left as above and + // below, and its mark moves right by half that to stay centered. + const inset: u32 = if (core) |p| if (std.mem.indexOfScalar(u16, p.col_x[0..p.ncol], col -| (if (grip) |indicator| indicator.offset else 0)) != null) anchorInset(g, row, win_h) else 0 else 0; if (grip) |indicator| { const cached = cachedGlyph(g, cellCodepoint(indicator.cell), .tagline, GlyphDecoration.fromCell(indicator.cell), true); const slot = cached orelse g.space_slot; - const offset: u32 = if (cached != null) @as(u32, indicator.offset) * g.cell_w else 0; + const offset: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, indicator.offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0; const resolved = resolveCell(g, indicator.cell, .tagline, false, page); instances[next.*].fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255; instances[next.*].fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255; instances[next.*].fb = @as(f32, @floatFromInt(resolved.fg[2])) / 255; - instances[next.*].u0 = @as(f32, @floatFromInt(slot.u + offset)) / atlas_w; + instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + offset) / atlas_w; instances[next.*].v0 = @as(f32, @floatFromInt(slot.v)) / atlas_h; instances[next.*].v1 = @as(f32, @floatFromInt(slot.v + g.tagline_height)) / atlas_h; } instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w; + if (inset > 0 and (grip == null or grip.?.offset == 0)) { + instances[next.*].x0 += @as(f32, @floatFromInt(inset)) / win_w * 2; + instances[next.*].u0 += @as(f32, @floatFromInt(inset)) / atlas_w; + } next.* += 1; return; } @@ -5690,7 +5706,8 @@ test "small tagline fonts leave no background gutters between adjacent tags" { var instances: [6]CellInstance = undefined; var next: u32 = 0; for (0..3) |row| { - emitSurfaceCell(&g, core, &instances, &next, 0, @intCast(row), fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default)); + // Column 1: past the anchor's inset edge at the column's left. + emitSurfaceCell(&g, core, &instances, &next, 1, @intCast(row), fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default)); const base = instances[row * 2]; const ink = instances[row * 2 + 1]; try std.testing.expectApproxEqAbs(@as(f32, 20.0 / 480.0 * 2), base.y0 - base.y1, 0.0001); @@ -5700,6 +5717,13 @@ test "small tagline fonts leave no background gutters between adjacent tags" { 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); } + // Column 0 is an anchor's edge: its ink is inset by the band's margin + // (2 px here) and its underlay still covers the whole cell. + next = 0; + emitSurfaceCell(&g, core, &instances, &next, 0, 1, fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, false, false, Ground.opaqueRgb(bg_default)); + try std.testing.expectApproxEqAbs(@as(f32, 10.0 / 800.0 * 2), instances[0].x1 - instances[0].x0, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 8.0 / 800.0 * 2), instances[1].x1 - instances[1].x0, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 8) / atlas_w, instances[1].u1 - instances[1].u0, 0.0001); next = 0; emitSurfaceCell(&g, null, &instances, &next, 0, 2, fixedCellLayout(&g), 800, 480, null, &cell, &cell, null, true, false, Ground.opaqueRgb(bg_default)); try std.testing.expectEqual(@as(u32, 2), next); @@ -5730,7 +5754,7 @@ fn emitInstance( else layout.h; const band_offset: f32 = if (resolved.role == .tagline) - @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height, true)) + @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height)) else 0; const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h + band_offset; @@ -6562,7 +6586,7 @@ fn addCursorBar( const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w; const height = std.math.clamp(visual_height, 1.0, layout.h); const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h + - @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height), true))); + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height)))); builder.addRect( x0, y0, @@ -6662,7 +6686,7 @@ fn buildOverlay( const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w; const x = left + @as(f32, @floatFromInt(cursor.col)) * width; if (x >= right) continue; - const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height, true))); + const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height))); builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[cursor.col], .tagline))); }; if (transient_on) appendTouchOverlay(g, &builder, debug_on); @@ -6683,7 +6707,7 @@ fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pard if (cellCodepoint(cell) != ' ') occupied = i + 1; } const editing = p.topbar_col != null and p.header_column == null; - const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height, true)); + const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height)); const region = if (occupied >= surface.cols) null else pet.lane( occupied, @floatFromInt(g.tagline_width), @@ -6810,7 +6834,10 @@ fn appendPaneChrome( } 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)); + // Like the band above it, the rule of a pane at the right edge + // runs on through the pixels past the last whole cell. + const rule_x1 = if (builder.win_w - x1 < layout.w) @max(x1, builder.win_w) else x1; + builder.addRect(x0, rule_y - @as(f32, if (core.settings.tag_bottom) 0 else 1), rule_x1, rule_y + @as(f32, if (core.settings.tag_bottom) 1 else 0), overlayRgb(chrome.border)); } // Do not paint over resize previews or debug text occupying the rail. @@ -6827,12 +6854,13 @@ fn appendPaneChrome( } if (row != body_end) continue; const y0 = @as(f32, @floatFromInt(body_y)) * layout.h; - const y1 = @as(f32, @floatFromInt(body_end)) * layout.h; + const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h; + const y1 = if (body_end == surface.rows and builder.win_h - body_y1 < layout.h) @max(body_y1, builder.win_h) else body_y1; builder.addRect(x0, y0, x0 + layout.w, y1, page); appendSquareRail(builder, x0, y0, y1, layout.w, false, overlayRgb(chrome.scroll_track)); if (thumb_start) |first| { const thumb_y0 = @as(f32, @floatFromInt(first)) * layout.h; - const thumb_y1 = @as(f32, @floatFromInt(thumb_end)) * layout.h; + const thumb_y1 = if (thumb_end == body_end) y1 else @as(f32, @floatFromInt(thumb_end)) * layout.h; appendSquareRail(builder, x0, thumb_y0, thumb_y1, layout.w, true, overlayRgb(chrome.scroll_thumb)); } } @@ -6854,8 +6882,8 @@ fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLay .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, true))) + @as(f32, @floatFromInt(g.tagline_height)) / 2; + const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, builder.win_h) / 2)); + 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)); @@ -6875,7 +6903,9 @@ test "column grips stay unmarked while pane marks retain physical centering" { 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, 12), builder.len); - try std.testing.expectApproxEqAbs(@as(f32, 57.0 / 200.0 * 2.0 - 1.0), vertices[0].x, 0.0001); + // Centered in the anchor, whose left edge is inset by the band's offset + // (8 px on this last row): the mark moves right by half of it. + try std.testing.expectApproxEqAbs(@as(f32, 61.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 { @@ -6885,7 +6915,9 @@ fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w: fn appendColumnSpines(core: *const pardes.Pardes, rows: u16, layout: CellLayout, builder: *OverlayBuilder) void { const y0 = @as(f32, @floatFromInt(pardes.TOPBAR_H)) * layout.h; - const y1 = @as(f32, @floatFromInt(rows)) * layout.h; + const grid_y1 = @as(f32, @floatFromInt(rows)) * layout.h; + // Down through the pixels past the last whole row, as tags run right. + const y1 = if (builder.win_h - grid_y1 < layout.h) @max(grid_y1, builder.win_h) else grid_y1; if (y1 <= y0) return; // A single boundary spans all panes, including their tags. Its left edge // agrees with the square scroll markers and existing move-box hit area. |
