renderer/metal: render the cursor

This commit is contained in:
Mitchell Hashimoto
2024-04-27 22:01:03 -07:00
parent fe4fc509e9
commit c15f4d7258
2 changed files with 165 additions and 47 deletions

View File

@ -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{ return Metal{
.alloc = alloc, .alloc = alloc,
.config = options.config, .config = options.config,
@ -557,7 +560,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.current_background_color = options.config.background, .current_background_color = options.config.background,
// Render state // Render state
.cells = .{}, .cells = cells,
.uniforms = .{ .uniforms = .{
.projection_matrix = undefined, .projection_matrix = undefined,
.cell_size = 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}); // log.debug("drawing frame index={}", .{self.gpu_state.frame_index});
// Setup our frame data // 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.uniforms.sync(self.gpu_state.device, &.{self.uniforms});
try frame.cells_bg.sync(self.gpu_state.device, self.cells.bgs.items); try frame.cells_bg.sync(self.gpu_state.device, cells_bg);
try frame.cells.sync(self.gpu_state.device, self.cells.text.items); try frame.cells.sync(self.gpu_state.device, cells_fg);
// If we have custom shaders, update the animation time. // If we have custom shaders, update the animation time.
if (self.custom_shader_state) |*state| { 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]); try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
// Then draw background cells // 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 // Then draw images under text
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
// Then draw fg cells // 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 // Then draw remaining images
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
@ -1864,7 +1869,6 @@ fn rebuildCells2(
) !void { ) !void {
// TODO: cursor_cell // TODO: cursor_cell
// TODO: cursor_Row // TODO: cursor_Row
_ = cursor_style_;
// Create an arena for all our temporary allocations while rebuilding // Create an arena for all our temporary allocations while rebuilding
var arena = ArenaAllocator.init(self.alloc); 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 // Setup our cursor rendering information.
// a cursor cell then we invert the colors on that and add it in so cursor: {
// that we can always see it. // If we have no cursor style then we don't render the cursor.
// if (cursor_style_) |cursor_style| cursor_style: { const style = cursor_style_ orelse {
// // If we have a preedit, we try to render the preedit text on top self.cells.setCursor(null);
// // of the cursor. break :cursor;
// if (preedit) |preedit_v| { };
// const range = preedit_range.?;
// var x = range.x[0]; // Prepare the cursor cell contents.
// for (preedit_v.codepoints[range.cp_offset..]) |cp| { self.addCursor2(screen, style);
// self.addPreeditCell(cp, x, range.y) catch |err| { }
// log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
// x, // If we have a preedit, we try to render the preedit text on top
// range.y, // of the cursor.
// err, // 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; // x += if (cp.wide) 2 else 1;
// }
//
// // Preedit hides the cursor
// break :cursor_style;
// } // }
// //
// _ = self.addCursor(screen, cursor_style); // // Preedit hides the cursor
// // if (cursor_cell) |*cell| { // break :cursor_style;
// // if (cell.mode == .fg) { // }
// // cell.color = if (self.config.cursor_text) |txt|
// // .{ txt.r, txt.g, txt.b, 255 } // if (cursor_cell) |*cell| {
// // else // if (cell.mode == .fg) {
// // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; // cell.color = if (self.config.cursor_text) |txt|
// // } // .{ txt.r, txt.g, txt.b, 255 }
// // // else
// // self.cells_text.appendAssumeCapacity(cell.*); // .{ 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; 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( fn addCursor(
self: *Metal, self: *Metal,
screen: *terminal.Screen, screen: *terminal.Screen,

View File

@ -35,19 +35,47 @@ pub const Contents = struct {
/// ///
/// Before any operation, this must be initialized by calling resize /// Before any operation, this must be initialized by calling resize
/// on the contents. /// on the contents.
map: []Map = undefined, map: []Map,
/// The grid size of the terminal. This is used to determine the /// The grid size of the terminal. This is used to determine the
/// map array index from a coordinate. /// 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. /// 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 /// This only contains the cells that have content set. To determine
/// if a cell has content set, we check the map. /// if a cell has content set, we check the map.
/// ///
/// This data is synced to a buffer on every frame. /// This data is synced to a buffer on every frame.
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, 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 { pub fn deinit(self: *Contents, alloc: Allocator) void {
alloc.free(self.map); alloc.free(self.map);
@ -70,7 +98,30 @@ pub const Contents = struct {
self.map = map; self.map = map;
self.cols = size.columns; self.cols = size.columns;
self.bgs.clearAndFree(alloc); 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. /// Get the cell contents for the given type and coordinate.
@ -244,7 +295,7 @@ test Contents {
const rows = 10; const rows = 10;
const cols = 10; const cols = 10;
var c: Contents = .{}; var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols }); try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc); defer c.deinit(alloc);
@ -287,7 +338,7 @@ test "Contents clear retains other content" {
const rows = 10; const rows = 10;
const cols = 10; const cols = 10;
var c: Contents = .{}; var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols }); try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc); defer c.deinit(alloc);
@ -319,7 +370,7 @@ test "Contents clear last added content" {
const rows = 10; const rows = 10;
const cols = 10; const cols = 10;
var c: Contents = .{}; var c = try Contents.init(alloc);
try c.resize(alloc, .{ .rows = rows, .columns = cols }); try c.resize(alloc, .{ .rows = rows, .columns = cols });
defer c.deinit(alloc); defer c.deinit(alloc);