mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
metal: cursor and underline
This commit is contained in:
@ -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");
|
||||
|
@ -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"));
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user