summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 14:46:38 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (patch)
tree642150be8703f1d9a0766ff18516f06bfdc8801a /src/gui/gui.zig
parent379f4fe55c7c9c7c8cc7b97dc921c83e2c24698b (diff)
downloadpardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.tar.gz
pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.zip
Add optional compact tagline styling for source context
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig227
1 files changed, 201 insertions, 26 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index f18f527b..91176a63 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -582,20 +582,23 @@ test "closing tombstone overlays but never owns canonical cells" {
try std.testing.expectEqual(pardes.layout.Phase.closing, plan.batches[1].track.?.phase);
}
-fn updateCoreResize(core: *pardes.Pardes, cols: u16, rows: u16, cell_w: u32, cell_h: u32) bool {
+fn updateCoreResize(core: *pardes.Pardes, cols: u16, rows: u16, cell_w: u32, cell_h: u32, tag_w: u32, tag_h: u32) bool {
+ const metrics: pardes.RowMetrics = .{ .body_w = @intCast(cell_w), .body_h = @intCast(cell_h), .tagline_w = @intCast(tag_w), .tagline_h = @intCast(tag_h) };
+ const same_metrics = std.meta.eql(metrics, core.row_metrics);
if (comptime pardes.pdf_enabled) {
const cell_px_w: u16 = @intCast(@min(@max(1, cell_w), std.math.maxInt(u16)));
const cell_px_h: u16 = @intCast(@min(@max(1, cell_h), std.math.maxInt(u16)));
if (cols == core.screen_w and rows == core.screen_h and
- cell_px_w == core.cell_pixels.w and cell_px_h == core.cell_pixels.h) return false;
+ cell_px_w == core.cell_pixels.w and cell_px_h == core.cell_pixels.h and same_metrics) return false;
core.update(.{ .resize = .{
.cols = cols,
.rows = rows,
.cell_pixels = .{ .w = cell_px_w, .h = cell_px_h },
+ .row_metrics = metrics,
} });
} else {
- if (cols == core.screen_w and rows == core.screen_h) return false;
- core.update(.{ .resize = .{ .cols = cols, .rows = rows } });
+ if (cols == core.screen_w and rows == core.screen_h and same_metrics) return false;
+ core.update(.{ .resize = .{ .cols = cols, .rows = rows, .row_metrics = metrics } });
}
return true;
}
@@ -763,7 +766,7 @@ fn emitTouchScroll(in: *Input, center: TouchNormPoint, ticks: i32, delta_y: f32,
const button: pardes.Mouse.Button = if (ticks > 0) .wheel_up else .wheel_down;
var remaining: u32 = @abs(ticks);
while (remaining > 0) : (remaining -= 1)
- in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row } });
+ in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row, .body_hit = cell.body_hit } });
in.post(.{ .touch_scroll = delta_y });
}
@@ -772,8 +775,8 @@ fn emitTouchClick(t: *Touch, in: *Input, point: TouchNormPoint, button: pardes.M
t.click_button = button;
t.click_flash_frames = touch_click_flash_max_frames;
const cell = gridCellAtDimensions(in.core, point.x * win_w, point.y * win_h, cell_w, cell_h, tagline_w);
- in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row } });
- in.post(.{ .mouse = .{ .button = button, .kind = .release, .col = cell.col, .row = cell.row } });
+ in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = cell.col, .row = cell.row, .body_hit = cell.body_hit } });
+ in.post(.{ .mouse = .{ .button = button, .kind = .release, .col = cell.col, .row = cell.row, .body_hit = cell.body_hit } });
}
fn normCell(norm: f32, win: f32, cell: f32) u16 {
@@ -1762,6 +1765,7 @@ const Gui = struct {
tagline_percent: u8,
tagline_width: u32,
tagline_height: u32,
+ attached_layers: ?*const [pardes.MAX_PANES]pardes.BodyLayer = null,
cell_w: u32,
cell_h: u32,
ascent: i32,
@@ -1890,7 +1894,7 @@ fn fontForCodepoint(g: *const Gui, cp: u32) *c.UIFont {
}
const CellLayout = struct { w: f32, h: f32, x_off: f32, y_off: f32 };
-const MouseCell = struct { col: u16, row: u16 };
+const MouseCell = struct { col: u16, row: u16, body_hit: ?pardes.Mouse.BodyHit = null };
const WindowGeometry = struct {
window_w: f32,
window_h: f32,
@@ -2527,13 +2531,24 @@ fn attachRequested(gpa: std.mem.Allocator, g: *Gui, requested: []const u8) !void
}
}
+fn attachedResize(client: *detached_client.Client, g: *const Gui, cols: u16, rows: u16) !void {
+ try client.send(.{ .event = .{ .resize = .{ .cols = cols, .rows = rows, .row_metrics = .{
+ .body_w = @intCast(g.cell_w),
+ .body_h = @intCast(g.cell_h),
+ .tagline_w = @intCast(g.tagline_width),
+ .tagline_h = @intCast(g.tagline_height),
+ } } } });
+}
+
fn attachedLoop(gpa: std.mem.Allocator, g: *Gui, client: *detached_client.Client) !void {
defer client.detach();
_ = c.SDL_StartTextInput(g.window);
var in: Input = .{ .client = client };
+ g.attached_layers = &client.body_layers;
var dirty = false;
var geom = windowCells(g);
+ try attachedResize(client, g, geom.cols, geom.rows);
while (true) {
const link = client.wait(detached_client.poll_ms);
while (true) {
@@ -2566,7 +2581,7 @@ fn attachedLoop(gpa: std.mem.Allocator, g: *Gui, client: *detached_client.Client
const now = windowCells(g);
if (now.cols != geom.cols or now.rows != geom.rows) {
geom = now;
- try client.resize(now.cols, now.rows);
+ try attachedResize(client, g, now.cols, now.rows);
}
if (!dirty) continue;
dirty = false;
@@ -2581,6 +2596,7 @@ fn paintAttached(g: *Gui, gpa: std.mem.Allocator, client: *detached_client.Clien
.cells = client.grid.items,
.cursor = if (client.cursor) |cu| .{ .x = cu.x, .y = cu.y, .bar = cu.bar } else null,
.pointer_shape = client.pointer_shape,
+ .body_layers = client.body_layers,
};
const chrome = frameChromeBg(&surface);
_ = renderFrame(
@@ -3053,6 +3069,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = .release,
.col = last.col,
.row = last.row,
+ .body_hit = last.body_hit,
} });
in.post(.pointer_leave);
return;
@@ -3065,6 +3082,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = if (b.down) .press else .release,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
.ctrl = (c.SDL_GetModState() & c.SDL_KMOD_CTRL) != 0,
},
});
@@ -3094,6 +3112,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = if (held != null) .drag else .motion,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
},
c.SDL_EVENT_MOUSE_WHEEL => {
@@ -3140,9 +3159,9 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
} else {
g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y);
while (g.scroll_delta >= 1) : (g.scroll_delta -= 1)
- in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row } });
+ in.post(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mc.col, .row = mc.row, .body_hit = mc.body_hit } });
while (g.scroll_delta <= -1) : (g.scroll_delta += 1)
- in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row } });
+ in.post(.{ .mouse = .{ .button = .wheel_up, .kind = .press, .col = mc.col, .row = mc.row, .body_hit = mc.body_hit } });
}
}
if (w.x != 0) in.post(.{ .mouse = .{
@@ -3150,6 +3169,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = .press,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
},
c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
@@ -3247,6 +3267,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = if (held != null) .drag else .motion,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
},
.click => |ck| {
@@ -3263,6 +3284,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = .release,
.col = last.col,
.row = last.row,
+ .body_hit = last.body_hit,
} });
in.post(.pointer_leave);
continue;
@@ -3274,6 +3296,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = if (ck.down) .press else .release,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
},
.wheel => |ticks| {
@@ -3293,6 +3316,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.kind = .press,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
}
},
@@ -3364,14 +3388,20 @@ fn gridCellAtDimensions(
}
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),
- );
+ var point = gridCellAtDimensions(core, x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width));
+ if (core) |p| if (p.presentation.pointerFractional(p.screen_w, p.screen_h, x / @as(f32, @floatFromInt(g.cell_w)), y / @as(f32, @floatFromInt(g.cell_h)))) |mapped| for (p.surface.bodyLayers()) |*layer| {
+ if (layer.hitAt(mapped.x * @as(f32, @floatFromInt(g.cell_w)), mapped.y * @as(f32, @floatFromInt(g.cell_h)), @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |hit| {
+ point.body_hit = hit;
+ break;
+ }
+ };
+ if (core == null) if (g.attached_layers) |layers| for (layers) |*layer| {
+ if (layer.hitAt(x, y, @floatFromInt(g.cell_w), @floatFromInt(g.cell_h), @floatFromInt(g.tagline_width), @floatFromInt(g.tagline_height))) |body_hit| {
+ point.body_hit = body_hit;
+ break;
+ }
+ };
+ return point;
}
test "tagline pointer mapping follows the smaller pane-anchored grid" {
@@ -3438,6 +3468,7 @@ fn refreshPresentedPointer(g: *Gui, core: *pardes.Pardes) void {
.kind = if (held != null) .drag else .motion,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
}
@@ -3473,6 +3504,7 @@ fn pollGamepad(g: *Gui, in: *Input) void {
.kind = if (held != null) .drag else .motion,
.col = mc.col,
.row = mc.row,
+ .body_hit = mc.body_hit,
} });
}
}
@@ -3483,12 +3515,12 @@ fn pollGamepad(g: *Gui, in: *Input) void {
while (@abs(g.pad_scroll) >= 1.0) {
const button: pardes.Mouse.Button = if (g.pad_scroll < 0) .wheel_up else .wheel_down;
g.pad_scroll += if (g.pad_scroll < 0) 1.0 else -1.0;
- in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row } });
+ in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row, .body_hit = mc.body_hit } });
}
while (@abs(g.pad_scroll_h) >= 1.0) {
const button: pardes.Mouse.Button = if (g.pad_scroll_h < 0) .wheel_left else .wheel_right;
g.pad_scroll_h += if (g.pad_scroll_h < 0) 1.0 else -1.0;
- in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row } });
+ in.post(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row, .body_hit = mc.body_hit } });
}
}
@@ -3750,7 +3782,7 @@ fn pollFrame(ctx: ?*anyopaque) void {
}
pollCwds(core, s.ptys);
const geom = windowCells(g);
- if (updateCoreResize(core, geom.cols, geom.rows, g.cell_w, g.cell_h)) resetScroll(g);
+ if (updateCoreResize(core, geom.cols, geom.rows, g.cell_w, g.cell_h, g.tagline_width, g.tagline_height)) resetScroll(g);
stepScroll(g, core, s.gpa);
}
@@ -4859,6 +4891,7 @@ fn renderFrame(
while (row < surface.rows) : (row += 1) {
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
+ if (bodyLayerAt(surface, col, row) != null) continue;
const batch_index = paintBatchAt(&paint_plan, col, row);
const track = paint_plan.batches[batch_index].track;
const data_effect = if (track) |active|
@@ -4887,6 +4920,7 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
+ if (bodyLayerIn(surface.previous_body_layers, col, row) != null) continue;
const destination = if (duplicate_under) &paint_plan.batches[0] else batch;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
destination.cell_count = std.math.add(
@@ -4897,6 +4931,29 @@ fn renderFrame(
}
}
}
+ for (surface.bodyLayers()) |*layer| {
+ if (layer.rows == 0) continue;
+ const batch = &paint_plan.batches[paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y)];
+ const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
+ for (0..layer.rows) |lr| {
+ if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break;
+ batch.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0);
+ }
+ }
+ for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
+ const track = batch.track.?;
+ const under = track.effect == .vertical and track.phase == .opening;
+ if (!under and track.phase != .closing and track.effect != .dissolve) continue;
+ for (surface.previous_body_layers) |*layer| {
+ if (layer.rows == 0 or layer.serial != track.serial) continue;
+ const destination = if (under) &paint_plan.batches[0] else &paint_plan.batches[batch_index];
+ const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
+ for (0..layer.rows) |lr| {
+ if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break;
+ destination.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0);
+ }
+ }
+ }
var cell_next: [pardes.MAX_PANES * 2 + 1]u32 = @splat(0);
var cell_total: u32 = 0;
for (paint_plan.batches[0..paint_plan.len], 0..) |*batch, batch_index| {
@@ -4920,6 +4977,7 @@ fn renderFrame(
while (row < surface.rows) : (row += 1) {
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
+ if (bodyLayerAt(surface, col, row) != null) continue;
const batch_index = paintBatchAt(&paint_plan, col, row);
const track = paint_plan.batches[batch_index].track;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
@@ -4961,11 +5019,26 @@ fn renderFrame(
surface.at(col, row),
&tagline_base,
false,
- paint_plan.len == 1 and logical_idx == cursor_idx and !cursor_bar,
+ paint_plan.len == 1 and logical_idx == cursor_idx and !cursor_bar and bodyLayerAt(surface, col, row) == null,
page,
);
}
}
+ for (surface.bodyLayers()) |*layer| {
+ if (layer.rows == 0) continue;
+ const batch_index = paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y);
+ emitBodyLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false);
+ }
+ for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
+ const track = batch.track.?;
+ const under = track.effect == .vertical and track.phase == .opening;
+ if (!under and track.phase != .closing and track.effect != .dissolve) continue;
+ for (surface.previous_body_layers) |*layer| {
+ if (layer.rows == 0 or layer.serial != track.serial) continue;
+ const destination: usize = if (under) 0 else batch_index;
+ emitBodyLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve);
+ }
+ }
for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
const track = batch.track.?;
const duplicate_under = track.effect == .vertical and track.phase == .opening;
@@ -4975,6 +5048,7 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
+ if (bodyLayerIn(surface.previous_body_layers, col, row) != null) continue;
const logical_idx: u32 = @as(u32, row) * surface.cols + col;
const destination: usize = if (duplicate_under) 0 else batch_index;
emitSurfaceCell(
@@ -5263,6 +5337,59 @@ test "compact column and pane underlays share focus tint without tinting the glo
try std.testing.expectEqual(chrome.tag_bg, taglineBaseRgb(core, active.x, pardes.TOPBAR_H));
}
+fn bodyLayerAt(surface: *const pardes.Surface, col: u16, row: u16) ?*const pardes.BodyLayer {
+ return bodyLayerIn(surface.bodyLayers(), col, row);
+}
+
+fn bodyLayerIn(layers: []const pardes.BodyLayer, col: u16, row: u16) ?*const pardes.BodyLayer {
+ for (layers) |*layer| {
+ const r = layer.viewport;
+ if (layer.rows > 0 and col >= r.x and col - r.x < r.w and row >= r.y and row - r.y < r.h) return layer;
+ }
+ return null;
+}
+
+fn clipBodyInstance(instance: *CellInstance, bottom: f32, win_h: f32) void {
+ const limit = 1 - bottom / win_h * 2;
+ if (instance.y1 >= limit) return;
+ const fraction = (instance.y0 - limit) / (instance.y0 - instance.y1);
+ instance.v1 = instance.v0 + (instance.v1 - instance.v0) * fraction;
+ instance.y1 = limit;
+}
+
+fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const pardes.BodyLayer, win_w: f32, win_h: f32, track: ?pardes.layout.Track, page: Ground, show_cursor: bool, old_layer: bool) void {
+ const bw: f32 = @floatFromInt(g.cell_w);
+ const bh: f32 = @floatFromInt(g.cell_h);
+ const tw: f32 = @floatFromInt(g.tagline_width);
+ const th: f32 = @floatFromInt(g.tagline_height);
+ const left = @as(f32, @floatFromInt(layer.viewport.x)) * bw;
+ const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * bh;
+ for (0..layer.rows) |index| {
+ const row: u16 = @intCast(index);
+ const top = layer.rowTop(row, bh, th);
+ if (top >= bottom) break;
+ const height = layer.rowHeight(row, bh, th);
+ const header = row < layer.context_rows;
+ const role: pardes.FontRole = if (header) .tagline else .body;
+ const width = if (header) tw else bw;
+ if (header) {
+ var background: pardes.Cell = .{ .default = false, .style = layer.cells[index * layer.cols].style };
+ background.style.font_role = .body;
+ emitInstance(g, instances, next.*, 0, 0, .{ .w = @as(f32, @floatFromInt(layer.viewport.w)) * bw, .h = height, .x_off = left, .y_off = top }, win_w, win_h, track, .body, &background, old_layer, false, page);
+ clipBodyInstance(&instances[next.*], bottom, win_h);
+ next.* += 1;
+ }
+ for (0..layer.cols) |col| {
+ const cell = &layer.cells[index * layer.cols + col];
+ const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == row else false;
+ const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height))) else 0);
+ emitInstance(g, instances, next.*, 0, 0, .{ .w = width, .h = height, .x_off = left + @as(f32, @floatFromInt(col)) * width, .y_off = glyph_top }, win_w, win_h, track, role, cell, old_layer, cursor, page);
+ clipBodyInstance(&instances[next.*], bottom, win_h);
+ next.* += 1;
+ }
+ }
+}
+
fn emitSurfaceCell(
g: *Gui,
core: ?*const pardes.Pardes,
@@ -5478,7 +5605,7 @@ fn refitFont(g: *Gui, core: ?*pardes.Pardes) void {
if (core) |p| {
const geom = windowCells(g);
- _ = updateCoreResize(p, geom.cols, geom.rows, g.cell_w, g.cell_h);
+ _ = updateCoreResize(p, geom.cols, geom.rows, g.cell_w, g.cell_h, g.tagline_width, g.tagline_height);
}
resetScroll(g);
@@ -5494,7 +5621,7 @@ fn resetGlyphAtlas(g: *Gui) void {
g.atlas_dirty = true;
}
-fn syncTaglineFont(g: *Gui, core: *const pardes.Pardes) void {
+fn syncTaglineFont(g: *Gui, core: *pardes.Pardes) void {
const percent = core.settings.font.tagline_percent;
if (percent == g.tagline_percent) return;
g.tagline_percent = percent;
@@ -5504,6 +5631,8 @@ fn syncTaglineFont(g: *Gui, core: *const pardes.Pardes) void {
g.tagline_baseline = tagline.baseline;
g.tagline_height = tagline.height;
resetGlyphAtlas(g);
+ const geom = windowCells(g);
+ _ = updateCoreResize(core, geom.cols, geom.rows, g.cell_w, g.cell_h, g.tagline_width, g.tagline_height);
}
fn ensureGlyph(g: *Gui, cp: u32, role: pardes.FontRole, decoration: GlyphDecoration) Slot {
@@ -6096,7 +6225,7 @@ fn buildOverlay(
if (transient_on) {
if (core) |p| appendPaneChrome(g, p, surface, layout, &builder);
if (surface.cursor) |cursor| {
- if (cursor.x < surface.cols and cursor.y < surface.rows) {
+ if (cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null) {
const cell = surface.cells[@as(usize, cursor.y) * surface.cols + cursor.x];
const role = drawnFontRole(core, &cell, cursor.y);
const visual_height: f32 = if (role == .tagline)
@@ -6111,6 +6240,25 @@ fn buildOverlay(
}
}
}
+ if (transient_on) for (surface.bodyLayers()) |*layer| {
+ if (layer.rows == 0) continue;
+ const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
+ const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
+ const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
+ const border = layer.rowTop(layer.context_rows, layout.h, @floatFromInt(g.tagline_height));
+ if (border <= bottom) builder.addRect(left, border - 1, right, border, overlayRgb(topbar_pane_border_rgb));
+ if (layer.cursor) |cursor| {
+ if (!cursor.bar or cursor.y >= layer.rows or cursor.x >= layer.cols) continue;
+ const top = layer.rowTop(cursor.y, layout.h, @floatFromInt(g.tagline_height));
+ if (top >= bottom) continue;
+ const height = layer.rowHeight(cursor.y, layout.h, @floatFromInt(g.tagline_height));
+ const role: pardes.FontRole = if (cursor.y < layer.context_rows) .tagline else .body;
+ 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];
+ builder.addRect(x, top, x + @max(1, width / 8), @min(bottom, top + height), overlayRgb(cursorColor(core, cell, role)));
+ }
+ };
if (transient_on) appendTouchOverlay(g, &builder, debug_on);
return @intCast(builder.len);
}
@@ -6655,3 +6803,30 @@ fn envU16(env: *std.process.Environ.Map, name: []const u8) ?u16 {
const raw = env.get(name) orelse return null;
return std.fmt.parseInt(u16, raw, 10) catch null;
}
+
+test "compact body layer rendering fills recovered rows and clips the final glyph" {
+ var g: Gui = undefined;
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ var cells: [24]pardes.Cell = @splat(.{});
+ const layer: pardes.BodyLayer = .{
+ .viewport = .{ .x = 3, .y = 2, .w = 2, .h = 5 },
+ .cols = 2,
+ .rows = 12,
+ .context_rows = 4,
+ .cells = &cells,
+ };
+ var instances: [32]CellInstance = undefined;
+ var next: u32 = 0;
+ emitBodyLayer(&g, &instances, &next, &layer, 200, 200, null, Ground.opaqueRgb(bg_default), false, false);
+ // Four compact header rows (48px), then three body rows: two complete
+ // and a third clipped to the remaining12px of the100px viewport.
+ try std.testing.expectEqual(@as(u32, 18), next);
+ try std.testing.expectApproxEqAbs(@as(f32, 0.6), instances[0].y0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, 0.12), instances[12].y0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, -0.4), instances[17].y1, 0.0001);
+ try std.testing.expectApproxEqAbs(instances[17].v0 + @as(f32, 12) / atlas_h, instances[17].v1, 0.0001);
+}