diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 23:50:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | fde030a699b3d6667d1fa21ae192b5011e3f373f (patch) | |
| tree | aae9bd94e7889cea53c82e1775290adcd94cd7e7 /src/draw.zig | |
| parent | 34b631e75dd646809961cae0ab215b6c43643a13 (diff) | |
| download | pardes-fde030a699b3d6667d1fa21ae192b5011e3f373f.tar.gz pardes-fde030a699b3d6667d1fa21ae192b5011e3f373f.zip | |
Rules and chrome scale with the display's density; dark pages get visible separators and focus tints
Density: dp() in gui.zig draws every size given in pixels in logical
pixels (SDL's window display scale, rounded, at least one), as acme scales
Border, ButtonBorder and Scrollgap: the rules (rule_px), the tag rule, the
grip's ring, its dirty gap and mode bars, the rail's gap to the text and
its minimum, the thumb's inset, the rim, a bar cursor's minimum width,
underline and strikethrough thickness (the atlas is dropped on a scale
change), the rule-grab margin, and the Crt's scanline and mask periods
(pardesScale in the post prefix). Cell- and tagline-measured sizes stay:
the rail's width, Lift's blur and offsets, every motion; iResolution,
fragCoord and iMouse stay in the display's pixels, as ghostty's.
PARDES_TEST_SCALE forces a density in a harness (a test window is 1x
otherwise, whatever display it lands on) and sizes the test font with it.
Colours: a separator that stands off its page by less than 1.5:1 is lifted
toward the text to 1.6:1 (helix, atelier, paper; a theme with no page,
dark, is judged against #121212), and empty_col follows it. forge's own
rule already stands at 3.83:1; what hid it was density: 2 device pixels
on a 3x screen, now 6 (dp). A focused tag's tint closer than 1.5:1 to the
plain tag on a dark page (1.25:1 on a light one) is pushed further the way
it leans, as far as its text keeps its own contrast or 4.5: forge
1.23 -> 1.51 (#2d3440 -> #374253), and likewise dusk, orchard, ink,
lagoon, clay, harvest, spectrum, solarium, forge_soft; daybreak, atelier,
paper to 1.25. dark and helix gain a tint of their own (dark: #484566
under #c8c8c8, 1.54:1; helix: ramp 237 under 250, 1.65:1).
Tag caret: past a full row's end, the G3 glide box sits on the tag's right
edge, where the layer's caret and the pixel shell's are.
Tests: every native theme's separator >= 1.5 and every dark one has a
tint >= 1.25 with readable text; forge keeps its own border; the tag caret
glide box at a full row; at 2x the spine, tag rule, ring, rail start and
gap, and bar cursor double. Legacy-ThemeFile test strips helix's new pair.
Goldens: snapshot goldens change in colour rows only, the tints (orchard
#283a2f -> #314738 at boot, forge, helix and dark in theme/themesel).
GUI goldens: the harness's single Del of the renamed wide pane is refused
since ysqnkyoy (a rename counted as unsaved; fixed on the 9P line in
wkzlsqvy), so scenes 09-17 were recorded with that pane open and the file
was stale since the merge (08 and 18 also moved with ykvnltvs's full-row
tag wrap). The goldens here were recorded with that pane closed (as
wkzlsqvy will close it); against the parent's binary under the same
conditions they differ only in the tints: 01-13 forge's (#2d3440 ->
#374253, and the glyph edges on it), 14 ink's; 15-18 are byte-identical.
Until wkzlsqvy is merged, a run here fails 09-17. Each commit reproduces its own
recorded goldens on this machine (checked at kvtmqkyn). A test window is
1x unless PARDES_TEST_SCALE says otherwise, whatever display it opens on.
Prebuilt shaders refreshed from their sources.
Captures: .scratch/render/dark-borders/{before,after,compare,scale2,scale3}.
Diffstat (limited to 'src/draw.zig')
| -rw-r--r-- | src/draw.zig | 55 |
1 files changed, 54 insertions, 1 deletions
diff --git a/src/draw.zig b/src/draw.zig index a2005e59..b8cf4e7d 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -197,8 +197,11 @@ fn focusedCursorBox(p: *const Pardes, s: *const Surface) ?animation.Box { const caret = layer.cursor orelse continue; if (layer.rows == 0 or layer.viewport.y + caret.y != cursor.y) continue; if (cursor.x < layer.viewport.x or cursor.x >= layer.viewport.x + layer.viewport.w) continue; - const x = @as(f32, @floatFromInt(layer.viewport.x)) + @as(f32, @floatFromInt(caret.x)) * tw / bw; const w = if (caret.bar) tw / bw / 8 else tw / bw; + // A caret past a full row's end sits at the tag's right edge, where + // the grid's is clamped too. + const right = @as(f32, @floatFromInt(layer.viewport.x + layer.viewport.w)); + const x = @min(@as(f32, @floatFromInt(layer.viewport.x)) + @as(f32, @floatFromInt(caret.x)) * tw / bw, right - w); return .{ .x = x, .y = @as(f32, @floatFromInt(cursor.y)) + (bh - th) / 2 / bh, .w = w, .h = th / bh }; } return .{ .x = @floatFromInt(cursor.x), .y = @floatFromInt(cursor.y), .w = if (cursor.bar) 1.0 / 8.0 else 1, .h = 1 }; @@ -1787,6 +1790,56 @@ test "the focused cursor glides a jump, lands a step at once, and is exact at re try std.testing.expectEqual(@as(u8, 2), s.chrome.cursor_glide); } +test "a tag caret past its row's end glides to the tag's right edge, where the layer's is" { + if (!builtins.capabilities.lift) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 16 }); + defer p.deinit(); + p.update(.{ .resize = .{ .cols = 60, .rows = 16, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 6, .tagline_h = 12 } } }); + const pane = try p.setTestFile("text\n"); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + // The tag's text grown a letter at a time, the caret after its last one + // in insert mode, until a row is full and the caret is past its end. + var found = false; + var n: usize = 1; + while (n < 200 and !found) : (n += 1) { + _ = arena.reset(.retain_capacity); + if (pane.tag.own) |own| p.gpa.free(own); + const own = try p.gpa.alloc(u8, n); + @memset(own, 'x'); + own[0] = ' '; + pane.tag.own = own; + tagline.enterTag(p, pane); + const whole = try tagline.tagText(p, arena.allocator(), pane); + pane.tag.mode = .insert; + pane.tag.cur_row = 0; + pane.tag.cur_col = @intCast(modal.lineSlice(whole, 0).len); + p.sync(); + const s = try p.render(arena.allocator()); + const layer = for (s.tagLayers()) |*layer| { + if (layer.rows != 0 and layer.kind == .pane and layer.cursor != null) break layer; + } else continue; + const caret = layer.cursor.?; + const right: f32 = @floatFromInt(layer.viewport.x + layer.viewport.w); + const box = focusedCursorBox(p, s).?; + // Never past the tag's right edge, and the pane's. + try std.testing.expect(box.x + box.w <= right + 1e-4); + try std.testing.expect(box.x + box.w <= @as(f32, @floatFromInt(p.rects[p.active].x + p.rects[p.active].w))); + const at = @as(f32, @floatFromInt(layer.viewport.x)) + @as(f32, @floatFromInt(caret.x)) * 6 / 10; + if (at + box.w <= right) { + // Inside its row: exactly where the layer's caret is. + try std.testing.expectApproxEqAbs(at, box.x, 1e-4); + } else { + // Past it: on the tag's right edge, as the pixel shell's caret + // is (gui.zig cursorBoxes). + try std.testing.expectApproxEqAbs(right, box.x + box.w, 1e-4); + try std.testing.expectApproxEqAbs(@as(f32, 6.0 / 10.0 / 8.0), box.w, 1e-4); + found = true; + } + } + try std.testing.expect(found); +} + test "a terminal's cursor lands with scrolling output, never gliding against it" { if (!builtins.capabilities.lift or !pardes.terminal_panes) return error.SkipZigTest; const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 12 }); |
