mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-15 03:52:39 +00:00
230 lines
7.0 KiB
Zig
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();
|
|
}
|