summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-18 23:40:51 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:45:58 -0300
commit7d2ade5ed706c67f30f54462684bced6a0978413 (patch)
tree3d73b92fe8072a3d250b334ff246200b76847c2e /src/gui
parentdba1ab69e5f38d8daf9312ab2d343b36fa078162 (diff)
downloadpardes-7d2ade5ed706c67f30f54462684bced6a0978413.tar.gz
pardes-7d2ade5ed706c67f30f54462684bced6a0978413.zip
gui: rendering rework, web css, config doc trim
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig321
1 files changed, 254 insertions, 67 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index dda843b5..63fe2091 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -114,26 +114,29 @@ const GlyphKey = struct {
role: pardes.FontRole,
};
-/// Raster parameters for the smaller tagline face. The atlas slot remains a
-/// body-sized cell: only the glyph shrinks, and its own line box is centered
-/// vertically inside that fixed geometry.
+/// Raster parameters for the smaller tagline face. Taglines are a real second
+/// grid: their glyph cell shrinks in both axes while pane geometry remains on
+/// the body grid.
const TaglineRaster = struct {
scale: f32,
+ width: u32,
baseline: i32,
height: u32,
};
-fn taglineRaster(font: *c.UIFont, body_px: f32, body_cell_h: u32, percent: u8) TaglineRaster {
+fn taglineRaster(font: *c.UIFont, body_px: f32, body_cell_w: u32, body_cell_h: u32, percent: u8) TaglineRaster {
const px = body_px * @as(f32, @floatFromInt(percent)) / 100.0;
const scale = c.ui_font_scale_for_height(font, px);
- var ignored_w: c_int = 1;
+ var own_w: c_int = 1;
var own_h: c_int = 1;
var own_ascent: c_int = 1;
- c.ui_font_cell_metrics(font, scale, &ignored_w, &own_h, &own_ascent);
+ c.ui_font_cell_metrics(font, scale, &own_w, &own_h, &own_ascent);
+ const fixed_w: c_int = @intCast(body_cell_w);
const fixed_h: c_int = @intCast(body_cell_h);
const band_h = std.math.clamp(own_h, 1, fixed_h);
return .{
.scale = scale,
+ .width = @intCast(std.math.clamp(own_w, 1, fixed_w)),
// Tagline slots use only their first `height` texel rows. Centering is
// done by the cell quad, not baked into this baseline, so changing the
// live percentage changes the chrome band as well as the glyph ink.
@@ -345,7 +348,7 @@ fn inkBounds(bitmap: []const u8, stride: usize, w: usize, h: usize) ?InkBounds {
return bounds;
}
-test "tagline glyph shrinks and stays centered in body cell geometry" {
+test "tagline glyph shrinks into its own cell and stays vertically centered" {
const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
defer c.ui_font_free(font);
@@ -354,8 +357,9 @@ test "tagline glyph shrinks and stays centered in body cell geometry" {
var cell_h: c_int = 1;
var body_ascent: c_int = 1;
c.ui_font_cell_metrics(font, body_scale, &cell_w, &cell_h, &body_ascent);
- const tag = taglineRaster(font, 27.0, @intCast(cell_h), config.gui_tagline_font_percent);
+ const tag = taglineRaster(font, 27.0, @intCast(cell_w), @intCast(cell_h), config.gui_tagline_font_percent);
try std.testing.expect(tag.scale < body_scale);
+ try std.testing.expect(tag.width < @as(u32, @intCast(cell_w)));
const stride: c_int = 128;
var body: [128 * 128]u8 = @splat(0);
@@ -376,12 +380,12 @@ test "tagline glyph shrinks and stays centered in body cell geometry" {
'H',
&tagline,
stride,
- cell_w,
+ @intCast(tag.width),
@intCast(tag.height),
tag.baseline,
));
const b = inkBounds(&body, @intCast(stride), @intCast(cell_w), @intCast(cell_h)).?;
- const t = inkBounds(&tagline, @intCast(stride), @intCast(cell_w), @intCast(cell_h)).?;
+ const t = inkBounds(&tagline, @intCast(stride), @intCast(tag.width), @intCast(tag.height)).?;
try std.testing.expect(t.max_x - t.min_x < b.max_x - b.min_x);
try std.testing.expect(t.max_y - t.min_y < b.max_y - b.min_y);
// Compare doubled centers to avoid floating point. Hinting may move either
@@ -391,7 +395,12 @@ test "tagline glyph shrinks and stays centered in body cell geometry" {
const body_center_y: isize = @intCast(b.min_y + b.max_y);
const centered_top: isize = @intCast((@as(u32, @intCast(cell_h)) - tag.height) / 2);
const tag_center_y: isize = @as(isize, @intCast(t.min_y + t.max_y)) + centered_top * 2;
- try std.testing.expect(@abs(tag_center_x - body_center_x) <= 2);
+ // Compare positions in their own grids. Both faces are centred within
+ // their natural monospace advance; the smaller face is not padded back
+ // out to a body-width cell.
+ const body_slot_center: isize = @intCast(@as(u32, @intCast(cell_w)) - 1);
+ const tag_slot_center: isize = @intCast(tag.width - 1);
+ try std.testing.expect(@abs((tag_center_x - tag_slot_center) - (body_center_x - body_slot_center)) <= 2);
try std.testing.expect(@abs(tag_center_y - body_center_y) <= 4);
try std.testing.expect(tag.height < @as(u32, @intCast(cell_h)));
}
@@ -404,9 +413,12 @@ test "tagline percentage changes measured band height without body metrics" {
var cell_h: c_int = 1;
var ascent: c_int = 1;
c.ui_font_cell_metrics(font, body_scale, &cell_w, &cell_h, &ascent);
- const small = taglineRaster(font, 27.0, @intCast(cell_h), 40);
- const configured = taglineRaster(font, 27.0, @intCast(cell_h), 82);
- const full = taglineRaster(font, 27.0, @intCast(cell_h), 100);
+ const small = taglineRaster(font, 27.0, @intCast(cell_w), @intCast(cell_h), 40);
+ const configured = taglineRaster(font, 27.0, @intCast(cell_w), @intCast(cell_h), 82);
+ const full = taglineRaster(font, 27.0, @intCast(cell_w), @intCast(cell_h), 100);
+ try std.testing.expect(small.width < configured.width);
+ try std.testing.expect(configured.width <= full.width);
+ try std.testing.expectEqual(@as(u32, @intCast(cell_w)), full.width);
try std.testing.expect(small.height < configured.height);
try std.testing.expect(configured.height <= full.height);
try std.testing.expect(full.height <= @as(u32, @intCast(cell_h)));
@@ -441,8 +453,9 @@ test "installed Nerd Symbols fallback covers Yazi directory icons" {
try std.testing.expectEqual(@as(c_int, 1), c.ui_font_has_glyph(face, 0xe5ff));
}
-// One instance per cell; the vertex shader expands it to a 2-triangle quad
-// with gl_VertexIndex. Coords are NDC (y up), uv into the atlas, colors 0..1.
+// One instance per body cell; a tagline cell adds its compact-grid foreground
+// instance. The vertex shader expands each to a 2-triangle quad with
+// gl_VertexIndex. Coords are NDC (y up), uv into the atlas, colors 0..1.
const CellInstance = extern struct {
x0: f32,
y0: f32,
@@ -770,12 +783,12 @@ fn takeScrollTicks(accum: *f32) i32 {
}
/// The SDL boundary owns touch policy: two-finger scroll and tap-as-execute.
-fn handleFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+fn handleFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32, tagline_w: f32) void {
std.debug.assert(cell_w > 0 and cell_h > 0);
- return handlePairFinger(t, core, f, win_w, win_h, cell_w, cell_h);
+ return handlePairFinger(t, core, f, win_w, win_h, cell_w, cell_h, tagline_w);
}
-fn handlePairFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+fn handlePairFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32, tagline_w: f32) void {
switch (f.kind) {
.down => {
t.updatePoint(f);
@@ -787,35 +800,33 @@ fn handlePairFinger(t: *Touch, core: *pardes.Pardes, f: Finger, win_w: f32, win_
t.syncPair();
if (!had_pair) return;
const res = t.noteMotion(f) orelse return;
- emitTouchScroll(core, res.center, res.ticks, res.delta_y, win_w, win_h, cell_w, cell_h);
+ emitTouchScroll(core, res.center, res.ticks, res.delta_y, win_w, win_h, cell_w, cell_h, tagline_w);
},
.up, .cancel => {
const tap_center = t.finishPair(f);
t.updatePoint(f);
t.syncPair();
- if (tap_center) |center| emitTouchClick(t, core, center, .middle, win_w, win_h, cell_w, cell_h);
+ if (tap_center) |center| emitTouchClick(t, core, center, .middle, win_w, win_h, cell_w, cell_h, tagline_w);
},
}
}
-fn emitTouchScroll(core: *pardes.Pardes, center: TouchNormPoint, ticks: i32, delta_y: f32, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
- const col = normCell(center.x, win_w, cell_w);
- const row = normCell(center.y, win_h, cell_h);
+fn emitTouchScroll(core: *pardes.Pardes, center: TouchNormPoint, ticks: i32, delta_y: f32, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32, tagline_w: f32) void {
+ const cell = gridCellAtDimensions(core, center.x * win_w, center.y * win_h, cell_w, cell_h, tagline_w);
const button: pardes.Mouse.Button = if (ticks > 0) .wheel_up else .wheel_down;
var remaining: u32 = @abs(ticks);
while (remaining > 0) : (remaining -= 1)
- core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } });
+ core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row } });
core.update(.{ .touch_scroll = delta_y });
}
-fn emitTouchClick(t: *Touch, core: *pardes.Pardes, point: TouchNormPoint, button: pardes.Mouse.Button, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32) void {
+fn emitTouchClick(t: *Touch, core: *pardes.Pardes, point: TouchNormPoint, button: pardes.Mouse.Button, win_w: f32, win_h: f32, cell_w: f32, cell_h: f32, tagline_w: f32) void {
t.click_count +%= 1;
t.click_button = button;
t.click_flash_frames = touch_click_flash_max_frames;
- const col = normCell(point.x, win_w, cell_w);
- const row = normCell(point.y, win_h, cell_h);
- core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } });
- core.update(.{ .mouse = .{ .button = button, .kind = .release, .col = col, .row = row } });
+ const cell = gridCellAtDimensions(core, point.x * win_w, point.y * win_h, cell_w, cell_h, tagline_w);
+ core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row } });
+ core.update(.{ .mouse = .{ .button = button, .kind = .release, .col = cell.col, .row = cell.row } });
}
fn normCell(norm: f32, win: f32, cell: f32) u16 {
@@ -1246,6 +1257,7 @@ const Gui = struct {
scale: f32,
tagline_scale: f32,
tagline_percent: u8,
+ tagline_width: u32,
tagline_height: u32,
cell_w: u32,
cell_h: u32,
@@ -1439,6 +1451,46 @@ fn fixedCellLayout(g: *const Gui) CellLayout {
return .{ .w = @floatFromInt(g.cell_w), .h = @floatFromInt(g.cell_h), .x_off = 0, .y_off = 0 };
}
+fn compactTaglineLayout(g: *const Gui, origin_col: f32) CellLayout {
+ const body_w: f32 = @floatFromInt(g.cell_w);
+ const tag_w: f32 = @floatFromInt(g.tagline_width);
+ return .{
+ .w = tag_w,
+ .h = @floatFromInt(g.cell_h),
+ // emitInstance still receives the canonical surface column. Offset
+ // the smaller grid so its column zero is the pane's physical left.
+ .x_off = origin_col * (body_w - tag_w),
+ .y_off = 0,
+ };
+}
+
+fn taglineLayoutForCell(
+ g: *const Gui,
+ core: *const pardes.Pardes,
+ col: u16,
+ row: u16,
+ track: ?pardes.panel_animation.Track,
+) CellLayout {
+ if (row < pardes.TOPBAR_H) return compactTaglineLayout(g, 0);
+ if (track) |active| {
+ const box = active.contentBox();
+ const tag_y = if (core.settings.tag_bottom) box.y + box.h - @as(f32, @floatFromInt(pardes.BOX_H)) else box.y;
+ if (@as(f32, @floatFromInt(row)) >= tag_y and
+ @as(f32, @floatFromInt(row)) < tag_y + @as(f32, @floatFromInt(pardes.BOX_H)))
+ return compactTaglineLayout(g, box.x);
+ }
+ for (core.panes, 0..) |slot, id| {
+ if (slot == null) continue;
+ const r = core.rects[id];
+ const tag_y = if (core.settings.tag_bottom) r.y + r.h -| pardes.BOX_H else r.y;
+ if (row == tag_y and col >= r.x and col < r.x + r.w)
+ return compactTaglineLayout(g, @floatFromInt(r.x));
+ }
+ // A stale/closing cell without a live pane should still remain legible.
+ // Its body-grid origin is the only safe fallback available.
+ return compactTaglineLayout(g, @floatFromInt(col));
+}
+
fn boxContains(box: pardes.panel_animation.Box, col: u16, row: u16) bool {
const x: f32 = @floatFromInt(col);
const y: f32 = @floatFromInt(row);
@@ -1649,7 +1701,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
c.ui_font_cell_metrics(font, scale, &cw, &chh, &asc);
const cell_w: u32 = @intCast(@max(cw, 1));
const cell_h: u32 = @intCast(@max(chh, 1));
- const tagline = taglineRaster(font, px, cell_h, config.gui_tagline_font_percent);
+ const tagline = taglineRaster(font, px, cell_w, cell_h, config.gui_tagline_font_percent);
if (test_mode) {
const cols = envU16(env, "PARDES_TEST_COLS") orelse 80;
const rows = envU16(env, "PARDES_TEST_ROWS") orelse 24;
@@ -1731,6 +1783,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
.scale = scale,
.tagline_scale = tagline.scale,
.tagline_percent = config.gui_tagline_font_percent,
+ .tagline_width = tagline.width,
.tagline_height = tagline.height,
.cell_w = cell_w,
.cell_h = cell_h,
@@ -2014,6 +2067,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
const presented = renderFrame(
&g,
gpa,
+ core,
surface,
core.theme().bg,
config.gui_topbar_pane_border_rgb orelse core.chromeTheme().scroll_track,
@@ -2259,14 +2313,14 @@ const StdinFeed = struct {
if (mapScenePoint(gp, finger.x * win_w, finger.y * win_h, win_w, win_h)) |m| {
finger.x = m.x / win_w;
finger.y = m.y / win_h;
- handleFinger(&gp.touch, core, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h));
+ handleFinger(&gp.touch, core, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h), @floatFromInt(gp.tagline_width));
} else {
finger.kind = .cancel;
- handleFinger(&gp.touch, core, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h));
+ handleFinger(&gp.touch, core, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h), @floatFromInt(gp.tagline_width));
core.update(.pointer_leave);
}
} else {
- handleFinger(&grid_touch, core, finger, @floatFromInt(core.screen_w), @floatFromInt(core.screen_h), 1, 1);
+ handleFinger(&grid_touch, core, finger, @floatFromInt(core.screen_w), @floatFromInt(core.screen_h), 1, 1, 1);
}
out.n_events += 1;
}
@@ -2548,7 +2602,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
g.pad_x = b.x;
g.pad_y = b.y;
g.pointer_present = true;
- const mc = mouseCell(g, b.x, b.y) orelse {
+ const mc = mouseCell(g, core, b.x, b.y) orelse {
g.pointer_mapped = false;
// A release outside the visible CRT tube still ends a drag at
// its last real cell; a press on black margin is inert.
@@ -2591,7 +2645,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
.right
else
null;
- const mc = mouseCell(g, m.x, m.y) orelse {
+ const mc = mouseCell(g, core, m.x, m.y) orelse {
g.pointer_mapped = false;
core.update(.pointer_leave);
return;
@@ -2611,7 +2665,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
g.pad_x = w.mouse_x;
g.pad_y = w.mouse_y;
g.pointer_present = true;
- const mc = mouseCell(g, w.mouse_x, w.mouse_y) orelse {
+ const mc = mouseCell(g, core, w.mouse_x, w.mouse_y) orelse {
g.pointer_mapped = false;
core.update(.pointer_leave);
return;
@@ -2688,10 +2742,10 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
if (mapScenePoint(g, finger.x * win_w, finger.y * win_h, win_w, win_h)) |m| {
finger.x = m.x / win_w;
finger.y = m.y / win_h;
- handleFinger(&g.touch, core, finger, win_w, win_h, fcw, fch);
+ handleFinger(&g.touch, core, finger, win_w, win_h, fcw, fch, @floatFromInt(g.tagline_width));
} else {
finger.kind = .cancel;
- handleFinger(&g.touch, core, finger, win_w, win_h, fcw, fch);
+ handleFinger(&g.touch, core, finger, win_w, win_h, fcw, fch, @floatFromInt(g.tagline_width));
core.update(.pointer_leave);
}
},
@@ -2741,7 +2795,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
for (acts[0..g.deck.feed(in, &acts)]) |act| switch (act) {
.move => |mv| {
const geometry = windowGeometry(g.window);
- const old = mouseCellWithGeometry(g, g.pad_x, g.pad_y, geometry);
+ const old = mouseCellWithGeometry(g, core, g.pad_x, g.pad_y, geometry);
// deck.Action.move is in physical screen pixels. Keep the
// stored/warped cursor in SDL window coordinates.
const dx = mv.dx * geometry.window_w / geometry.pixel_w;
@@ -2752,7 +2806,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
// cursor and a hardware mouse are one visible pointer
c.SDL_WarpMouseInWindow(g.window, g.pad_x, g.pad_y);
g.pointer_present = true;
- const mc = mouseCellWithGeometry(g, g.pad_x, g.pad_y, geometry) orelse {
+ const mc = mouseCellWithGeometry(g, core, g.pad_x, g.pad_y, geometry) orelse {
g.pointer_mapped = false;
core.update(.pointer_leave);
continue;
@@ -2778,7 +2832,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
.look => .right,
};
g.pointer_present = true;
- const mc = mouseCell(g, g.pad_x, g.pad_y) orelse {
+ const mc = mouseCell(g, core, g.pad_x, g.pad_y) orelse {
g.pointer_mapped = false;
// Mirror hardware mouse releases: black CRT margins
// are inert for presses, but cannot strand a drag whose
@@ -2803,7 +2857,7 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void {
},
.wheel => |ticks| {
g.pointer_present = true;
- const mc = mouseCell(g, g.pad_x, g.pad_y) orelse {
+ const mc = mouseCell(g, core, g.pad_x, g.pad_y) orelse {
g.pointer_mapped = false;
core.update(.pointer_leave);
continue;
@@ -2879,20 +2933,84 @@ fn pixelCell(px: f32, cell: u32) u16 {
return @intFromFloat(@min(idx, 10_000));
}
+fn gridCellAtDimensions(
+ core: *const pardes.Pardes,
+ x: f32,
+ y: f32,
+ body_w: f32,
+ body_h: f32,
+ tagline_w: f32,
+) MouseCell {
+ const safe_body_w = @max(body_w, 1);
+ const safe_body_h = @max(body_h, 1);
+ const tag_w = @max(tagline_w, 1);
+ const row: u16 = @intFromFloat(@min(@floor(@max(y, 0) / safe_body_h), 10_000));
+ if (row < pardes.TOPBAR_H)
+ return .{ .col = @intFromFloat(@min(@floor(@max(x, 0) / tag_w), 10_000)), .row = row };
+
+ for (core.panes, 0..) |slot, id| {
+ if (slot == null) continue;
+ const r = core.rects[id];
+ const tag_y = if (core.settings.tag_bottom) r.y + r.h -| pardes.BOX_H else r.y;
+ if (row != tag_y or r.w == 0) continue;
+ const left = @as(f32, @floatFromInt(r.x)) * safe_body_w;
+ const right = @as(f32, @floatFromInt(r.x + r.w)) * safe_body_w;
+ if (x < left or x >= right) continue;
+ const within: u16 = @intFromFloat(@min(@floor(@max(0, x - left) / tag_w), @as(f32, @floatFromInt(r.w - 1))));
+ return .{ .col = r.x + within, .row = row };
+ }
+ return .{ .col = @intFromFloat(@min(@floor(@max(x, 0) / safe_body_w), 10_000)), .row = row };
+}
+
+fn gridCellAtPixels(g: *const Gui, core: *const pardes.Pardes, x: f32, y: f32) MouseCell {
+ return gridCellAtDimensions(
+ core,
+ x,
+ y,
+ @floatFromInt(g.cell_w),
+ @floatFromInt(g.cell_h),
+ @floatFromInt(g.tagline_width),
+ );
+}
+
+test "tagline pointer mapping follows the smaller pane-anchored grid" {
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ core.update(.{ .resize = .{ .cols = 60, .rows = 12 } });
+ core.update(.{ .command = "Newcol" });
+
+ const r = core.rects[core.active];
+ const body_w: f32 = 10;
+ const body_h: f32 = 20;
+ const tag_w: f32 = 8;
+ const tag_y = r.y;
+ const x = @as(f32, @floatFromInt(r.x)) * body_w + 3.5 * tag_w;
+
+ const tag = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(tag_y)) + 0.5) * body_h, body_w, body_h, tag_w);
+ try std.testing.expectEqual(MouseCell{ .col = r.x + 3, .row = tag_y }, tag);
+
+ const body = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(tag_y + 1)) + 0.5) * body_h, body_w, body_h, tag_w);
+ try std.testing.expectEqual(pixelCell(x, 10), body.col);
+ try std.testing.expectEqual(tag_y + 1, body.row);
+
+ const topbar = gridCellAtDimensions(core, 3.5 * tag_w, 0.5 * body_h, body_w, body_h, tag_w);
+ try std.testing.expectEqual(MouseCell{ .col = 3, .row = 0 }, topbar);
+}
+
fn mapScenePoint(g: *const Gui, x: f32, y: f32, w: f32, h: f32) ?crt.Point {
return crt.mapScene(x, y, w, h, g.presented_scene.effects, g.presented_scene.time_seconds);
}
-fn mouseCellWithGeometry(g: *const Gui, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell {
+fn mouseCellWithGeometry(g: *const Gui, core: *const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell {
const physical = windowPointToPixels(geometry, x, y);
const mapped = mapScenePoint(g, physical.x, physical.y, geometry.pixel_w, geometry.pixel_h) orelse return null;
- return .{ .col = pixelCell(mapped.x, g.cell_w), .row = pixelCell(mapped.y, g.cell_h) };
+ return gridCellAtPixels(g, core, mapped.x, mapped.y);
}
/// SDL window coords → the scene cell displayed at that physical point.
/// Mouse, touch and the Deck pointer all share the same CRT/ripple/glitch map.
-fn mouseCell(g: *const Gui, x: f32, y: f32) ?MouseCell {
- return mouseCellWithGeometry(g, x, y, windowGeometry(g.window));
+fn mouseCell(g: *const Gui, core: *const pardes.Pardes, x: f32, y: f32) ?MouseCell {
+ return mouseCellWithGeometry(g, core, x, y, windowGeometry(g.window));
}
fn heldPointerButton(g: *const Gui) ?pardes.Mouse.Button {
@@ -2908,7 +3026,7 @@ fn heldPointerButton(g: *const Gui) ?pardes.Mouse.Button {
/// scene would continuously reset the core's hover debounce.
fn refreshPresentedPointer(g: *Gui, core: *pardes.Pardes) void {
if (!g.pointer_present) return;
- const mc = mouseCell(g, g.pad_x, g.pad_y) orelse {
+ const mc = mouseCell(g, core, g.pad_x, g.pad_y) orelse {
if (g.pointer_mapped) core.update(.pointer_leave);
g.pointer_mapped = false;
return;
@@ -2938,7 +3056,7 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void {
// right stick = pointer
const ax: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTX));
const ay: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTY));
- const old = mouseCellWithGeometry(g, g.pad_x, g.pad_y, geometry);
+ const old = mouseCellWithGeometry(g, core, g.pad_x, g.pad_y, geometry);
// full tilt ≈ 0.4 cell-heights per frame: a gentle, aimable glide (the
// mouse-move sensitivity knob — raise for a faster pointer)
const speed: f32 = @as(f32, @floatFromInt(g.cell_h)) * 0.4;
@@ -2953,7 +3071,7 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void {
g.pointer_present = true;
c.SDL_WarpMouseInWindow(g.window, g.pad_x, g.pad_y);
}
- const mc = mouseCellWithGeometry(g, g.pad_x, g.pad_y, geometry) orelse {
+ const mc = mouseCellWithGeometry(g, core, g.pad_x, g.pad_y, geometry) orelse {
if (g.pointer_present and g.pointer_mapped) core.update(.pointer_leave);
if (g.pointer_present) g.pointer_mapped = false;
return;
@@ -3883,6 +4001,7 @@ fn drawNativeImagesGpu(
fn renderFrame(
g: *Gui,
gpa: std.mem.Allocator,
+ core: *pardes.Pardes,
surface: *pardes.Surface,
theme_bg: ?[3]u8,
topbar_pane_border_rgb: [3]u8,
@@ -3954,6 +4073,7 @@ fn renderFrame(
// topbar/pane rule is anchored window chrome and remains present.
var overlay_count = buildOverlay(
g,
+ core,
surface,
layout,
sw,
@@ -3978,6 +4098,10 @@ fn renderFrame(
const cells: u32 = @as(u32, surface.cols) * surface.rows;
const page = ground(theme_bg);
+ const tagline_base = pardes.Cell{
+ .style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline },
+ .default = false,
+ };
var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
color_target.texture = scene;
color_target.clear_color = .{
@@ -4001,10 +4125,15 @@ fn renderFrame(
else
false;
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(&surface.previous_cells[logical_idx]) + cellInstanceCount(surface.at(col, row))
+ else
+ cellInstanceCount(surface.at(col, row));
paint_plan.batches[batch_index].cell_count = std.math.add(
u32,
paint_plan.batches[batch_index].cell_count,
- if (data_diff) 2 else 1,
+ instance_count,
) catch return error.GpuCapacity;
}
}
@@ -4021,7 +4150,12 @@ fn renderFrame(
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
const destination = if (duplicate_under) &paint_plan.batches[0] else batch;
- destination.cell_count = std.math.add(u32, destination.cell_count, 1) catch return error.GpuCapacity;
+ const logical_idx: u32 = @as(u32, row) * surface.cols + col;
+ destination.cell_count = std.math.add(
+ u32,
+ destination.cell_count,
+ cellInstanceCount(&surface.previous_cells[logical_idx]),
+ ) catch return error.GpuCapacity;
}
}
}
@@ -4062,10 +4196,11 @@ fn renderFrame(
false;
const data_diff = data_effect and surface.panelCellChanged(col, row);
if (data_diff) {
- emitInstance(
+ emitSurfaceCell(
g,
+ core,
instances,
- cell_next[batch_index],
+ &cell_next[batch_index],
col,
row,
layout,
@@ -4073,16 +4208,17 @@ fn renderFrame(
win_h,
track,
&surface.previous_cells[logical_idx],
+ &tagline_base,
true,
false,
page,
);
- cell_next[batch_index] += 1;
}
- emitInstance(
+ emitSurfaceCell(
g,
+ core,
instances,
- cell_next[batch_index],
+ &cell_next[batch_index],
col,
row,
layout,
@@ -4090,11 +4226,11 @@ fn renderFrame(
win_h,
if (data_effect and !data_diff) null else track,
surface.at(col, row),
+ &tagline_base,
false,
paint_plan.len == 1 and logical_idx == cursor_idx and !cursor_bar,
page,
);
- cell_next[batch_index] += 1;
}
}
for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
@@ -4108,10 +4244,11 @@ fn renderFrame(
if (!boxContains(track.contentBox(), col, row)) continue;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
const destination: usize = if (duplicate_under) 0 else batch_index;
- emitInstance(
+ emitSurfaceCell(
g,
+ core,
instances,
- cell_next[destination],
+ &cell_next[destination],
col,
row,
layout,
@@ -4119,11 +4256,11 @@ fn renderFrame(
win_h,
if (duplicate_under) null else track,
&surface.previous_cells[logical_idx],
+ &tagline_base,
false,
false,
page,
);
- cell_next[destination] += 1;
}
}
}
@@ -4340,6 +4477,47 @@ fn resolveCell(g: *Gui, cell: *const pardes.Cell, is_cursor: bool, page: [3]u8)
};
}
+fn cellFontRole(cell: *const pardes.Cell) pardes.FontRole {
+ return if (cell.default) .body else cell.style.font_role;
+}
+
+fn cellInstanceCount(cell: *const pardes.Cell) u32 {
+ return if (cellFontRole(cell) == .tagline) 2 else 1;
+}
+
+/// A tagline cell has two quads. The first preserves the pane-wide chrome
+/// band on the body grid; the second draws the real cell on the smaller text
+/// grid. Emitting them together in increasing column order is sufficient:
+/// the compact cell never reaches the next body's cell origin.
+fn emitSurfaceCell(
+ g: *Gui,
+ core: *const pardes.Pardes,
+ instances: [*]CellInstance,
+ next: *u32,
+ col: u16,
+ row: u16,
+ body_layout: CellLayout,
+ win_w: f32,
+ win_h: f32,
+ track: ?pardes.panel_animation.Track,
+ cell: *const pardes.Cell,
+ tagline_base: *const pardes.Cell,
+ old_layer: bool,
+ is_cursor: bool,
+ page: [3]u8,
+) void {
+ if (cellFontRole(cell) == .tagline) {
+ emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, tagline_base, old_layer, false, page);
+ next.* += 1;
+ const tag_layout = taglineLayoutForCell(g, core, col, row, track);
+ emitInstance(g, instances, next.*, col, row, tag_layout, win_w, win_h, track, cell, old_layer, is_cursor, page);
+ next.* += 1;
+ return;
+ }
+ emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, cell, old_layer, is_cursor, page);
+ next.* += 1;
+}
+
fn emitInstance(
g: *Gui,
instances: [*]CellInstance,
@@ -4378,6 +4556,7 @@ fn emitInstance(
const y1 = 1.0 - ((py0 + visual_h) / win_h) * 2.0;
const aw_f: f32 = @floatFromInt(atlas_w);
const ah_f: f32 = @floatFromInt(atlas_h);
+ const uv_w: u32 = if (resolved.role == .tagline) g.tagline_width else g.cell_w;
const uv_h: u32 = if (resolved.role == .tagline) g.tagline_height else g.cell_h;
instances[idx] = .{
@@ -4387,7 +4566,7 @@ fn emitInstance(
.y1 = y1,
.u0 = @as(f32, @floatFromInt(resolved.slot.u)) / aw_f,
.v0 = @as(f32, @floatFromInt(resolved.slot.v)) / ah_f,
- .u1 = @as(f32, @floatFromInt(resolved.slot.u + g.cell_w)) / aw_f,
+ .u1 = @as(f32, @floatFromInt(resolved.slot.u + uv_w)) / aw_f,
.v1 = @as(f32, @floatFromInt(resolved.slot.v + uv_h)) / ah_f,
.fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255.0,
.fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255.0,
@@ -4411,7 +4590,7 @@ fn emitInstance(
setTransitionFields(
&instances[idx],
track,
- layout,
+ fixedCellLayout(g),
win_w,
win_h,
if (track) |active| panelCellCoord(active, col, row) else 0,
@@ -4492,8 +4671,9 @@ fn refitFont(g: *Gui, core: *pardes.Pardes) void {
g.cell_w = @intCast(@max(cw, 1));
g.cell_h = @intCast(@max(chh, 1));
g.ascent = @intCast(@max(asc, 1));
- const tagline = taglineRaster(g.font, g.px, g.cell_h, g.tagline_percent);
+ const tagline = taglineRaster(g.font, g.px, g.cell_w, g.cell_h, g.tagline_percent);
g.tagline_scale = tagline.scale;
+ g.tagline_width = tagline.width;
g.tagline_baseline = tagline.baseline;
g.tagline_height = tagline.height;
@@ -4542,8 +4722,9 @@ fn syncTaglineFont(g: *Gui, core: *const pardes.Pardes) void {
const percent = core.settings.font.tagline_percent;
if (percent == g.tagline_percent) return;
g.tagline_percent = percent;
- const tagline = taglineRaster(g.font, g.px, g.cell_h, percent);
+ const tagline = taglineRaster(g.font, g.px, g.cell_w, g.cell_h, percent);
g.tagline_scale = tagline.scale;
+ g.tagline_width = tagline.width;
g.tagline_baseline = tagline.baseline;
g.tagline_height = tagline.height;
resetGlyphAtlas(g);
@@ -4562,8 +4743,9 @@ fn ensureGlyph(g: *Gui, cp: u32, role: pardes.FontRole) Slot {
const face = fontForCodepoint(g, cp);
const scale = if (role == .tagline) g.tagline_scale else g.scale;
const baseline = if (role == .tagline) g.tagline_baseline else g.ascent;
+ const raster_w = if (role == .tagline) g.tagline_width else g.cell_w;
const raster_h = if (role == .tagline) g.tagline_height else g.cell_h;
- _ = c.ui_font_raster(face, scale, @intCast(cp), out, @intCast(atlas_w), @intCast(g.cell_w), @intCast(raster_h), baseline);
+ _ = c.ui_font_raster(face, scale, @intCast(cp), out, @intCast(atlas_w), @intCast(raster_w), @intCast(raster_h), baseline);
g.glyphs.put(key, s) catch return g.space_slot;
g.pen_x += g.cell_w;
g.atlas_dirty = true;
@@ -4916,6 +5098,7 @@ fn addCursorBar(
fn buildOverlay(
g: *Gui,
+ core: *const pardes.Pardes,
surface: *const pardes.Surface,
layout: CellLayout,
sw: u32,
@@ -4961,7 +5144,11 @@ fn buildOverlay(
@floatFromInt(g.tagline_height)
else
layout.h;
- addCursorBar(&builder, cursor.x, cursor.y, cursor.bar, visual_height, layout);
+ const cursor_layout = if (role == .tagline)
+ taglineLayoutForCell(g, core, cursor.x, cursor.y, null)
+ else
+ layout;
+ addCursorBar(&builder, cursor.x, cursor.y, cursor.bar, visual_height, cursor_layout);
}
}
}