diff options
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. |
