diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-18 23:40:51 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:45:58 -0300 |
| commit | 7d2ade5ed706c67f30f54462684bced6a0978413 (patch) | |
| tree | 3d73b92fe8072a3d250b334ff246200b76847c2e /src | |
| parent | dba1ab69e5f38d8daf9312ab2d343b36fa078162 (diff) | |
| download | pardes-7d2ade5ed706c67f30f54462684bced6a0978413.tar.gz pardes-7d2ade5ed706c67f30f54462684bced6a0978413.zip | |
gui: rendering rework, web css, config doc trim
Diffstat (limited to 'src')
| -rw-r--r-- | src/gui/gui.zig | 321 | ||||
| -rw-r--r-- | src/web/pardes.css | 6 |
2 files changed, 257 insertions, 70 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); } } } diff --git a/src/web/pardes.css b/src/web/pardes.css index d523ec5c..5cb44f88 100644 --- a/src/web/pardes.css +++ b/src/web/pardes.css @@ -58,9 +58,9 @@ body { white-space: pre; } -/* The cell stays body-sized; only the glyph changes. The inherited fixed - line-height centers the smaller face vertically, and one glyph remains - centered in its original column just like the native atlas backends. */ +/* The browser keeps the canonical DOM grid body-sized for native text + selection. The inherited fixed line-height centers the smaller face, and + each glyph remains centered in its original DOM column. */ .pardes-cell.is-tagline { font-size: var(--pardes-tagline-font-size, 100%); text-align: center; |
