Files
ghostty/src/renderer/Overlay.zig
2026-01-30 20:39:20 -08:00

230 lines
7.0 KiB
Zig

/// The debug overlay that can be drawn on top of the terminal
/// during the rendering process.
///
/// This is implemented by doing all the drawing on the CPU via z2d,
/// since the debug overlay isn't that common, z2d is pretty fast, and
/// it simplifies our implementation quite a bit by not relying on us
/// having a bunch of shaders that we have to write per-platform.
///
/// Initialize the overlay, apply features with `applyFeatures`, then
/// get the resulting image with `pendingImage` to upload to the GPU.
/// This works in concert with `renderer.image.State` to simplify. Draw
/// it on the GPU as an image composited on top of the terminal output.
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;
const Image = @import("image.zig").Image;
/// 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 set of available features and their configuration.
pub const Feature = union(enum) {
highlight_hyperlinks,
};
pub const InitError = Allocator.Error || error{
// The terminal dimensions are invalid to support an overlay.
// Either too small or too big.
InvalidDimensions,
};
/// Initialize a new, blank overlay.
pub fn init(alloc: Allocator, sz: Size) InitError!Overlay {
// Our surface does NOT need to take into account padding because
// we render the overlay using the image subsystem and shaders which
// already take that into account.
const term_size = sz.terminal();
var sfc = z2d.Surface.initPixel(
.{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 0 } },
alloc,
std.math.cast(i32, term_size.width) orelse
return error.InvalidDimensions,
std.math.cast(i32, term_size.height) orelse
return error.InvalidDimensions,
) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.InvalidWidth, error.InvalidHeight => return error.InvalidDimensions,
};
errdefer sfc.deinit(alloc);
return .{
.surface = sfc,
.cell_size = sz.cell,
};
}
pub fn deinit(self: *Overlay, alloc: Allocator) void {
self.surface.deinit(alloc);
}
/// Returns a pending image that can be used to copy, convert, upload, etc.
pub fn pendingImage(self: *const Overlay) Image.Pending {
return .{
.width = @intCast(self.surface.getWidth()),
.height = @intCast(self.surface.getHeight()),
.pixel_format = .rgba,
.data = @ptrCast(self.surface.image_surface_rgba.buf.ptr),
};
}
/// Clear the overlay.
pub fn reset(self: *Overlay) void {
self.surface.paintPixel(.{ .rgba = .{
.r = 0,
.g = 0,
.b = 0,
.a = 0,
} });
}
/// Apply the given features to this overlay. This will draw on top of
/// any pre-existing content in the overlay.
pub fn applyFeatures(
self: *Overlay,
alloc: Allocator,
state: *const terminal.RenderState,
features: []const Feature,
) void {
for (features) |f| switch (f) {
.highlight_hyperlinks => self.highlightHyperlinks(
alloc,
state,
),
};
}
/// Add rectangles around contiguous 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.
fn highlightHyperlinks(
self: *Overlay,
alloc: Allocator,
state: *const terminal.RenderState,
) void {
const border_fill_rgb: z2d.pixel.RGB = .{ .r = 180, .g = 180, .b = 255 };
const border_color = border_fill_rgb.asPixel();
const fill_color: z2d.Pixel = px: {
var rgba: z2d.pixel.RGBA = .fromPixel(border_color);
rgba.a = 128;
break :px rgba.multiply().asPixel();
};
const row_slice = state.row_data.slice();
const row_raw = row_slice.items(.raw);
const row_cells = row_slice.items(.cells);
for (row_raw, row_cells, 0..) |row, cells, y| {
if (!row.hyperlink) continue;
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: z2d.Context = .init(alloc, &self.surface);
defer ctx.deinit();
// Don't need AA because we use sharp edges
ctx.setAntiAliasingMode(.none);
// Can use hairline since we have 1px borders
ctx.setHairline(true);
// 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();
}