Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416/// 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();}