metal: cursor and underline

This commit is contained in:
Mitchell Hashimoto
2022-10-30 19:47:15 -07:00
parent 4b5174d2c6
commit ee45d363a9
4 changed files with 264 additions and 52 deletions

View File

@ -9,6 +9,7 @@
const builtin = @import("builtin"); const builtin = @import("builtin");
pub usingnamespace @import("renderer/cursor.zig");
pub usingnamespace @import("renderer/size.zig"); pub usingnamespace @import("renderer/size.zig");
pub const Metal = @import("renderer/Metal.zig"); pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig"); pub const OpenGL = @import("renderer/OpenGL.zig");

View File

@ -35,6 +35,11 @@ alloc: std.mem.Allocator,
/// Current cell dimensions for this grid. /// Current cell dimensions for this grid.
cell_size: renderer.CellSize, 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 /// Default foreground color
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
@ -64,6 +69,7 @@ texture_greyscale: objc.Object, // MTLTexture
const GPUCell = extern struct { const GPUCell = extern struct {
mode: GPUCellMode, mode: GPUCellMode,
grid_pos: [2]f32, grid_pos: [2]f32,
cell_width: u8,
color: [4]u8, color: [4]u8,
glyph_pos: [2]u32 = .{ 0, 0 }, glyph_pos: [2]u32 = .{ 0, 0 },
glyph_size: [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. /// Size of a single cell in pixels, unscaled.
cell_size: [2]f32, cell_size: [2]f32,
/// Metrics for underline/strikethrough
underline_position: f32,
underline_thickness: f32,
strikethrough_position: f32,
strikethrough_thickness: f32,
}; };
const GPUCellMode = enum(u8) { const GPUCellMode = enum(u8) {
@ -93,6 +105,14 @@ const GPUCellMode = enum(u8) {
cursor_bar = 5, cursor_bar = 5,
underline = 6, underline = 6,
strikethrough = 8, 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 /// 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 }, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
.background = .{ .r = 0, .g = 0, .b = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 },
.foreground = .{ .r = 255, .g = 255, .b = 255 }, .foreground = .{ .r = 255, .g = 255, .b = 255 },
.cursor_visible = true,
.cursor_style = .box,
// Render state // Render state
.cells = .{}, .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 // Fonts
.font_group = font_group, .font_group = font_group,
@ -263,6 +293,15 @@ pub fn render(
if (state.resize_screen) |size| try self.setScreenSize(size); if (state.resize_screen) |size| try self.setScreenSize(size);
defer state.resize_screen = null; 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 // Swap bg/fg if the terminal is reversed
const bg = self.background; const bg = self.background;
const fg = self.foreground; const fg = self.foreground;
@ -301,6 +340,7 @@ pub fn render(
const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height); const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height);
// Setup our uniforms // Setup our uniforms
const old = self.uniforms;
self.uniforms = .{ self.uniforms = .{
.projection_matrix = math.ortho2d( .projection_matrix = math.ortho2d(
0, 0,
@ -310,6 +350,10 @@ pub fn render(
), ),
.px_scale = .{ scaleX, scaleY }, .px_scale = .{ scaleX, scaleY },
.cell_size = .{ self.cell_size.width, self.cell_size.height }, .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, (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 // Build each cell
var rowIter = term.screen.rowIterator(.viewport); var rowIter = term.screen.rowIterator(.viewport);
var y: usize = 0; var y: usize = 0;
while (rowIter.next()) |row| { while (rowIter.next()) |row| {
defer y += 1; 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. // Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(self.font_group, row); var iter = self.font_shaper.runIterator(self.font_group, row);
while (try iter.next(self.alloc)) |run| { while (try iter.next(self.alloc)) |run| {
@ -470,6 +537,15 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void {
// Set row is not dirty anymore // Set row is not dirty anymore
row.setDirty(false); 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( pub fn updateCell(
@ -537,19 +613,8 @@ pub fn updateCell(
self.cells.appendAssumeCapacity(.{ self.cells.appendAssumeCapacity(.{
.mode = .bg, .mode = .bg,
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
.cell_width = cell.widthLegacy(),
.color = .{ rgb.r, rgb.g, rgb.b, alpha }, .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(.{ self.cells.appendAssumeCapacity(.{
.mode = .fg, .mode = .fg,
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
.cell_width = cell.widthLegacy(),
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
.glyph_size = .{ glyph.width, glyph.height }, .glyph_size = .{ glyph.width, glyph.height },
.glyph_offset = .{ glyph.offset_x, glyph.offset_y }, .glyph_offset = .{ glyph.offset_x, glyph.offset_y },
// .mode = mode, // .mode = mode,
// .grid_width = cell.widthLegacy(), });
// .fg_r = colors.fg.r, }
// .fg_g = colors.fg.g,
// .fg_b = colors.fg.b, if (cell.attrs.underline) {
// .fg_a = alpha, self.cells.appendAssumeCapacity(.{
// .bg_r = 0, .mode = .underline,
// .bg_g = 0, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
// .bg_b = 0, .cell_width = cell.widthLegacy(),
// .bg_a = 0, .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; 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 /// 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 /// the existing buffer (of course!) but will allocate a new buffer if
/// our cells don't fit in it. /// 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("offset", @as(c_ulong, @offsetOf(GPUCell, "color")));
attr.setProperty("bufferIndex", @as(c_ulong, 0)); 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. // The layout describes how and when we fetch the next vertex input.
const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));

View File

@ -66,7 +66,7 @@ font_shaper: font.Shaper,
/// Whether the cursor is visible or not. This is used to control cursor /// Whether the cursor is visible or not. This is used to control cursor
/// blinking. /// blinking.
cursor_visible: bool, cursor_visible: bool,
cursor_style: CursorStyle, cursor_style: renderer.CursorStyle,
/// Default foreground color /// Default foreground color
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
@ -74,25 +74,6 @@ foreground: terminal.color.RGB,
/// Default background color /// Default background color
background: terminal.color.RGB, 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. /// 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 /// This must be "extern" so that the field order is not reordered by the
/// Zig compiler. /// Zig compiler.
@ -145,6 +126,14 @@ const GPUCellMode = enum(u8) {
// Non-exhaustive because masks change it // 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. /// Apply a mask to the mode.
pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode { pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode {
return @intToEnum( return @intToEnum(
@ -468,7 +457,7 @@ pub fn render(
// Setup our cursor state // Setup our cursor state
if (state.focused) { if (state.focused) {
self.cursor_visible = state.cursor.visible and !state.cursor.blink; 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 { } else {
self.cursor_visible = true; self.cursor_visible = true;
self.cursor_style = .box_hollow; self.cursor_style = .box_hollow;
@ -701,13 +690,8 @@ fn addCursor(self: *OpenGL, term: *Terminal) void {
term.screen.cursor.x, term.screen.cursor.x,
); );
var mode: GPUCellMode = @intToEnum(
GPUCellMode,
@enumToInt(self.cursor_style),
);
self.cells.appendAssumeCapacity(.{ self.cells.appendAssumeCapacity(.{
.mode = mode, .mode = GPUCellMode.fromCursor(self.cursor_style),
.grid_col = @intCast(u16, term.screen.cursor.x), .grid_col = @intCast(u16, term.screen.cursor.x),
.grid_row = @intCast(u16, term.screen.cursor.y), .grid_row = @intCast(u16, term.screen.cursor.y),
.grid_width = if (cell.attrs.wide) 2 else 1, .grid_width = if (cell.attrs.wide) 2 else 1,

View File

@ -4,12 +4,21 @@ using namespace metal;
enum Mode : uint8_t { enum Mode : uint8_t {
MODE_BG = 1u, MODE_BG = 1u,
MODE_FG = 2u, MODE_FG = 2u,
MODE_CURSOR_RECT = 3u,
MODE_CURSOR_RECT_HOLLOW = 4u,
MODE_CURSOR_BAR = 5u,
MODE_UNDERLINE = 6u,
MODE_STRIKETHROUGH = 8u,
}; };
struct Uniforms { struct Uniforms {
float4x4 projection_matrix; float4x4 projection_matrix;
float2 px_scale; float2 px_scale;
float2 cell_size; float2 cell_size;
float underline_position;
float underline_thickness;
float strikethrough_position;
float strikethrough_thickness;
}; };
struct VertexIn { struct VertexIn {
@ -19,6 +28,9 @@ struct VertexIn {
// The grid coordinates (x, y) where x < columns and y < rows // The grid coordinates (x, y) where x < columns and y < rows
float2 grid_pos [[ attribute(1) ]]; 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 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. // the text color. For styles, this is the color of the style.
uchar4 color [[ attribute(5) ]]; uchar4 color [[ attribute(5) ]];
@ -47,8 +59,8 @@ vertex VertexOut uber_vertex(
VertexIn input [[ stage_in ]], VertexIn input [[ stage_in ]],
constant Uniforms &uniforms [[ buffer(1) ]] constant Uniforms &uniforms [[ buffer(1) ]]
) { ) {
// TODO: scale with cell width
float2 cell_size = uniforms.cell_size * uniforms.px_scale; 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 // Convert the grid x,y into world space x, y by accounting for cell size
float2 cell_pos = cell_size * input.grid_pos; 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); out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
break; break;
case MODE_FG: case MODE_FG: {
float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale;
float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale;
@ -103,6 +115,67 @@ vertex VertexOut uber_vertex(
break; 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; return out;
} }
@ -116,7 +189,7 @@ fragment float4 uber_fragment(
case MODE_BG: case MODE_BG:
return in.color; return in.color;
case MODE_FG: case MODE_FG: {
// Normalize the texture coordinates to [0,1] // Normalize the texture coordinates to [0,1]
float2 size = float2(textureGreyscale.get_width(), textureGreyscale.get_height()); float2 size = float2(textureGreyscale.get_width(), textureGreyscale.get_height());
float2 coord = in.tex_coord / size; float2 coord = in.tex_coord / size;
@ -124,4 +197,51 @@ fragment float4 uber_fragment(
float a = textureGreyscale.sample(textureSampler, coord).r; float a = textureGreyscale.sample(textureSampler, coord).r;
return float4(in.color.rgb, in.color.a * a); 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;
}
} }