mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 21:12:39 +00:00
renderer: overlay system
This commit is contained in:
182
src/renderer/Overlay.zig
Normal file
182
src/renderer/Overlay.zig
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user