diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 14:46:38 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (patch) | |
| tree | 642150be8703f1d9a0766ff18516f06bfdc8801a /src/gui | |
| parent | 379f4fe55c7c9c7c8cc7b97dc921c83e2c24698b (diff) | |
| download | pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.tar.gz pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.zip | |
Add optional compact tagline styling for source context
Diffstat (limited to 'src/gui')
| -rw-r--r-- | src/gui/gui.zig | 227 |
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); +} |
