From ee45d363a96077a7142ee72130df5d2f36d667bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 19:47:15 -0700 Subject: [PATCH] metal: cursor and underline --- src/renderer.zig | 1 + src/renderer/Metal.zig | 151 ++++++++++++++++++++++++++++++++++------ src/renderer/OpenGL.zig | 38 +++------- src/shaders/cell.metal | 126 ++++++++++++++++++++++++++++++++- 4 files changed, 264 insertions(+), 52 deletions(-) diff --git a/src/renderer.zig b/src/renderer.zig index 8d3e899a4..2f3018558 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -9,6 +9,7 @@ const builtin = @import("builtin"); +pub usingnamespace @import("renderer/cursor.zig"); pub usingnamespace @import("renderer/size.zig"); pub const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 45780ea8e..5c73a5fb4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -35,6 +35,11 @@ alloc: std.mem.Allocator, /// Current cell dimensions for this grid. cell_size: renderer.CellSize, +/// Whether the cursor is visible or not. This is used to control cursor +/// blinking. +cursor_visible: bool, +cursor_style: renderer.CursorStyle, + /// Default foreground color foreground: terminal.color.RGB, @@ -64,6 +69,7 @@ texture_greyscale: objc.Object, // MTLTexture const GPUCell = extern struct { mode: GPUCellMode, grid_pos: [2]f32, + cell_width: u8, color: [4]u8, glyph_pos: [2]u32 = .{ 0, 0 }, glyph_size: [2]u32 = .{ 0, 0 }, @@ -82,6 +88,12 @@ const GPUUniforms = extern struct { /// Size of a single cell in pixels, unscaled. cell_size: [2]f32, + + /// Metrics for underline/strikethrough + underline_position: f32, + underline_thickness: f32, + strikethrough_position: f32, + strikethrough_thickness: f32, }; const GPUCellMode = enum(u8) { @@ -93,6 +105,14 @@ const GPUCellMode = enum(u8) { cursor_bar = 5, underline = 6, strikethrough = 8, + + pub fn fromCursor(cursor: renderer.CursorStyle) GPUCellMode { + return switch (cursor) { + .box => .cursor_rect, + .box_hollow => .cursor_rect_hollow, + .bar => .cursor_bar, + }; + } }; /// Returns the hints that we want for this @@ -186,10 +206,20 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .cursor_visible = true, + .cursor_style = .box, // Render state .cells = .{}, - .uniforms = undefined, + .uniforms = .{ + .projection_matrix = undefined, + .px_scale = undefined, + .cell_size = undefined, + .underline_position = metrics.underline_position, + .underline_thickness = metrics.underline_thickness, + .strikethrough_position = metrics.strikethrough_position, + .strikethrough_thickness = metrics.strikethrough_thickness, + }, // Fonts .font_group = font_group, @@ -263,6 +293,15 @@ pub fn render( if (state.resize_screen) |size| try self.setScreenSize(size); defer state.resize_screen = null; + // Setup our cursor state + if (state.focused) { + self.cursor_visible = state.cursor.visible and !state.cursor.blink; + self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; + } else { + self.cursor_visible = true; + self.cursor_style = .box_hollow; + } + // Swap bg/fg if the terminal is reversed const bg = self.background; const fg = self.foreground; @@ -301,6 +340,7 @@ pub fn render( const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height); // Setup our uniforms + const old = self.uniforms; self.uniforms = .{ .projection_matrix = math.ortho2d( 0, @@ -310,6 +350,10 @@ pub fn render( ), .px_scale = .{ scaleX, scaleY }, .cell_size = .{ self.cell_size.width, self.cell_size.height }, + .underline_position = old.underline_position, + .underline_thickness = old.underline_thickness, + .strikethrough_position = old.strikethrough_position, + .strikethrough_thickness = old.strikethrough_thickness, }; } @@ -446,12 +490,35 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void { (term.screen.rows * term.screen.cols * 3) + 1, ); + // This is the cell that has [mode == .fg] and is underneath our cursor. + // We keep track of it so that we can invert the colors so the character + // remains visible. + var cursor_cell: ?GPUCell = null; + // Build each cell var rowIter = term.screen.rowIterator(.viewport); var y: usize = 0; while (rowIter.next()) |row| { defer y += 1; + // If this is the row with our cursor, then we may have to modify + // the cell with the cursor. + const start_i: usize = self.cells.items.len; + defer if (self.cursor_visible and + self.cursor_style == .box and + term.screen.viewportIsBottom() and + y == term.screen.cursor.y) + { + for (self.cells.items[start_i..]) |cell| { + if (cell.grid_pos[0] == @intToFloat(f32, term.screen.cursor.x) and + cell.mode == .fg) + { + cursor_cell = cell; + break; + } + } + }; + // Split our row into runs and shape each one. var iter = self.font_shaper.runIterator(self.font_group, row); while (try iter.next(self.alloc)) |run| { @@ -470,6 +537,15 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void { // Set row is not dirty anymore row.setDirty(false); } + + // Add the cursor at the end so that it overlays everything. If we have + // a cursor cell then we invert the colors on that and add it in so + // that we can always see it. + self.addCursor(term); + if (cursor_cell) |*cell| { + cell.color = .{ 0, 0, 0, 255 }; + self.cells.appendAssumeCapacity(cell.*); + } } pub fn updateCell( @@ -537,19 +613,8 @@ pub fn updateCell( self.cells.appendAssumeCapacity(.{ .mode = .bg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), .color = .{ rgb.r, rgb.g, rgb.b, alpha }, - - // .grid_col = @intCast(u16, x), - // .grid_row = @intCast(u16, y), - // .grid_width = cell.widthLegacy(), - // .fg_r = 0, - // .fg_g = 0, - // .fg_b = 0, - // .fg_a = 0, - // .bg_r = rgb.r, - // .bg_g = rgb.g, - // .bg_b = rgb.b, - // .bg_a = alpha, }); } @@ -568,27 +633,58 @@ pub fn updateCell( self.cells.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, .glyph_size = .{ glyph.width, glyph.height }, .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, // .mode = mode, - // .grid_width = cell.widthLegacy(), - // .fg_r = colors.fg.r, - // .fg_g = colors.fg.g, - // .fg_b = colors.fg.b, - // .fg_a = alpha, - // .bg_r = 0, - // .bg_g = 0, - // .bg_b = 0, - // .bg_a = 0, + }); + } + + if (cell.attrs.underline) { + self.cells.appendAssumeCapacity(.{ + .mode = .underline, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + }); + } + + if (cell.attrs.strikethrough) { + self.cells.appendAssumeCapacity(.{ + .mode = .strikethrough, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, }); } return true; } +fn addCursor(self: *Metal, term: *Terminal) void { + // Add the cursor + if (self.cursor_visible and term.screen.viewportIsBottom()) { + const cell = term.screen.getCell( + .active, + term.screen.cursor.y, + term.screen.cursor.x, + ); + + self.cells.appendAssumeCapacity(.{ + .mode = GPUCellMode.fromCursor(self.cursor_style), + .grid_pos = .{ + @intToFloat(f32, term.screen.cursor.x), + @intToFloat(f32, term.screen.cursor.y), + }, + .cell_width = if (cell.attrs.wide) 2 else 1, + .color = .{ 0xFF, 0xFF, 0xFF, 0xFF }, + }); + } +} + /// Sync the vertex buffer inputs to the GPU. This will attempt to reuse /// the existing buffer (of course!) but will allocate a new buffer if /// our cells don't fit in it. @@ -768,6 +864,17 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 6)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "cell_width"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } // The layout describes how and when we fetch the next vertex input. const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index eaf2d319a..323406431 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -66,7 +66,7 @@ font_shaper: font.Shaper, /// Whether the cursor is visible or not. This is used to control cursor /// blinking. cursor_visible: bool, -cursor_style: CursorStyle, +cursor_style: renderer.CursorStyle, /// Default foreground color foreground: terminal.color.RGB, @@ -74,25 +74,6 @@ foreground: terminal.color.RGB, /// Default background color background: terminal.color.RGB, -/// Available cursor styles for drawing. The values represents the mode value -/// in the shader. -pub const CursorStyle = enum(u8) { - box = 3, - box_hollow = 4, - bar = 5, - - /// Create a cursor style from the terminal style request. - pub fn fromTerminal(style: terminal.CursorStyle) ?CursorStyle { - return switch (style) { - .blinking_block, .steady_block => .box, - .blinking_bar, .steady_bar => .bar, - .blinking_underline, .steady_underline => null, // TODO - .default => .box, - else => null, - }; - } -}; - /// The raw structure that maps directly to the buffer sent to the vertex shader. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. @@ -145,6 +126,14 @@ const GPUCellMode = enum(u8) { // Non-exhaustive because masks change it _, + pub fn fromCursor(cursor: renderer.CursorStyle) GPUCellMode { + return switch (cursor) { + .box => .cursor_rect, + .box_hollow => .cursor_rect_hollow, + .bar => .cursor_bar, + }; + } + /// Apply a mask to the mode. pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode { return @intToEnum( @@ -468,7 +457,7 @@ pub fn render( // Setup our cursor state if (state.focused) { self.cursor_visible = state.cursor.visible and !state.cursor.blink; - self.cursor_style = CursorStyle.fromTerminal(state.cursor.style) orelse .box; + self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; } else { self.cursor_visible = true; self.cursor_style = .box_hollow; @@ -701,13 +690,8 @@ fn addCursor(self: *OpenGL, term: *Terminal) void { term.screen.cursor.x, ); - var mode: GPUCellMode = @intToEnum( - GPUCellMode, - @enumToInt(self.cursor_style), - ); - self.cells.appendAssumeCapacity(.{ - .mode = mode, + .mode = GPUCellMode.fromCursor(self.cursor_style), .grid_col = @intCast(u16, term.screen.cursor.x), .grid_row = @intCast(u16, term.screen.cursor.y), .grid_width = if (cell.attrs.wide) 2 else 1, diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index cddd427b5..1fc4ff9e1 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -4,12 +4,21 @@ using namespace metal; enum Mode : uint8_t { MODE_BG = 1u, MODE_FG = 2u, + MODE_CURSOR_RECT = 3u, + MODE_CURSOR_RECT_HOLLOW = 4u, + MODE_CURSOR_BAR = 5u, + MODE_UNDERLINE = 6u, + MODE_STRIKETHROUGH = 8u, }; struct Uniforms { float4x4 projection_matrix; float2 px_scale; float2 cell_size; + float underline_position; + float underline_thickness; + float strikethrough_position; + float strikethrough_thickness; }; struct VertexIn { @@ -19,6 +28,9 @@ struct VertexIn { // The grid coordinates (x, y) where x < columns and y < rows float2 grid_pos [[ attribute(1) ]]; + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[ attribute(6) ]]; + // The color. For BG modes, this is the bg color, for FG modes this is // the text color. For styles, this is the color of the style. uchar4 color [[ attribute(5) ]]; @@ -47,8 +59,8 @@ vertex VertexOut uber_vertex( VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - // TODO: scale with cell width float2 cell_size = uniforms.cell_size * uniforms.px_scale; + cell_size.x = cell_size.x * input.cell_width; // Convert the grid x,y into world space x, y by accounting for cell size float2 cell_pos = cell_size * input.grid_pos; @@ -80,7 +92,7 @@ vertex VertexOut uber_vertex( out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); break; - case MODE_FG: + case MODE_FG: { float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; @@ -103,6 +115,67 @@ vertex VertexOut uber_vertex( break; } + case MODE_CURSOR_RECT: + // Same as background since we're taking up the whole cell. + cell_pos = cell_pos + cell_size * position; + + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + break; + + case MODE_CURSOR_RECT_HOLLOW: + // Same as background since we're taking up the whole cell. + cell_pos = cell_pos + cell_size * position; + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + + // Top-left position of this cell is needed for the hollow rect. + out.tex_coord = cell_pos; + break; + + case MODE_CURSOR_BAR: { + // Make the bar a smaller version of our cell + float2 bar_size = float2(cell_size.x * 0.2, cell_size.y); + + // Same as background since we're taking up the whole cell. + cell_pos = cell_pos + bar_size * position; + + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + break; + } + + case MODE_UNDERLINE: { + // Underline Y value is just our thickness + float2 underline_size = float2(cell_size.x, uniforms.underline_thickness); + + // Position the underline where we are told to + float2 underline_offset = float2(cell_size.x, uniforms.underline_position * uniforms.px_scale.y); + + // Go to the bottom of the cell, take away the size of the + // underline, and that is our position. We also float it slightly + // above the bottom. + cell_pos = cell_pos + underline_offset - (underline_size * position); + + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + break; + } + + case MODE_STRIKETHROUGH: { + // Strikethrough Y value is just our thickness + float2 strikethrough_size = float2(cell_size.x, uniforms.strikethrough_thickness); + + // Position the strikethrough where we are told to + float2 strikethrough_offset = float2(cell_size.x, uniforms.strikethrough_position * uniforms.px_scale.y); + + // Go to the bottom of the cell, take away the size of the + // strikethrough, and that is our position. We also float it slightly + // above the bottom. + cell_pos = cell_pos + strikethrough_offset - (strikethrough_size * position); + + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + break; + } + + } + return out; } @@ -116,7 +189,7 @@ fragment float4 uber_fragment( case MODE_BG: return in.color; - case MODE_FG: + case MODE_FG: { // Normalize the texture coordinates to [0,1] float2 size = float2(textureGreyscale.get_width(), textureGreyscale.get_height()); float2 coord = in.tex_coord / size; @@ -124,4 +197,51 @@ fragment float4 uber_fragment( float a = textureGreyscale.sample(textureSampler, coord).r; return float4(in.color.rgb, in.color.a * a); } + + case MODE_CURSOR_RECT: + return in.color; + + case MODE_CURSOR_RECT_HOLLOW: + // Okay so yeah this is probably horrendously slow and a shader + // should never do this, but we only ever render a cursor for ONE + // rectangle so we take the slowdown for that one. + + // // We subtracted one from cell size because our coordinates start at 0. + // // So a width of 50 means max pixel of 49. + // vec2 cell_size_coords = cell_size - 1; + // + // // Apply padding + // vec2 padding = vec2(1.,1.); + // cell_size_coords = cell_size_coords - (padding * 2); + // vec2 screen_cell_pos_padded = screen_cell_pos + padding; + // + // // Convert our frag coord to offset of this cell. We have to subtract + // // 0.5 because the frag coord is in center pixels. + // vec2 cell_frag_coord = gl_FragCoord.xy - screen_cell_pos_padded - 0.5; + // + // // If the frag coords are in the bounds, then we color it. + // const float eps = 0.1; + // if (cell_frag_coord.x >= 0 && cell_frag_coord.y >= 0 && + // cell_frag_coord.x <= cell_size_coords.x && + // cell_frag_coord.y <= cell_size_coords.y) { + // if (abs(cell_frag_coord.x) < eps || + // abs(cell_frag_coord.x - cell_size_coords.x) < eps || + // abs(cell_frag_coord.y) < eps || + // abs(cell_frag_coord.y - cell_size_coords.y) < eps) { + // out_FragColor = color; + // } + // } + + // Default to no color. + return float4(0.0f); + + case MODE_CURSOR_BAR: + return in.color; + + case MODE_UNDERLINE: + return in.color; + + case MODE_STRIKETHROUGH: + return in.color; + } }