renderer: overlay system

This commit is contained in:
Mitchell Hashimoto
2026-01-30 11:23:45 -08:00
parent 89d3ad2bf3
commit f176342537
2 changed files with 213 additions and 0 deletions

182
src/renderer/Overlay.zig Normal file
View File

@@ -0,0 +1,182 @@
const Overlay = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const terminal = @import("../terminal/main.zig");
const size = @import("size.zig");
const Size = size.Size;
const CellSize = size.CellSize;
/// The surface we're drawing our overlay to.
surface: z2d.Surface,
/// Cell size information so we can map grid coordinates to pixels.
cell_size: CellSize,
/// The transformation to apply to the overlay to account for the
/// screen padding.
padding_transformation: z2d.Transformation,
/// Initialize a new, blank overlay.
pub fn init(alloc: Allocator, sz: Size) !Overlay {
var sfc: z2d.Surface = try .initPixel(
.{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 0 } },
alloc,
std.math.cast(i32, sz.screen.width).?,
std.math.cast(i32, sz.screen.height).?,
);
errdefer sfc.deinit(alloc);
return .{
.surface = sfc,
.cell_size = sz.cell,
.padding_transformation = .{
.ax = 1,
.by = 0,
.cx = 0,
.dy = 1,
.tx = @as(f64, @floatFromInt(sz.padding.left)),
.ty = @as(f64, @floatFromInt(sz.padding.top)),
},
};
}
pub fn deinit(self: *Overlay, alloc: Allocator) void {
self.surface.deinit(alloc);
}
/// Add rectangles around continguous hyperlinks in the render state.
///
/// Note: this currently doesn't take into account unique hyperlink IDs
/// because the render state doesn't contain this. This will be added
/// later.
pub fn highlightHyperlinks(
self: *Overlay,
alloc: Allocator,
state: *const terminal.RenderState,
) void {
// Border and fill colors (premultiplied alpha, 50% alpha for fill)
const border_color: z2d.Pixel = .{ .rgba = .{
.r = 128,
.g = 128,
.b = 255,
.a = 255,
} };
// Fill: 50% alpha (128/255), so premultiply RGB by 128/255
const fill_color: z2d.Pixel = .{ .rgba = .{
.r = 64,
.g = 64,
.b = 128,
.a = 128,
} };
const row_slice = state.row_data.slice();
const row_cells = row_slice.items(.cells);
for (row_cells, 0..) |cells, y| {
const cells_slice = cells.slice();
const raw_cells = cells_slice.items(.raw);
var x: usize = 0;
while (x < raw_cells.len) {
// Skip cells without hyperlinks
if (!raw_cells[x].hyperlink) {
x += 1;
continue;
}
// Found start of a hyperlink run
const start_x = x;
// Find end of contiguous hyperlink cells
while (x < raw_cells.len and raw_cells[x].hyperlink) x += 1;
const end_x = x;
self.highlightRect(
alloc,
start_x,
y,
end_x - start_x,
1,
border_color,
fill_color,
) catch |err| {
std.log.warn("Error drawing hyperlink border: {}", .{err});
};
}
}
}
/// Creates a rectangle for highlighting a grid region. x/y/width/height
/// are all in grid cells.
fn highlightRect(
self: *Overlay,
alloc: Allocator,
x: usize,
y: usize,
width: usize,
height: usize,
border_color: z2d.Pixel,
fill_color: z2d.Pixel,
) !void {
// All math below uses checked arithmetic to avoid overflows. The
// inputs aren't trusted and the path this is in isn't hot enough
// to wrarrant unsafe optimizations.
// Calculate our width/height in pixels.
const px_width = std.math.cast(i32, try std.math.mul(
usize,
width,
self.cell_size.width,
)) orelse return error.Overflow;
const px_height = std.math.cast(i32, try std.math.mul(
usize,
height,
self.cell_size.height,
)) orelse return error.Overflow;
// Calculate pixel coordinates
const start_x: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
usize,
x,
self.cell_size.width,
)) orelse return error.Overflow);
const start_y: f64 = @floatFromInt(std.math.cast(i32, try std.math.mul(
usize,
y,
self.cell_size.height,
)) orelse return error.Overflow);
const end_x: f64 = start_x + @as(f64, @floatFromInt(px_width));
const end_y: f64 = start_y + @as(f64, @floatFromInt(px_height));
// Grab our context to draw
var ctx = self.newContext(alloc);
defer ctx.deinit();
// Draw rectangle path
try ctx.moveTo(start_x, start_y);
try ctx.lineTo(end_x, start_y);
try ctx.lineTo(end_x, end_y);
try ctx.lineTo(start_x, end_y);
try ctx.closePath();
// Fill
ctx.setSourceToPixel(fill_color);
try ctx.fill();
// Border
ctx.setLineWidth(1);
ctx.setSourceToPixel(border_color);
try ctx.stroke();
}
/// Creates a new context for drawing to the overlay that takes into
/// account the padding transformation so you can work directly in the
/// terminal's coordinate space.
///
/// Caller must deinit the context when done.
fn newContext(self: *Overlay, alloc: Allocator) z2d.Context {
var ctx: z2d.Context = .init(alloc, &self.surface);
ctx.setTransformation(self.padding_transformation);
return ctx;
}

View File

@@ -1282,6 +1282,29 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Reset our dirty state after updating.
defer self.terminal_state.dirty = .false;
// Rebuild the overlay image set.
overlay: {
const alloc = arena_alloc;
// Create a surface that is the size of the entire screen,
// including padding. It is transparent, since we'll overlay
// it on top of our screen.
var overlay: Overlay = self.rebuildOverlay(alloc) catch |err| {
log.warn("error rebuilding overlay surface err={}", .{err});
break :overlay;
};
defer overlay.deinit(alloc);
// Grab our mutex so we can upload some images.
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// IMPORTANT: This must be done AFTER kitty graphics
// are setup because Kitty graphics will clear all our
// "unused" images and our overlay will appear unused since
// its not part of the Kitty state.
}
// Acquire the draw mutex for all remaining state updates.
{
self.draw_mutex.lock();
@@ -2179,6 +2202,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
const Overlay = @import("Overlay.zig");
fn rebuildOverlay(self: *Self, alloc: Allocator) !Overlay {
var overlay: Overlay = try .init(alloc, self.size);
overlay.highlightHyperlinks(alloc, &self.terminal_state);
return overlay;
}
const PreeditRange = struct {
y: terminal.size.CellCountInt,
x: [2]terminal.size.CellCountInt,