diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 58e75afa0..30dfc6d52 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -543,6 +543,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; }; + const cells = try mtl_cell.Contents.init(alloc); + errdefer cells.deinit(alloc); + return Metal{ .alloc = alloc, .config = options.config, @@ -557,7 +560,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .current_background_color = options.config.background, // Render state - .cells = .{}, + .cells = cells, .uniforms = .{ .projection_matrix = undefined, .cell_size = undefined, @@ -829,9 +832,11 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); // Setup our frame data + const cells_bg = self.cells.bgCells(); + const cells_fg = self.cells.fgCells(); try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - try frame.cells_bg.sync(self.gpu_state.device, self.cells.bgs.items); - try frame.cells.sync(self.gpu_state.device, self.cells.text.items); + try frame.cells_bg.sync(self.gpu_state.device, cells_bg); + try frame.cells.sync(self.gpu_state.device, cells_fg); // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { @@ -930,13 +935,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCellBgs(encoder, frame, self.cells.bgs.items.len); + try self.drawCellBgs(encoder, frame, cells_bg.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCellFgs(encoder, frame, self.cells.text.items.len); + try self.drawCellFgs(encoder, frame, cells_fg.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1864,7 +1869,6 @@ fn rebuildCells2( ) !void { // TODO: cursor_cell // TODO: cursor_Row - _ = cursor_style_; // Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); @@ -1978,42 +1982,48 @@ fn rebuildCells2( } } - // 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. - // if (cursor_style_) |cursor_style| cursor_style: { - // // If we have a preedit, we try to render the preedit text on top - // // of the cursor. - // if (preedit) |preedit_v| { - // const range = preedit_range.?; - // var x = range.x[0]; - // for (preedit_v.codepoints[range.cp_offset..]) |cp| { - // self.addPreeditCell(cp, x, range.y) catch |err| { - // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - // x, - // range.y, - // err, - // }); - // }; + // Setup our cursor rendering information. + cursor: { + // If we have no cursor style then we don't render the cursor. + const style = cursor_style_ orelse { + self.cells.setCursor(null); + break :cursor; + }; + + // Prepare the cursor cell contents. + self.addCursor2(screen, style); + } + + // If we have a preedit, we try to render the preedit text on top + // of the cursor. + // if (preedit) |preedit_v| { + // const range = preedit_range.?; + // var x = range.x[0]; + // for (preedit_v.codepoints[range.cp_offset..]) |cp| { + // self.addPreeditCell(cp, x, range.y) catch |err| { + // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + // x, + // range.y, + // err, + // }); + // }; // - // x += if (cp.wide) 2 else 1; - // } - // - // // Preedit hides the cursor - // break :cursor_style; + // x += if (cp.wide) 2 else 1; // } // - // _ = self.addCursor(screen, cursor_style); - // // if (cursor_cell) |*cell| { - // // if (cell.mode == .fg) { - // // cell.color = if (self.config.cursor_text) |txt| - // // .{ txt.r, txt.g, txt.b, 255 } - // // else - // // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; - // // } - // // - // // self.cells_text.appendAssumeCapacity(cell.*); - // // } + // // Preedit hides the cursor + // break :cursor_style; + // } + + // if (cursor_cell) |*cell| { + // if (cell.mode == .fg) { + // cell.color = if (self.config.cursor_text) |txt| + // .{ txt.r, txt.g, txt.b, 255 } + // else + // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; + // } + // + // self.cells_text.appendAssumeCapacity(cell.*); // } } @@ -2456,6 +2466,63 @@ fn updateCell( return true; } +fn addCursor2( + self: *Metal, + screen: *terminal.Screen, + cursor_style: renderer.CursorStyle, +) void { + // Add the cursor. We render the cursor over the wide character if + // we're on the wide characer tail. + const wide, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.cursor.page_cell; + if (cell.wide != .spacer_tail or screen.cursor.x == 0) + break :cell .{ cell.wide == .wide, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + const prev_cell = screen.cursorCellLeft(1); + break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; + }; + + const color = self.cursor_color orelse self.foreground_color; + const alpha: u8 = if (!self.focused) 255 else alpha: { + const alpha = 255 * self.config.cursor_opacity; + break :alpha @intFromFloat(@ceil(alpha)); + }; + + const sprite: font.Sprite = switch (cursor_style) { + .block => .cursor_rect, + .block_hollow => .cursor_hollow_rect, + .bar => .cursor_bar, + .underline => .underline, + }; + + const render = self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + }; + + self.cells.setCursor(.{ + .mode = .fg, + .grid_pos = .{ x, screen.cursor.y }, + .cell_width = if (wide) 2 else 1, + .color = .{ color.r, color.g, color.b, alpha }, + .bg_color = .{ 0, 0, 0, 0 }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + }); +} + fn addCursor( self: *Metal, screen: *terminal.Screen, diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 29f101534..66ffd7497 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -35,19 +35,47 @@ pub const Contents = struct { /// /// Before any operation, this must be initialized by calling resize /// on the contents. - map: []Map = undefined, + map: []Map, /// The grid size of the terminal. This is used to determine the /// map array index from a coordinate. - cols: usize = 0, + cols: usize, /// The actual GPU data (on the CPU) for all the cells in the terminal. /// This only contains the cells that have content set. To determine /// if a cell has content set, we check the map. /// /// This data is synced to a buffer on every frame. - bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, - text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, + bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg), + text: std.ArrayListUnmanaged(mtl_shaders.CellText), + + /// True when the cursor should be rendered. + cursor: bool, + + /// The amount of text elements we reserve at the beginning for + /// special elements like the cursor. + const text_reserved_len = 1; + + pub fn init(alloc: Allocator) !Contents { + const map = try alloc.alloc(Map, 0); + errdefer alloc.free(map); + + var result: Contents = .{ + .map = map, + .cols = 0, + .bgs = .{}, + .text = .{}, + .cursor = false, + }; + + // We preallocate some amount of space for cell contents + // we always have as a prefix. For now the current prefix + // is length 1: the cursor. + try result.text.ensureTotalCapacity(alloc, text_reserved_len); + result.text.items.len = text_reserved_len; + + return result; + } pub fn deinit(self: *Contents, alloc: Allocator) void { alloc.free(self.map); @@ -70,7 +98,30 @@ pub const Contents = struct { self.map = map; self.cols = size.columns; self.bgs.clearAndFree(alloc); - self.text.clearAndFree(alloc); + self.text.shrinkAndFree(alloc, text_reserved_len); + } + + /// Returns the slice of fg cell contents to sync with the GPU. + pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText { + const start: usize = if (self.cursor) 0 else 1; + return self.text.items[start..]; + } + + /// Returns the slice of bg cell contents to sync with the GPU. + pub fn bgCells(self: *const Contents) []const mtl_shaders.CellBg { + return self.bgs.items; + } + + /// Set the cursor value. If the value is null then the cursor + /// is hidden. + pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void { + const cell = v orelse { + self.cursor = false; + return; + }; + + self.cursor = true; + self.text.items[0] = cell; } /// Get the cell contents for the given type and coordinate. @@ -244,7 +295,7 @@ test Contents { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc); @@ -287,7 +338,7 @@ test "Contents clear retains other content" { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc); @@ -319,7 +370,7 @@ test "Contents clear last added content" { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc);