aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/Overlay.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/Overlay.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/Overlay.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/Overlay.zig415
1 files changed, 415 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/Overlay.zig b/vendor/libghostty-vt/src/renderer/Overlay.zig
new file mode 100644
index 00000000..62eb004b
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/Overlay.zig
@@ -0,0 +1,415 @@
+/// 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;
+
+const log = std.log.scoped(.renderer_overlay);
+
+/// The colors we use for overlays.
+pub const Color = enum {
+ hyperlink, // light blue
+ semantic_prompt, // orange/gold
+ semantic_input, // cyan
+
+ pub fn rgb(self: Color) z2d.pixel.RGB {
+ return switch (self) {
+ .hyperlink => .{ .r = 180, .g = 180, .b = 255 },
+ .semantic_prompt => .{ .r = 255, .g = 200, .b = 64 },
+ .semantic_input => .{ .r = 64, .g = 200, .b = 255 },
+ };
+ }
+
+ /// The fill color for rectangles.
+ pub fn rectFill(self: Color) z2d.Pixel {
+ return self.alphaPixel(96);
+ }
+
+ /// The border color for rectangles.
+ pub fn rectBorder(self: Color) z2d.Pixel {
+ return self.alphaPixel(200);
+ }
+
+ /// The raw RGB as a pixel.
+ pub fn pixel(self: Color) z2d.Pixel {
+ return self.rgb().asPixel();
+ }
+
+ fn alphaPixel(self: Color, alpha: u8) z2d.Pixel {
+ var rgba: z2d.pixel.RGBA = .fromPixel(self.pixel());
+ rgba.a = alpha;
+ return rgba.multiply().asPixel();
+ }
+};
+
+/// 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,
+ semantic_prompts,
+};
+
+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,
+ ),
+ .semantic_prompts => self.highlightSemanticPrompts(
+ 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_color = Color.hyperlink.rectBorder();
+ const fill_color = Color.hyperlink.rectFill();
+
+ 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.highlightGridRect(
+ alloc,
+ start_x,
+ y,
+ end_x - start_x,
+ 1,
+ border_color,
+ fill_color,
+ ) catch |err| {
+ std.log.warn("Error drawing hyperlink border: {}", .{err});
+ };
+ }
+ }
+}
+
+fn highlightSemanticPrompts(
+ self: *Overlay,
+ alloc: Allocator,
+ state: *const terminal.RenderState,
+) void {
+ const row_slice = state.row_data.slice();
+ const row_raw = row_slice.items(.raw);
+ const row_cells = row_slice.items(.cells);
+
+ // Highlight the row-level semantic prompt bars. The prompts are easy
+ // because they're part of the row metadata.
+ {
+ const prompt_border = Color.semantic_prompt.rectBorder();
+ const prompt_fill = Color.semantic_prompt.rectFill();
+
+ var y: usize = 0;
+ while (y < row_raw.len) {
+ // If its not a semantic prompt row, skip it.
+ if (row_raw[y].semantic_prompt == .none) {
+ y += 1;
+ continue;
+ }
+
+ // Find the full length of the semantic prompt row by connecting
+ // all continuations.
+ const start_y = y;
+ y += 1;
+ while (y < row_raw.len and
+ row_raw[y].semantic_prompt == .prompt_continuation)
+ {
+ y += 1;
+ }
+ const end_y = y; // Exclusive
+
+ const bar_width = @min(@as(usize, 5), self.cell_size.width);
+ self.highlightPixelRect(
+ alloc,
+ 0,
+ start_y,
+ bar_width,
+ end_y - start_y,
+ prompt_border,
+ prompt_fill,
+ ) catch |err| {
+ log.warn("Error drawing semantic prompt bar: {}", .{err});
+ };
+ }
+ }
+
+ // Highlight contiguous semantic cells within rows.
+ 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) {
+ const cell = raw_cells[x];
+ const content = cell.semantic_content;
+ const start_x = x;
+
+ // We skip output because its just the rest of the non-prompt
+ // parts and it makes the overlay too noisy.
+ if (cell.semantic_content == .output) {
+ x += 1;
+ continue;
+ }
+
+ // Find the end of this content.
+ x += 1;
+ while (x < raw_cells.len) {
+ const next = raw_cells[x];
+ if (next.semantic_content != content) break;
+ x += 1;
+ }
+
+ const color: Color = switch (content) {
+ .prompt => .semantic_prompt,
+ .input => .semantic_input,
+ .output => unreachable,
+ };
+
+ self.highlightGridRect(
+ alloc,
+ start_x,
+ y,
+ x - start_x,
+ 1,
+ color.rectBorder(),
+ color.rectFill(),
+ ) catch |err| {
+ log.warn("Error drawing semantic content highlight: {}", .{err});
+ };
+ }
+ }
+}
+
+/// Creates a rectangle for highlighting a grid region. x/y/width/height
+/// are all in grid cells.
+fn highlightGridRect(
+ 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();
+}
+
+/// Creates a rectangle for highlighting a region. x/y are grid cells and
+/// width/height are pixels.
+fn highlightPixelRect(
+ self: *Overlay,
+ alloc: Allocator,
+ x: usize,
+ y: usize,
+ width_px: usize,
+ height: usize,
+ border_color: z2d.Pixel,
+ fill_color: z2d.Pixel,
+) !void {
+ const px_width = std.math.cast(i32, width_px) 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;
+
+ 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));
+
+ var ctx: z2d.Context = .init(alloc, &self.surface);
+ defer ctx.deinit();
+
+ ctx.setAntiAliasingMode(.none);
+ ctx.setHairline(true);
+
+ 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();
+
+ ctx.setSourceToPixel(fill_color);
+ try ctx.fill();
+
+ ctx.setLineWidth(1);
+ ctx.setSourceToPixel(border_color);
+ try ctx.stroke();
+}