mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
renderer(opengl): implement blinking cells, make render modes a bitfield
The render modes in the vertex shader has always been... strange. It kind of functioned like an enum and a bitfield at the same time. To comfortably accommodate adding blink information on cells, I've refactored it to make it into a true bitfield -- this made the logic on the Zig side much simpler, with the tradeoff being that logic is slightly harder to read in the shader code as GLSL does not support implicitly casting integers after bitmasking to booleans. The blink is currently synchronized to the cursor blinking (which is what most other terminal emulators do), though we should be able to have a separate cell blink timer in the future should the need appear.
This commit is contained in:
@ -108,6 +108,9 @@ cursor_color: ?terminal.color.RGB,
|
||||
/// foreground color as the cursor color.
|
||||
cursor_invert: bool,
|
||||
|
||||
/// Whether blinking cells are currently visible. Synchronized with cursor blinking.
|
||||
blink_visible: bool = true,
|
||||
|
||||
/// Padding options
|
||||
padding: renderer.Options.Padding,
|
||||
|
||||
@ -701,7 +704,7 @@ pub fn updateFrame(
|
||||
self: *OpenGL,
|
||||
surface: *apprt.Surface,
|
||||
state: *renderer.State,
|
||||
cursor_blink_visible: bool,
|
||||
blink_visible: bool,
|
||||
) !void {
|
||||
_ = surface;
|
||||
|
||||
@ -770,7 +773,7 @@ pub fn updateFrame(
|
||||
const cursor_style = renderer.cursorStyle(
|
||||
state,
|
||||
self.focused,
|
||||
cursor_blink_visible,
|
||||
blink_visible,
|
||||
);
|
||||
|
||||
// Get our preedit state
|
||||
@ -854,6 +857,9 @@ pub fn updateFrame(
|
||||
.color_palette = state.terminal.color_palette.colors,
|
||||
};
|
||||
};
|
||||
|
||||
self.blink_visible = blink_visible;
|
||||
|
||||
defer {
|
||||
critical.screen.deinit();
|
||||
if (critical.preedit) |p| p.deinit(self.alloc);
|
||||
@ -1279,7 +1285,7 @@ pub fn rebuildCells(
|
||||
const screen_cell = row.cells(.all)[screen.cursor.x];
|
||||
const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail);
|
||||
for (self.cells.items[start_i..]) |cell| {
|
||||
if (cell.grid_col == x and cell.mode.isFg()) {
|
||||
if (cell.grid_col == x and cell.mode.fg) {
|
||||
cursor_cell = cell;
|
||||
break;
|
||||
}
|
||||
@ -1416,7 +1422,7 @@ pub fn rebuildCells(
|
||||
|
||||
_ = try self.addCursor(screen, cursor_style, cursor_color);
|
||||
if (cursor_cell) |*cell| {
|
||||
if (cell.mode.isFg() and cell.mode != .fg_color) {
|
||||
if (cell.mode.fg and !cell.mode.fg_color) {
|
||||
const cell_color = if (self.cursor_invert) blk: {
|
||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
||||
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color;
|
||||
@ -1436,8 +1442,8 @@ pub fn rebuildCells(
|
||||
|
||||
// Some debug mode safety checks
|
||||
if (std.debug.runtime_safety) {
|
||||
for (self.cells_bg.items) |cell| assert(cell.mode == .bg);
|
||||
for (self.cells.items) |cell| assert(cell.mode != .bg);
|
||||
for (self.cells_bg.items) |cell| assert(!cell.mode.fg);
|
||||
for (self.cells.items) |cell| assert(cell.mode.fg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1469,7 +1475,7 @@ fn addPreeditCell(
|
||||
|
||||
// Add our opaque background cell
|
||||
try self.cells_bg.append(self.alloc, .{
|
||||
.mode = .bg,
|
||||
.mode = .{ .fg = false },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = if (cp.wide) 2 else 1,
|
||||
@ -1491,7 +1497,7 @@ fn addPreeditCell(
|
||||
|
||||
// Add our text
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = .fg,
|
||||
.mode = .{ .fg = true },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = if (cp.wide) 2 else 1,
|
||||
@ -1558,7 +1564,7 @@ fn addCursor(
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = .fg,
|
||||
.mode = .{ .fg = true },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(screen.cursor.y),
|
||||
.grid_width = if (wide) 2 else 1,
|
||||
@ -1702,7 +1708,7 @@ fn updateCell(
|
||||
};
|
||||
|
||||
try self.cells_bg.append(self.alloc, .{
|
||||
.mode = .bg,
|
||||
.mode = .{ .fg = false },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
@ -1743,16 +1749,20 @@ fn updateCell(
|
||||
},
|
||||
);
|
||||
|
||||
var mode: CellProgram.CellMode = .{ .fg = true };
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
.normal => {},
|
||||
.color => mode.fg_color = true,
|
||||
.constrained => mode.fg_constrained = true,
|
||||
.powerline => mode.fg_powerline = true,
|
||||
}
|
||||
|
||||
mode.fg_blink = style.flags.blink;
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
@ -1799,7 +1809,7 @@ fn updateCell(
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = .fg,
|
||||
.mode = .{ .fg = true },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
@ -1832,7 +1842,7 @@ fn updateCell(
|
||||
);
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = .fg,
|
||||
.mode = .{ .fg = true },
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
@ -2156,6 +2166,10 @@ fn drawCellProgram(
|
||||
"padding_vertical_bottom",
|
||||
self.padding_extend_bottom,
|
||||
);
|
||||
try program.program.setUniform(
|
||||
"blink_visible",
|
||||
self.blink_visible,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw background images first
|
||||
|
@ -48,31 +48,14 @@ pub const Cell = extern struct {
|
||||
grid_width: u8,
|
||||
};
|
||||
|
||||
pub const CellMode = enum(u8) {
|
||||
bg = 1,
|
||||
fg = 2,
|
||||
fg_constrained = 3,
|
||||
fg_color = 7,
|
||||
fg_powerline = 15,
|
||||
pub const CellMode = packed struct(u8) {
|
||||
fg: bool,
|
||||
fg_constrained: bool = false,
|
||||
fg_color: bool = false,
|
||||
fg_powerline: bool = false,
|
||||
fg_blink: bool = false,
|
||||
|
||||
// Non-exhaustive because masks change it
|
||||
_,
|
||||
|
||||
/// Apply a mask to the mode.
|
||||
pub fn mask(self: CellMode, m: CellMode) CellMode {
|
||||
return @enumFromInt(@intFromEnum(self) | @intFromEnum(m));
|
||||
}
|
||||
|
||||
pub fn isFg(self: CellMode) bool {
|
||||
// Since we use bit tricks below, we want to ensure the enum
|
||||
// doesn't change without us looking at this logic again.
|
||||
comptime {
|
||||
const info = @typeInfo(CellMode).Enum;
|
||||
std.debug.assert(info.fields.len == 5);
|
||||
}
|
||||
|
||||
return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0;
|
||||
}
|
||||
_padding: u3 = 0,
|
||||
};
|
||||
|
||||
pub fn init() !CellProgram {
|
||||
|
@ -22,32 +22,29 @@ uniform sampler2D text_color;
|
||||
|
||||
// Dimensions of the cell
|
||||
uniform vec2 cell_size;
|
||||
uniform bool blink_visible;
|
||||
|
||||
// See vertex shader
|
||||
const uint MODE_BG = 1u;
|
||||
const uint MODE_FG = 2u;
|
||||
const uint MODE_FG_CONSTRAINED = 3u;
|
||||
const uint MODE_FG_COLOR = 7u;
|
||||
const uint MODE_FG_POWERLINE = 15u;
|
||||
const uint MODE_FG = 1u;
|
||||
const uint MODE_FG_CONSTRAINED = 2u;
|
||||
const uint MODE_FG_COLOR = 4u;
|
||||
const uint MODE_FG_POWERLINE = 8u;
|
||||
const uint MODE_FG_BLINK = 16u;
|
||||
|
||||
void main() {
|
||||
float a;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_BG:
|
||||
out_FragColor = color;
|
||||
break;
|
||||
|
||||
case MODE_FG:
|
||||
case MODE_FG_CONSTRAINED:
|
||||
case MODE_FG_POWERLINE:
|
||||
a = texture(text, glyph_tex_coords).r;
|
||||
vec3 premult = color.rgb * color.a;
|
||||
out_FragColor = vec4(premult.rgb*a, a);
|
||||
break;
|
||||
|
||||
case MODE_FG_COLOR:
|
||||
out_FragColor = texture(text_color, glyph_tex_coords);
|
||||
break;
|
||||
if ((mode & MODE_FG) == 0u) {
|
||||
// Background
|
||||
out_FragColor = color;
|
||||
}
|
||||
if ((mode & MODE_FG_BLINK) != 0u && !blink_visible) {
|
||||
discard;
|
||||
}
|
||||
if ((mode & MODE_FG_COLOR) != 0u) {
|
||||
out_FragColor = texture(text_color, glyph_tex_coords);
|
||||
return;
|
||||
}
|
||||
|
||||
float a = texture(text, glyph_tex_coords).r;
|
||||
vec3 premult = color.rgb * color.a;
|
||||
out_FragColor = vec4(premult.rgb*a, a);
|
||||
}
|
||||
|
@ -4,11 +4,11 @@
|
||||
// used to multiplex multiple render modes into a single shader.
|
||||
//
|
||||
// NOTE: this must be kept in sync with the fragment shader
|
||||
const uint MODE_BG = 1u;
|
||||
const uint MODE_FG = 2u;
|
||||
const uint MODE_FG_CONSTRAINED = 3u;
|
||||
const uint MODE_FG_COLOR = 7u;
|
||||
const uint MODE_FG_POWERLINE = 15u;
|
||||
const uint MODE_FG = 1u;
|
||||
const uint MODE_FG_CONSTRAINED = 2u;
|
||||
const uint MODE_FG_COLOR = 4u;
|
||||
const uint MODE_FG_POWERLINE = 8u;
|
||||
const uint MODE_FG_BLINK = 16u;
|
||||
|
||||
// The grid coordinates (x, y) where x < columns and y < rows
|
||||
layout (location = 0) in vec2 grid_coord;
|
||||
@ -170,8 +170,8 @@ void main() {
|
||||
vec2 cell_size_scaled = cell_size;
|
||||
cell_size_scaled.x = cell_size_scaled.x * grid_width;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_BG:
|
||||
if ((mode & MODE_FG) == 0u) {
|
||||
// Draw background
|
||||
// If we're at the edge of the grid, we add our padding to the background
|
||||
// to extend it. Note: grid_padding is top/right/bottom/left.
|
||||
if (grid_coord.y == 0 && padding_vertical_top) {
|
||||
@ -194,12 +194,7 @@ void main() {
|
||||
|
||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||
color = color_in / 255.0;
|
||||
break;
|
||||
|
||||
case MODE_FG:
|
||||
case MODE_FG_CONSTRAINED:
|
||||
case MODE_FG_COLOR:
|
||||
case MODE_FG_POWERLINE:
|
||||
} else {
|
||||
vec2 glyph_offset_calc = glyph_offset;
|
||||
|
||||
// The glyph_offset.y is the y bearing, a y value that when added
|
||||
@ -211,7 +206,7 @@ void main() {
|
||||
// We also always constrain colored glyphs since we should have
|
||||
// their scaled cell size exactly correct.
|
||||
vec2 glyph_size_calc = glyph_size;
|
||||
if (mode == MODE_FG_CONSTRAINED || mode == MODE_FG_COLOR) {
|
||||
if ((mode & (MODE_FG_CONSTRAINED | MODE_FG_COLOR)) != 0u) {
|
||||
if (glyph_size.x > cell_size_scaled.x) {
|
||||
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
|
||||
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
|
||||
@ -227,16 +222,10 @@ void main() {
|
||||
// We need to convert our texture position and size to normalized
|
||||
// device coordinates (0 to 1.0) by dividing by the size of the texture.
|
||||
ivec2 text_size;
|
||||
switch(mode) {
|
||||
case MODE_FG_CONSTRAINED:
|
||||
case MODE_FG_POWERLINE:
|
||||
case MODE_FG:
|
||||
text_size = textureSize(text, 0);
|
||||
break;
|
||||
|
||||
case MODE_FG_COLOR:
|
||||
text_size = textureSize(text_color, 0);
|
||||
break;
|
||||
if ((mode & MODE_FG_COLOR) != 0u) {
|
||||
text_size = textureSize(text_color, 0);
|
||||
} else {
|
||||
text_size = textureSize(text, 0);
|
||||
}
|
||||
vec2 glyph_tex_pos = glyph_pos / text_size;
|
||||
vec2 glyph_tex_size = glyph_size / text_size;
|
||||
@ -250,11 +239,10 @@ void main() {
|
||||
// and Powerline glyphs to be unaffected (else parts of the line would
|
||||
// have different colors as some parts are displayed via background colors).
|
||||
vec4 color_final = color_in / 255.0;
|
||||
if (min_contrast > 1.0 && mode == MODE_FG) {
|
||||
if (min_contrast > 1.0 && (mode & ~(MODE_FG | MODE_FG_BLINK)) != 0u) {
|
||||
vec4 bg_color = bg_color_in / 255.0;
|
||||
color_final = contrasted_color(min_contrast, color_final, bg_color);
|
||||
}
|
||||
color = color_final;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user