summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 23:50:56 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitfde030a699b3d6667d1fa21ae192b5011e3f373f (patch)
treeaae9bd94e7889cea53c82e1775290adcd94cd7e7 /src/gui
parent34b631e75dd646809961cae0ab215b6c43643a13 (diff)
downloadpardes-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/gui')
-rw-r--r--src/gui/Post.zig5
-rw-r--r--src/gui/gui.zig214
2 files changed, 171 insertions, 48 deletions
diff --git a/src/gui/Post.zig b/src/gui/Post.zig
index f715965f..45798f8d 100644
--- a/src/gui/Post.zig
+++ b/src/gui/Post.zig
@@ -98,6 +98,9 @@ ping: [2]?*c.SDL_GPUTexture = .{ null, null },
ping_w: u32 = 0,
ping_h: u32 = 0,
uniforms: Uniforms = .{},
+/// The display's density, for a pass whose pattern is in logical pixels
+/// (pardesScale); the shell sets it.
+scale: f32 = 1,
started_ns: ?u64 = null,
last_ns: u64 = 0,
@@ -422,7 +425,7 @@ pub fn draw(post: *Post, device: *c.SDL_GPUDevice, cmd: *c.SDL_GPUCommandBuffer,
const binding = c.SDL_GPUTextureSamplerBinding{ .texture = input, .sampler = sampler };
c.SDL_BindGPUFragmentSamplers(pass_rp, 0, &binding, 1);
c.SDL_PushGPUFragmentUniformData(cmd, 0, u, @sizeOf(Uniforms));
- const own: [4]f32 = .{ @floatFromInt(@intFromBool(opaque_window)), @floatFromInt(pass.level), 0, 0 };
+ const own: [4]f32 = .{ @floatFromInt(@intFromBool(opaque_window)), @floatFromInt(pass.level), post.scale, 0 };
c.SDL_PushGPUFragmentUniformData(cmd, 1, &own, @sizeOf(@TypeOf(own)));
c.SDL_DrawGPUPrimitives(pass_rp, 3, 1, 0, 0);
c.SDL_EndGPURenderPass(pass_rp);
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 491373a8..fdcd7b2a 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1981,7 +1981,7 @@ const CellLayout = struct { w: f32, h: f32, x_off: f32, y_off: f32 };
const MouseCell = struct { col: u16, row: u16, body_hit: ?pardes.Mouse.BodyHit = null, tag_hit: ?pardes.TagHit = null, rule_above: bool = false };
/// How near a row's top edge a press is on the rule drawn there between two
-/// panes: the rule's 2 px and a pixel either side.
+/// panes: the rule's 2 px and a pixel either side, in logical pixels (dp).
const rule_grab_px: f32 = 3;
const WindowGeometry = struct {
window_w: f32,
@@ -2331,6 +2331,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
return error.SdlInit;
};
defer c.SDL_DestroyWindow(window);
+ updateDisplayScale(window);
if (native_wayland) log.info("Wayland opacity: background-only transparent surface", .{});
const arrow_cursor = c.SDL_CreateCursor(&p9_arrow_set, &p9_arrow_mask, 16, 16, 1, 1);
defer if (arrow_cursor) |cursor| c.SDL_DestroyCursor(cursor);
@@ -2403,7 +2404,13 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
font_allocator = std.heap.smp_allocator;
c.ui_font_prime(font);
font_allocator = gpa;
- const px: f32 = 27.0;
+ // A harness forcing a density (PARDES_TEST_SCALE) draws the text as a
+ // person on that display would size it, so its chrome is seen at the
+ // proportion it has there; PARDES_TEST_FONT_SCALE sizes the text alone
+ // (a density's text with 1x chrome: how it looked before dp). A live
+ // window keeps the size the user sets.
+ const font_scale: f32 = if (env.get("PARDES_TEST_FONT_SCALE")) |v| std.fmt.parseFloat(f32, v) catch display_scale else display_scale;
+ const px: f32 = if (test_mode) 27.0 * std.math.clamp(font_scale, 1, 4) else 27.0;
var cw: c_int = 10;
var chh: c_int = 20;
var asc: c_int = 16;
@@ -3349,6 +3356,12 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
const at = windowPointToPixels(windowGeometry(g.window), sev.button.x, sev.button.y);
g.post.mouse(at.x, at.y, sev.button.down);
},
+ c.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED => {
+ updateDisplayScale(g.window);
+ // Underlines are drawn into the glyphs a logical pixel thick.
+ resetGlyphAtlas(g);
+ if (in.core) |core| core.needs_frame = true;
+ },
c.SDL_EVENT_WINDOW_FOCUS_GAINED => g.post.focus(true),
c.SDL_EVENT_WINDOW_FOCUS_LOST => g.post.focus(false),
else => {},
@@ -3734,7 +3747,7 @@ fn gridCellAtDimensions(
const row: u16 = @intFromFloat(@min(@floor(@max(y, 0) / @max(body_h, 1)), 10_000));
var point: MouseCell = .{ .col = @intFromFloat(@min(@floor(@max(x, 0) / @max(body_w, 1)), 10_000)), .row = row };
// On the rule over this row, which is where two panes meet if they do.
- point.rule_above = row > 0 and @max(y, 0) - @as(f32, @floatFromInt(row)) * @max(body_h, 1) < rule_grab_px;
+ point.rule_above = row > 0 and @max(y, 0) - @as(f32, @floatFromInt(row)) * @max(body_h, 1) < dp(rule_grab_px);
if (core) |p| if (p.presentation.pointerFractional(p.screen_w, p.screen_h, x / @max(body_w, 1), y / @max(body_h, 1))) |mapped| {
for (p.surface.tagLayers()) |*layer| {
if (layer.tagHit(mapped.x * body_w, mapped.y * body_h, body_w, body_h, tagline_w, @floatFromInt(p.row_metrics.tagline_h))) |hit| {
@@ -5729,6 +5742,7 @@ fn renderFrame(
}
const chain_zone = tracy.zone(@src(), "post chain");
defer chain_zone.end();
+ g.post.scale = display_scale;
g.post.draw(g.device, cmd, g.linear_sampler, g.swapchain_format, g.scene_tex.?, target, sw, sh, c.SDL_GetTicksNS(), background_opacity >= 1) catch {
g.scene_target_failed = true;
};
@@ -6131,7 +6145,7 @@ fn scrollColumn(g: *const Gui, chrome: *const pardes.Chrome, col: u16, layout: C
const x0 = @round(@as(f32, @floatFromInt(col)) * layout.w + rule);
const scaled: f32 = @round(@as(f32, @floatFromInt(@max(1, chrome.rail_px))) * @as(f32, @floatFromInt(g.tagline_height)) / 17);
// The rail and the grip are GUTTER cells wide; the text starts after.
- const w = std.math.clamp(scaled, 4, @max(4, @floor(layout.w * @as(f32, @floatFromInt(config.GUTTER))) - rule - 3));
+ const w = std.math.clamp(scaled, dp(4), @max(dp(4), @floor(layout.w * @as(f32, @floatFromInt(config.GUTTER))) - rule - dp(3)));
return .{ x0, x0 + w };
}
@@ -7150,31 +7164,34 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
// the outline.
fn decorateLine(out: [*]u8, stride: usize, width: usize, height: usize, baseline: i32, decoration: GlyphDecoration) void {
if (width == 0 or height == 0) return;
+ // A line is a logical pixel thick, its dots and waves as long: the same
+ // weight at any density (dp).
+ const t: usize = @intFromFloat(dp(1));
if (decoration.underline != .off) {
const base: usize = @intCast(@max(0, baseline));
- const row = @min(base + 1, height - 1);
+ const row = @min(base + 1, height -| t);
for (0..width) |x| {
- switch (decoration.underline) {
+ const top = switch (decoration.underline) {
.off => unreachable,
- .single => out[row * stride + x] = 255,
+ .single => row,
.double => {
- const upper = @min(base + 1, height -| 3);
- out[upper * stride + x] = 255;
- out[@min(upper + 2, height - 1) * stride + x] = 255;
- },
- .curly => out[(row -| @as(usize, if (x % 4 < 2) 1 else 0)) * stride + x] = 255,
- .dotted => if (x % 3 == 0) {
- out[row * stride + x] = 255;
- },
- .dashed => if (x % 6 < 4) {
- out[row * stride + x] = 255;
+ const upper = @min(base + 1, height -| 3 * t);
+ for (0..t) |k| {
+ out[@min(upper + k, height - 1) * stride + x] = 255;
+ out[@min(upper + 2 * t + k, height - 1) * stride + x] = 255;
+ }
+ continue;
},
- }
+ .curly => row -| @as(usize, if (x % (4 * t) < 2 * t) t else 0),
+ .dotted => if (x % (3 * t) < t) row else continue,
+ .dashed => if (x % (6 * t) < 4 * t) row else continue,
+ };
+ for (0..t) |k| out[@min(top + k, height - 1) * stride + x] = 255;
}
}
if (decoration.strikethrough) {
- const row = @min(height - 1, @as(usize, @intCast(@max(0, baseline))) * 2 / 3);
- @memset(out[row * stride ..][0..width], 255);
+ const row = @min(height -| t, @as(usize, @intCast(@max(0, baseline))) * 2 / 3);
+ for (0..t) |k| @memset(out[@min(row + k, height - 1) * stride ..][0..width], 255);
}
}
@@ -7819,7 +7836,7 @@ fn addLift(g: *Gui, gpa: std.mem.Allocator, style: pardes.config.Runtime.LiftSty
if (style == .rim) if (rim_edge) |edge| {
const light = ceilings.rim[0] == 255;
const alpha = @min(Lift.rim * lift, if (light) ceilings.white else ceilings.shadow);
- try addDecor(g, gpa, edge[0], edge[2] - 1, edge[1], edge[2], ceilings.rim, alpha, track, win_w, win_h);
+ try addDecor(g, gpa, edge[0], edge[2] - dp(1), edge[1], edge[2], ceilings.rim, alpha, track, win_w, win_h);
};
}
@@ -8059,7 +8076,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
else => {},
} else false;
// As wide as the other rules, where the tagline leaves the room.
- const height: u32 = @max(1, @min(chrome.rule_px, (g.cell_h -| g.tagline_height) * 2));
+ const height: u32 = @intFromFloat(@max(1, @min(rulePx(chrome), @as(f32, @floatFromInt((g.cell_h -| g.tagline_height) * 2)))));
if (top != 0 and top < surface.rows and tag_under and height != 0) {
const y0: f32 = @floatFromInt(top * g.cell_h - height / 2);
try addDecor(g, gpa, 0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), chrome.topbar_rule, 1, null, win_w, win_h);
@@ -8145,9 +8162,9 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
// A band still falling into place is cut at its row's top; its
// rule comes with it, and shows only once it is below that cut.
const rule_y = top + (1 + layer.slide) * layout.h;
- if (rule_y - 1 < top) continue;
+ if (rule_y - dp(1) < top) continue;
const track = groups.items[groupAt(groups, layer.viewport.x, layer.viewport.y)].track;
- try addDecor(g, gpa, x0, rule_y - 1, rule_x1, rule_y, chrome.border, layer.fade, track, win_w, win_h);
+ try addDecor(g, gpa, x0, rule_y - dp(1), rule_x1, rule_y, chrome.border, layer.fade, track, win_w, win_h);
}
if (index == groups.ink and cursors_shown) try cursorDecor(g, gpa, surface, layout, win_w, win_h);
}
@@ -8173,7 +8190,7 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
const rule_x1 = if (win_w - x1 < layout.w) @max(x1, win_w) else x1;
const tag_bottom = tag.rect.y > rail.rect.y;
const rule_y = @as(f32, @floatFromInt(if (tag_bottom) tag.rect.y else tag.rect.y + tag.rect.h)) * layout.h;
- try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.tag_rule orelse mixRgb(chrome.tag_bg, page), 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - dp(1), rule_x1, if (tag_bottom) rule_y + dp(1) else rule_y, chrome.tag_rule orelse mixRgb(chrome.tag_bg, page), 1, track, win_w, win_h);
// Stacked panes are ruled apart as columns are, where one pane's
// body meets the next one's tag (acme's Border).
{
@@ -8189,10 +8206,10 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
const spare = g.cell_h -| g.tagline_height;
if (stacked and !tag_bottom) {
const room = taglineBandOffset(pane_top, win_h, g.cell_h, g.tagline_height);
- try addDecor(g, gpa, x0, y, rule_x1, y + @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), chrome.border, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y, rule_x1, y + @max(1, @min(rulePx(chrome), @as(f32, @floatFromInt(room)))), chrome.border, 1, track, win_w, win_h);
} else if (stacked and pane_top > 0) {
const room = spare - taglineBandOffset(pane_top - 1, win_h, g.cell_h, g.tagline_height);
- try addDecor(g, gpa, x0, y - @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), rule_x1, y, chrome.border, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y - @max(1, @min(rulePx(chrome), @as(f32, @floatFromInt(room)))), rule_x1, y, chrome.border, 1, track, win_w, win_h);
}
}
@@ -8211,7 +8228,7 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
if (rail.thumb_h != 0) {
const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h;
const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h;
- try addDecor(g, gpa, column[0], thumb_y0, column[1] - 1, thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h);
+ try addDecor(g, gpa, column[0], thumb_y0, column[1] - dp(1), thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h);
}
}
}
@@ -8225,7 +8242,34 @@ fn mixRgb(a: [3]u8, b: [3]u8) [3]u8 {
/// A rule's width: the theme's own, or one pixel.
fn rulePx(chrome: *const pardes.Chrome) f32 {
- return @floatFromInt(@max(1, chrome.rule_px));
+ return dp(@floatFromInt(@max(1, chrome.rule_px)));
+}
+
+/// The display's density (SDL's window display scale: 2 on a 2x screen),
+/// by which every size given in logical pixels is drawn, as acme scales its
+/// Border, ButtonBorder and Scrollgap (scalesize). Cells, glyphs and what is
+/// measured in them are already the display's pixels and are not scaled.
+/// ponytail: one window per process, so one global; per window if ever two.
+var display_scale: f32 = 1;
+
+/// The window's display scale, or PARDES_TEST_SCALE (a harness forcing 2x);
+/// a test window is 1x unless forced, whatever display it lands on, so its
+/// captures are the same on any machine.
+fn updateDisplayScale(window: *c.SDL_Window) void {
+ if (std.c.getenv("PARDES_TEST_SCALE")) |forced| {
+ display_scale = std.fmt.parseFloat(f32, std.mem.span(forced)) catch 1;
+ } else if (std.c.getenv("PARDES_TEST") != null) {
+ display_scale = 1;
+ } else {
+ const scale = c.SDL_GetWindowDisplayScale(window);
+ display_scale = if (scale > 0 and std.math.isFinite(scale)) scale else 1;
+ }
+ display_scale = std.math.clamp(display_scale, 1, 4);
+}
+
+/// A logical pixel size in the display's pixels: rounded, at least one.
+fn dp(logical: f32) f32 {
+ return @max(1, @round(logical * display_scale));
}
/// The two short bars on each row of a grip with no mode glyph there,
@@ -8262,22 +8306,25 @@ fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, ce
if ((other.kind == .body or other.kind == .tag) and other.owner != grip.owner and other.rect.y + other.rect.h == grip.rect.y and
other.rect.x < tag.rect.x + tag.rect.w and tag.rect.x < other.rect.x + other.rect.w) break true;
} else false;
- const top_rule: f32 = if (tag_bottom) 1 else if (stacked) @floatFromInt(@max(1, @min(chrome.rule_px, taglineBandOffset(grip.rect.y, win_h, g.cell_h, g.tagline_height)))) else 0;
- const bottom_rule: f32 = if (!tag_bottom and grip.rect.h == 1) 1 else 0;
+ const top_rule: f32 = if (tag_bottom) dp(1) else if (stacked) @max(1, @min(rulePx(chrome), @as(f32, @floatFromInt(taglineBandOffset(grip.rect.y, win_h, g.cell_h, g.tagline_height))))) else 0;
+ const bottom_rule: f32 = if (!tag_bottom and grip.rect.h == 1) dp(1) else 0;
const x0 = button[0];
const x1 = button[2];
const y0 = button[1] + top_rule;
const y1 = button[3] - bottom_rule;
const ring = if (grip.active) chrome.grip_focus_ring else chrome.grip_border;
- try addDecor(g, gpa, x0, y0, x1, y0 + 2, ring, 1, track, win_w, win_h);
- try addDecor(g, gpa, x0, y1 - 2, x1, y1, ring, 1, track, win_w, win_h);
- try addDecor(g, gpa, x0, y0 + 2, x0 + 2, y1 - 2, ring, 1, track, win_w, win_h);
- try addDecor(g, gpa, x1 - 2, y0 + 2, x1, y1 - 2, ring, 1, track, win_w, win_h);
+ // acme's ButtonBorder: two logical pixels.
+ const b = dp(2);
+ try addDecor(g, gpa, x0, y0, x1, y0 + b, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y1 - b, x1, y1, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y0 + b, x0 + b, y1 - b, ring, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x1 - b, y0 + b, x1, y1 - b, ring, 1, track, win_w, win_h);
if (grip.dirty and !grip.active) {
- try addDecor(g, gpa, x0 + 2, y0 + 2, x1 - 2, y0 + 3, band, 1, track, win_w, win_h);
- try addDecor(g, gpa, x0 + 2, y1 - 3, x1 - 2, y1 - 2, band, 1, track, win_w, win_h);
- try addDecor(g, gpa, x0 + 2, y0 + 3, x0 + 3, y1 - 3, band, 1, track, win_w, win_h);
- try addDecor(g, gpa, x1 - 3, y0 + 3, x1 - 2, y1 - 3, band, 1, track, win_w, win_h);
+ const gap = b + dp(1);
+ try addDecor(g, gpa, x0 + b, y0 + b, x1 - b, y0 + gap, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0 + b, y1 - gap, x1 - b, y1 - b, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0 + b, y0 + gap, x0 + gap, y1 - gap, band, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x1 - gap, y0 + gap, x1 - b, y1 - gap, band, 1, track, win_w, win_h);
}
}
const x = grip.rect.x;
@@ -8292,9 +8339,12 @@ fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, ce
if (line != 0) continue;
const center_x = @floor((button[0] + button[2]) / 2);
const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
- const half_w = @max(1, @min(3, @floor((button[2] - button[0] - 6) / 2)));
- try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), rgb, 1, track, win_w, win_h);
- try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), rgb, 1, track, win_w, win_h);
+ const half_w = @max(dp(1), @min(dp(3), @floor((button[2] - button[0] - 2 * dp(3)) / 2)));
+ // The mode's two bars: a logical pixel thick, a pixel either
+ // side of the middle.
+ const bar = dp(1);
+ try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y - bar - dp(1)), @floor(center_x + half_w), @floor(center_y - dp(1)), rgb, 1, track, win_w, win_h);
+ try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y + dp(1)), @floor(center_x + half_w), @floor(center_y + dp(1) + bar), rgb, 1, track, win_w, win_h);
}
}
}
@@ -8412,6 +8462,75 @@ test "an empty column is ruled on both sides, as between two filled ones" {
}
}
+test "at a 2x display every logical pixel is two: rules, the tag rule, the grip's ring, the rail's edge" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme acme"));
+ for (0..40) |_| core.update(.tick);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ for ([_]f32{ 1, 2 }) |scale| {
+ display_scale = scale;
+ defer display_scale = 1;
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 16;
+ g.cell_h = 32;
+ g.tagline_width = 12;
+ g.tagline_height = 24;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 1280, 768, true);
+ const w = struct {
+ fn of(item: CellInstance) f32 {
+ return (item.x1 - item.x0) / 2 * 1280;
+ }
+ fn h(item: CellInstance) f32 {
+ return (item.y0 - item.y1) / 2 * 768;
+ }
+ };
+ var spine: ?f32 = null;
+ var tag_rule: ?f32 = null;
+ var ring: ?f32 = null;
+ for (g.decor.items) |item| {
+ const rgb: [3]u8 = .{ @intFromFloat(@round(item.br * 255)), @intFromFloat(@round(item.bg * 255)), @intFromFloat(@round(item.bb * 255)) };
+ // The spine between the columns: black, tall.
+ if (std.mem.eql(u8, &rgb, &.{ 0, 0, 0 }) and w.h(item) > 300) spine = @round(w.of(item));
+ // The tag rule: #8888cc, wide and thin.
+ if (std.mem.eql(u8, &rgb, &.{ 0x88, 0x88, 0xcc }) and w.of(item) > 200) tag_rule = @round(w.h(item));
+ // The ring's left side: #8888cc, a tag row tall.
+ if (std.mem.eql(u8, &rgb, &.{ 0x88, 0x88, 0xcc }) and w.h(item) > 10 and w.of(item) < 8) ring = @round(w.of(item));
+ }
+ try std.testing.expectEqual(@as(?f32, 2 * scale), spine);
+ try std.testing.expectEqual(@as(?f32, 1 * scale), tag_rule);
+ try std.testing.expectEqual(@as(?f32, 2 * scale), ring);
+ // The rail starts after the rule, a logical 2 in, and keeps a
+ // logical 3 clear of the text when the cell is narrow.
+ const column = scrollColumn(&g, &s.chrome, 40, fixedCellLayout(&g));
+ try std.testing.expectEqual(40 * 16 + 2 * scale, column[0]);
+ g.cell_w = 12;
+ defer g.cell_w = 16;
+ const narrow = scrollColumn(&g, &s.chrome, 40, fixedCellLayout(&g));
+ try std.testing.expectEqual(@as(f32, 40 * 12 + 2 * scale), narrow[0]);
+ try std.testing.expectEqual(@as(f32, 40 * 12) + 12 * @as(f32, @floatFromInt(config.GUTTER)) - 3 * scale, narrow[1]);
+ g.cell_w = 8;
+ // A bar cursor is an eighth of its cell, never under a logical pixel
+ // (an 8 px cell: one pixel, two at 2x).
+ const saved = s.cursor;
+ defer s.cursor = saved;
+ s.cursor = .{ .x = 60, .y = 10, .bar = true };
+ var boxes: [max_cursors]CursorBox = undefined;
+ const n = cursorBoxes(&g, s, fixedCellLayout(&g), 768, &boxes);
+ const bar = for (boxes[0..n]) |box| {
+ if (box.bar and box.x0 == 60 * 8) break box;
+ } else return error.NoBarCursor;
+ try std.testing.expectEqual(scale, bar.x1 - bar.x0);
+ }
+}
+
test "decor is read from the regions, each pane's in its own group" {
const gpa = std.testing.allocator;
const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
@@ -8493,7 +8612,7 @@ fn cursorBoxes(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout
const height = std.math.clamp(if (role == .tagline) @as(f32, @floatFromInt(g.tagline_height)) else layout.h, 1.0, layout.h);
const x0 = @as(f32, @floatFromInt(cursor.x)) * layout.w;
const y0 = @as(f32, @floatFromInt(cursor.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(cursor.y, win_h, @intFromFloat(layout.h), @intFromFloat(height))));
- out[n] = .{ .x0 = x0, .y0 = y0, .x1 = x0 + if (cursor.bar) @max(1.0, width / 8.0) else width, .y1 = y0 + height, .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar };
+ out[n] = .{ .x0 = x0, .y0 = y0, .x1 = x0 + if (cursor.bar) @max(dp(1), width / 8.0) else width, .y1 = y0 + height, .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar };
n += 1;
}
}
@@ -8510,7 +8629,7 @@ fn cursorBoxes(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout
const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w;
const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
const cell = &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x];
- out[n] = .{ .x0 = x, .y0 = top, .x1 = x + if (cursor.bar) @max(1, width / 8) else width, .y1 = @min(bottom, top + height), .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar };
+ out[n] = .{ .x0 = x, .y0 = top, .x1 = x + if (cursor.bar) @max(dp(1), width / 8) else width, .y1 = @min(bottom, top + height), .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar };
n += 1;
}
for (surface.tagLayers()) |*layer| {
@@ -8520,11 +8639,12 @@ fn cursorBoxes(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout
const width: f32 = @floatFromInt(g.tagline_width);
const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
- const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
- if (x >= right) continue;
+ // Past a full row's end the caret sits at the tag's right edge, as
+ // the grid's does (and the core's cursor box, draw.focusedCursorBox).
+ const x = @min(left + @as(f32, @floatFromInt(cursor.x)) * width, right - if (cursor.bar) @max(dp(1), width / 8) else width);
const y = layer.viewport.y + cursor.y;
const top = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height)));
- out[n] = .{ .x0 = x, .y0 = top, .x1 = @min(right, x + if (cursor.bar) @max(1, width / 8) else width), .y1 = top + @as(f32, @floatFromInt(g.tagline_height)), .rgb = cursorColor(chrome, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline), .text = page, .bar = cursor.bar };
+ out[n] = .{ .x0 = x, .y0 = top, .x1 = @min(right, x + if (cursor.bar) @max(dp(1), width / 8) else width), .y1 = top + @as(f32, @floatFromInt(g.tagline_height)), .rgb = cursorColor(chrome, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline), .text = page, .bar = cursor.bar };
n += 1;
}
return n;