From 1d04c52bb2bb67207ad87ae6c477177ed8fb33ab Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 31 Aug 2024 01:05:21 +0200 Subject: [PATCH 01/10] 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. --- src/renderer/OpenGL.zig | 50 ++++++++++++++++++----------- src/renderer/opengl/CellProgram.zig | 31 ++++-------------- src/renderer/shaders/cell.f.glsl | 43 ++++++++++++------------- src/renderer/shaders/cell.v.glsl | 40 ++++++++--------------- 4 files changed, 73 insertions(+), 91 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d220cdadc..94216c3aa 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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 diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig index 48386362e..b68ee4feb 100644 --- a/src/renderer/opengl/CellProgram.zig +++ b/src/renderer/opengl/CellProgram.zig @@ -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 { diff --git a/src/renderer/shaders/cell.f.glsl b/src/renderer/shaders/cell.f.glsl index f9c1ce2b1..9e0969daf 100644 --- a/src/renderer/shaders/cell.f.glsl +++ b/src/renderer/shaders/cell.f.glsl @@ -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); } diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl index 942b7ac44..12d2d4f4e 100644 --- a/src/renderer/shaders/cell.v.glsl +++ b/src/renderer/shaders/cell.v.glsl @@ -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; } } From 49415ef1d283aec9df5019148ba63a596473a751 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 31 Aug 2024 01:05:59 +0200 Subject: [PATCH 02/10] renderer(metal): implement blinking (untested!) See a9561a46 (this commit's OpenGL equivalent) for more information. Untested for now since I don't have a macOS setup. --- src/renderer/Metal.zig | 33 +++++++++++++++----- src/renderer/metal/shaders.zig | 18 +++++++---- src/renderer/shaders/cell.metal | 53 ++++++++++++++++----------------- 3 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1629678f6..1b05316d9 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -97,6 +97,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, + /// The current frame background color. This is only updated during /// the updateFrame method. current_background_color: terminal.color.RGB, @@ -867,7 +870,7 @@ pub fn updateFrame( self: *Metal, surface: *apprt.Surface, state: *renderer.State, - cursor_blink_visible: bool, + blink_visible: bool, ) !void { _ = surface; @@ -950,7 +953,7 @@ pub fn updateFrame( const cursor_style = renderer.cursorStyle( state, self.focused, - cursor_blink_visible, + blink_visible, ); // Get our preedit state @@ -1027,6 +1030,9 @@ pub fn updateFrame( .full_rebuild = full_rebuild, }; }; + + self.blink_visible = blink_visible; + defer { critical.screen.deinit(); if (critical.preedit) |p| p.deinit(self.alloc); @@ -1101,6 +1107,8 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { errdefer self.gpu_state.releaseFrame(); // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); + self.uniforms.blink_visible = self.blink_visible; + // Setup our frame data try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); try frame.cells_bg.sync(self.gpu_state.device, self.cells.bg_cells); @@ -1603,6 +1611,11 @@ fn drawCellFgs( @as(c_ulong, 1), }, ); + encoder.msgSend( + void, + objc.sel("setFragmentBuffer:offset:atIndex:"), + .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, + ); encoder.msgSend( void, @@ -2539,15 +2552,19 @@ fn updateCell( break :glyph; } - const mode: mtl_shaders.CellText.Mode = switch (try fgMode( + var mode: mtl_shaders.CellText.Mode = .{ .fg = true }; + + 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.add(self.alloc, .text, .{ .mode = mode, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 2a202de30..057e9c6fd 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -137,6 +137,9 @@ pub const Uniforms = extern struct { cursor_pos: [2]u16 align(4), cursor_color: [4]u8 align(4), + /// Whether blinking cells and cursors are visible on this frame. + blink_visible: bool align(1), + const PaddingExtend = packed struct(u8) { left: bool = false, right: bool = false, @@ -324,12 +327,15 @@ pub const CellText = extern struct { mode: Mode align(1), constraint_width: u8 align(1) = 0, - pub const Mode = enum(u8) { - fg = 1, - fg_constrained = 2, - fg_color = 3, - cursor = 4, - fg_powerline = 5, + pub const Mode = packed struct(u8) { + fg: bool, + fg_constrained: bool, + fg_color: bool, + cursor: bool, + fg_powerline: bool, + fg_blink: bool, + + _padding: u3 = 0, }; test { diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 734608c76..296c478b8 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -18,6 +18,7 @@ struct Uniforms { float min_contrast; ushort2 cursor_pos; uchar4 cursor_color; + bool blink_visible; }; //------------------------------------------------------------------- @@ -161,9 +162,9 @@ fragment float4 cell_bg_fragment( enum CellTextMode : uint8_t { MODE_TEXT = 1u, MODE_TEXT_CONSTRAINED = 2u, - MODE_TEXT_COLOR = 3u, - MODE_TEXT_CURSOR = 4u, - MODE_TEXT_POWERLINE = 5u, + MODE_TEXT_COLOR = 4u, + MODE_TEXT_CURSOR = 8u, + MODE_TEXT_POWERLINE = 16u, }; struct CellTextVertexIn { @@ -257,7 +258,7 @@ vertex CellTextVertexOut cell_text_vertex( // If we're constrained then we need to scale the glyph. // We also always constrain colored glyphs since we should have // their scaled cell size exactly correct. - if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) { + if (in.mode & (MODE_TEXT_CONSTRAINED | MODE_TEXT_COLOR)) { float max_width = uniforms.cell_size.x * in.constraint_width; if (size.x > max_width) { float new_y = size.y * (max_width / size.x); @@ -285,14 +286,14 @@ vertex CellTextVertexOut cell_text_vertex( // since we want color glyphs to appear in their original color // and Powerline glyphs to be unaffected (else parts of the line would // have different colors as some parts are displayed via background colors). - if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) { + if (uniforms.min_contrast > 1.0f && !(mode & ~(MODE_TEXT | MODE_TEXT_BLINK))) { float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f; out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); } // If this cell is the cursor cell, then we need to change the color. if ( - in.mode != MODE_TEXT_CURSOR && + !(in.mode & MODE_TEXT_CURSOR) && in.grid_pos.x == uniforms.cursor_pos.x && in.grid_pos.y == uniforms.cursor_pos.y ) { @@ -305,7 +306,8 @@ vertex CellTextVertexOut cell_text_vertex( fragment float4 cell_text_fragment( CellTextVertexOut in [[stage_in]], texture2d textureGrayscale [[texture(0)]], - texture2d textureColor [[texture(1)]] + texture2d textureColor [[texture(1)]], + constant Uniforms& uniforms [[buffer(1)]] ) { constexpr sampler textureSampler( coord::pixel, @@ -313,28 +315,23 @@ fragment float4 cell_text_fragment( filter::nearest ); - switch (in.mode) { - default: - case MODE_TEXT_CURSOR: - case MODE_TEXT_CONSTRAINED: - case MODE_TEXT_POWERLINE: - case MODE_TEXT: { - // We premult the alpha to our whole color since our blend function - // uses One/OneMinusSourceAlpha to avoid blurry edges. - // We first premult our given color. - float4 premult = float4(in.color.rgb * in.color.a, in.color.a); - - // Then premult the texture color - float a = textureGrayscale.sample(textureSampler, in.tex_coord).r; - premult = premult * a; - - return premult; - } - - case MODE_TEXT_COLOR: { - return textureColor.sample(textureSampler, in.tex_coord); - } + if (in.mode & MODE_TEXT_COLOR) { + return textureColor.sample(textureSampler, in.tex_coord); } + if (in.mode & MODE_TEXT_BLINK && !uniforms.blink_visible) { + discard_fragment(); + } + + // We premult the alpha to our whole color since our blend function + // uses One/OneMinusSourceAlpha to avoid blurry edges. + // We first premult our given color. + float4 premult = float4(in.color.rgb * in.color.a, in.color.a); + + // Then premult the texture color + float a = textureGrayscale.sample(textureSampler, in.tex_coord).r; + premult = premult * a; + + return premult; } //------------------------------------------------------------------- // Image Shader From b68f420376783d0f49043a25a4ef1bb8e87231ff Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 31 Aug 2024 15:36:06 +0200 Subject: [PATCH 03/10] renderer(metal): fix shader --- src/renderer/shaders/cell.metal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 296c478b8..b030072ee 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -165,6 +165,7 @@ enum CellTextMode : uint8_t { MODE_TEXT_COLOR = 4u, MODE_TEXT_CURSOR = 8u, MODE_TEXT_POWERLINE = 16u, + MODE_TEXT_BLINK = 32u, }; struct CellTextVertexIn { @@ -286,7 +287,7 @@ vertex CellTextVertexOut cell_text_vertex( // since we want color glyphs to appear in their original color // and Powerline glyphs to be unaffected (else parts of the line would // have different colors as some parts are displayed via background colors). - if (uniforms.min_contrast > 1.0f && !(mode & ~(MODE_TEXT | MODE_TEXT_BLINK))) { + if (uniforms.min_contrast > 1.0f && !(in.mode & ~(MODE_TEXT | MODE_TEXT_BLINK))) { float4 bg_color = float4(bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x]) / 255.0f; out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); } From 99742879c2454990206b06f42b544539bae18e28 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Sat, 31 Aug 2024 09:14:26 -0700 Subject: [PATCH 04/10] metal: fix compilation errors with blinking --- src/renderer/Metal.zig | 48 ++++++++++++++++++++++++++++++---- src/renderer/metal/shaders.zig | 3 ++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1b05316d9..29136dfee 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -621,6 +621,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .background_color = options.config.background, .cursor_color = options.config.cursor_color, .cursor_invert = options.config.cursor_invert, + .blink_visible = true, .current_background_color = options.config.background, // Render state @@ -634,6 +635,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .min_contrast = options.config.min_contrast, .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, .cursor_color = undefined, + .blink_visible = true, }, // Fonts @@ -2044,6 +2046,7 @@ pub fn setScreenSize( .min_contrast = old.min_contrast, .cursor_pos = old.cursor_pos, .cursor_color = old.cursor_color, + .blink_visible = old.blink_visible, }; // Reset our cell contents if our grid size has changed. @@ -2552,7 +2555,14 @@ fn updateCell( break :glyph; } - var mode: mtl_shaders.CellText.Mode = .{ .fg = true }; + var mode: mtl_shaders.CellText.Mode = .{ + .fg = true, + .fg_constrained = false, + .fg_color = false, + .cursor = false, + .fg_powerline = false, + .fg_blink = false, + }; switch (try fgMode( render.presentation, @@ -2603,7 +2613,14 @@ fn updateCell( const color = style.underlineColor(palette) orelse colors.fg; try self.cells.add(self.alloc, .underline, .{ - .mode = .fg, + .mode = .{ + .fg = true, + .fg_constrained = false, + .fg_color = false, + .cursor = false, + .fg_powerline = false, + .fg_blink = false, + }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .constraint_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, @@ -2628,7 +2645,14 @@ fn updateCell( ); try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .fg, + .mode = .{ + .fg = true, + .fg_constrained = false, + .fg_color = false, + .cursor = false, + .fg_powerline = false, + .fg_blink = false, + }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .constraint_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, @@ -2690,7 +2714,14 @@ fn addCursor( }; self.cells.setCursor(.{ - .mode = .cursor, + .mode = .{ + .fg = false, + .fg_constrained = false, + .fg_color = false, + .cursor = true, + .fg_powerline = false, + .fg_blink = false, + }, .grid_pos = .{ x, screen.cursor.y }, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, @@ -2739,7 +2770,14 @@ fn addPreeditCell( // Add our text try self.cells.add(self.alloc, .text, .{ - .mode = .fg, + .mode = .{ + .fg = true, + .fg_constrained = false, + .fg_color = false, + .cursor = false, + .fg_powerline = false, + .fg_blink = false, + }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .color = .{ fg.r, fg.g, fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 057e9c6fd..cad1f5b8b 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -335,7 +335,7 @@ pub const CellText = extern struct { fg_powerline: bool, fg_blink: bool, - _padding: u3 = 0, + _padding: u2 = 0, }; test { @@ -668,6 +668,7 @@ fn autoAttribute(T: type, attrs: objc.Object) void { [2]u32 => mtl.MTLVertexFormat.uint2, [4]u32 => mtl.MTLVertexFormat.uint4, u8 => mtl.MTLVertexFormat.uchar, + CellText.Mode => mtl.MTLVertexFormat.uchar, else => comptime unreachable, }; From 4834c706db306f7a8ef18ac10942b370c7c2f8f7 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sun, 1 Sep 2024 21:20:37 +0200 Subject: [PATCH 05/10] renderer(metal): set defaults for cell mode enum To reduce code duplication and bring it more in line with the OpenGL equivalent. --- src/renderer/Metal.zig | 45 ++++------------------------------ src/renderer/metal/shaders.zig | 10 ++++---- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 29136dfee..ae9868db5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2555,14 +2555,7 @@ fn updateCell( break :glyph; } - var mode: mtl_shaders.CellText.Mode = .{ - .fg = true, - .fg_constrained = false, - .fg_color = false, - .cursor = false, - .fg_powerline = false, - .fg_blink = false, - }; + var mode: mtl_shaders.CellText.Mode = .{ .fg = true }; switch (try fgMode( render.presentation, @@ -2613,14 +2606,7 @@ fn updateCell( const color = style.underlineColor(palette) orelse colors.fg; try self.cells.add(self.alloc, .underline, .{ - .mode = .{ - .fg = true, - .fg_constrained = false, - .fg_color = false, - .cursor = false, - .fg_powerline = false, - .fg_blink = false, - }, + .mode = .{ .fg = true }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .constraint_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, @@ -2645,14 +2631,7 @@ fn updateCell( ); try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .{ - .fg = true, - .fg_constrained = false, - .fg_color = false, - .cursor = false, - .fg_powerline = false, - .fg_blink = false, - }, + .mode = .{ .fg = true }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .constraint_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, @@ -2714,14 +2693,7 @@ fn addCursor( }; self.cells.setCursor(.{ - .mode = .{ - .fg = false, - .fg_constrained = false, - .fg_color = false, - .cursor = true, - .fg_powerline = false, - .fg_blink = false, - }, + .mode = .{ .fg = false, .cursor = true }, .grid_pos = .{ x, screen.cursor.y }, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, @@ -2770,14 +2742,7 @@ fn addPreeditCell( // Add our text try self.cells.add(self.alloc, .text, .{ - .mode = .{ - .fg = true, - .fg_constrained = false, - .fg_color = false, - .cursor = false, - .fg_powerline = false, - .fg_blink = false, - }, + .mode = .{ .fg = true }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .color = .{ fg.r, fg.g, fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index cad1f5b8b..4ab9655dd 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -329,11 +329,11 @@ pub const CellText = extern struct { pub const Mode = packed struct(u8) { fg: bool, - fg_constrained: bool, - fg_color: bool, - cursor: bool, - fg_powerline: bool, - fg_blink: bool, + fg_constrained: bool = false, + fg_color: bool = false, + cursor: bool = false, + fg_powerline: bool = false, + fg_blink: bool = false, _padding: u2 = 0, }; From 7f22f15e5b8be9437de10506d04bf1480c5b6bbd Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 2 Sep 2024 00:14:16 +0200 Subject: [PATCH 06/10] renderer: decouple text blink from cursor blink Coupling the text blink to the cursor blink leads to interesting problems, such as the text blink stopping when the user is typing, as the cursor is coded to stop blinking during typing. We now make the text blink on a separate timer that drives the cursor blink, so that text input events can easily cancel the cursor blink, waiting for it to be re-synchronized to the text blink. --- src/renderer/Metal.zig | 3 +- src/renderer/OpenGL.zig | 3 +- src/renderer/Thread.zig | 71 ++++++++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ae9868db5..991e7a298 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -873,6 +873,7 @@ pub fn updateFrame( surface: *apprt.Surface, state: *renderer.State, blink_visible: bool, + cursor_blink_visible: bool, ) !void { _ = surface; @@ -955,7 +956,7 @@ pub fn updateFrame( const cursor_style = renderer.cursorStyle( state, self.focused, - blink_visible, + cursor_blink_visible, ); // Get our preedit state diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 94216c3aa..1d96a17ae 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -705,6 +705,7 @@ pub fn updateFrame( surface: *apprt.Surface, state: *renderer.State, blink_visible: bool, + cursor_blink_visible: bool, ) !void { _ = surface; @@ -773,7 +774,7 @@ pub fn updateFrame( const cursor_style = renderer.cursorStyle( state, self.focused, - blink_visible, + cursor_blink_visible, ); // Get our preedit state diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index b3e54262d..a460b4594 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -56,7 +56,16 @@ draw_active: bool = false, draw_now: xev.Async, draw_now_c: xev.Completion = .{}, -/// The timer used for cursor blinking +/// The timer used for text blinking. This timer will always run, uninterrupted by +/// user text input, unlike the cursor blink timer which gets reset during typing. +blink_h: xev.Timer, +blink_c: xev.Completion = .{}, +blink_c_cancel: xev.Completion = .{}, + +/// The timer used for cursor blinking. This timer will get reset on user text input, +/// ensuring the cursor will always remain visible while typing. +/// When the user stops typing, the timer will wait till the main blink timer fires, +/// ensuring that the cursor remains in sync with text blinking. cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{}, @@ -81,6 +90,11 @@ app_mailbox: App.Mailbox, config: DerivedConfig, flags: packed struct { + /// This is true when blinking text should be visible and false + /// when it should not be visible. This is toggled on a timer by the + /// thread automatically. + blink_visible: bool = false, + /// This is true when a blinking cursor should be visible and false /// when it should not be visible. This is toggled on a timer by the /// thread automatically. @@ -139,6 +153,10 @@ pub fn init( var draw_now = try xev.Async.init(); errdefer draw_now.deinit(); + // Setup a timer for blinking the text + var blink_timer = try xev.Timer.init(); + errdefer blink_timer.deinit(); + // Setup a timer for blinking the cursor var cursor_timer = try xev.Timer.init(); errdefer cursor_timer.deinit(); @@ -156,6 +174,7 @@ pub fn init( .render_h = render_h, .draw_h = draw_h, .draw_now = draw_now, + .blink_h = blink_timer, .cursor_h = cursor_timer, .surface = surface, .renderer = renderer_impl, @@ -173,6 +192,7 @@ pub fn deinit(self: *Thread) void { self.render_h.deinit(); self.draw_h.deinit(); self.draw_now.deinit(); + self.blink_h.deinit(); self.cursor_h.deinit(); self.loop.deinit(); @@ -218,7 +238,15 @@ fn threadMain_(self: *Thread) !void { // Send an initial wakeup message so that we render right away. try self.wakeup.notify(); - // Start blinking the cursor. + // Start blinking the cursor and the text on screen. + self.blink_h.run( + &self.loop, + &self.blink_c, + CURSOR_BLINK_INTERVAL, + Thread, + self, + blinkTimerCallback, + ); self.cursor_h.run( &self.loop, &self.cursor_c, @@ -338,15 +366,15 @@ fn drainMailbox(self: *Thread) !void { .reset_cursor_blink => { self.flags.cursor_blink_visible = true; + if (self.cursor_c.state() == .active) { - self.cursor_h.reset( + self.cursor_h.cancel( &self.loop, &self.cursor_c, &self.cursor_c_cancel, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, + void, + null, + cursorCancelCallback, ); } }, @@ -529,6 +557,7 @@ fn renderCallback( t.renderer.updateFrame( t.surface, t.state, + t.flags.blink_visible, t.flags.cursor_blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -539,6 +568,28 @@ fn renderCallback( return .disarm; } +fn blinkTimerCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + + const t: *Thread = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + t.flags.blink_visible = !t.flags.blink_visible; + t.wakeup.notify() catch {}; + + t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); + t.blink_h.run(&t.loop, &t.blink_c, CURSOR_BLINK_INTERVAL, Thread, t, blinkTimerCallback); + + return .disarm; +} fn cursorTimerCallback( self_: ?*Thread, _: *xev.Loop, @@ -561,10 +612,8 @@ fn cursorTimerCallback( return .disarm; }; - t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; - t.wakeup.notify() catch {}; - - t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); + t.flags.cursor_blink_visible = !t.flags.blink_visible; + // We intentionally don't call `t.wakeup.notify()` here to avoid a double redraw. return .disarm; } From 7d150b050c4858562db700061a9872ac45621b40 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 2 Sep 2024 20:16:24 +0200 Subject: [PATCH 07/10] renderer: make blinking more robust As it turns out you can just set an "always visible" flag for the cursor when a user starts typing, and unset it when the next blink occurs. Note to self: maybe consider spending less time coding at 1am. --- src/renderer/Thread.zig | 132 ++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index a460b4594..a3ce8fc4c 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -16,7 +16,7 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); const DRAW_INTERVAL = 8; // 120 FPS -const CURSOR_BLINK_INTERVAL = 600; +const BLINK_INTERVAL = 600; /// The type used for sending messages to the IO thread. For now this is /// hardcoded with a capacity. We can make this a comptime parameter in @@ -57,15 +57,15 @@ draw_now: xev.Async, draw_now_c: xev.Completion = .{}, /// The timer used for text blinking. This timer will always run, uninterrupted by -/// user text input, unlike the cursor blink timer which gets reset during typing. +/// user text input. blink_h: xev.Timer, blink_c: xev.Completion = .{}, blink_c_cancel: xev.Completion = .{}, -/// The timer used for cursor blinking. This timer will get reset on user text input, -/// ensuring the cursor will always remain visible while typing. -/// When the user stops typing, the timer will wait till the main blink timer fires, -/// ensuring that the cursor remains in sync with text blinking. +/// The timer used to reset cursor blinking. When the cursor is set to always visible +/// (for example, while typing), this timer introduces a delay that synchronizes with +/// the main blink timer, to unset the always visible flag and allow the cursor to blink +/// in sync with the rest of the screen again. cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{}, @@ -95,10 +95,9 @@ flags: packed struct { /// thread automatically. blink_visible: bool = false, - /// This is true when a blinking cursor should be visible and false - /// when it should not be visible. This is toggled on a timer by the - /// thread automatically. - cursor_blink_visible: bool = false, + /// Whether the cursor should be forced into staying visible regardless + /// of blinking. Used when the user is typing. + cursor_always_visible: bool = false, /// This is true when the inspector is active. has_inspector: bool = false, @@ -242,19 +241,11 @@ fn threadMain_(self: *Thread) !void { self.blink_h.run( &self.loop, &self.blink_c, - CURSOR_BLINK_INTERVAL, + BLINK_INTERVAL, Thread, self, blinkTimerCallback, ); - self.cursor_h.run( - &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, - ); // Start the draw timer self.startDrawTimer(); @@ -331,52 +322,51 @@ fn drainMailbox(self: *Thread) !void { self.stopDrawTimer(); } - // If we're not focused, then we stop the cursor blink - if (self.cursor_c.state() == .active and - self.cursor_c_cancel.state() == .dead) + // If we're not focused, then we stop the blink + if (self.blink_c.state() == .active and + self.blink_c_cancel.state() == .dead) { - self.cursor_h.cancel( + self.blink_h.cancel( &self.loop, - &self.cursor_c, - &self.cursor_c_cancel, + &self.blink_c, + &self.blink_c_cancel, void, null, - cursorCancelCallback, + blinkCancelCallback, ); } } else { // Start the draw timer self.startDrawTimer(); - // If we're focused, we immediately show the cursor again - // and then restart the timer. - if (self.cursor_c.state() != .active) { - self.flags.cursor_blink_visible = true; - self.cursor_h.run( + // If we're focused, we immediately make blinking cells visible + // again and then restart the timer. + if (self.blink_c.state() != .active) { + self.flags.blink_visible = true; + self.blink_h.run( &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, + &self.blink_c, + BLINK_INTERVAL, Thread, self, - cursorTimerCallback, + blinkTimerCallback, ); } } }, .reset_cursor_blink => { - self.flags.cursor_blink_visible = true; + self.flags.cursor_always_visible = true; - if (self.cursor_c.state() == .active) { - self.cursor_h.cancel( - &self.loop, - &self.cursor_c, - &self.cursor_c_cancel, - void, - null, - cursorCancelCallback, - ); - } + self.cursor_h.reset( + &self.loop, + &self.cursor_c, + &self.cursor_c_cancel, + BLINK_INTERVAL, + Thread, + self, + resetCursorBlinkCallback, + ); }, .font_grid => |grid| { @@ -558,7 +548,7 @@ fn renderCallback( t.surface, t.state, t.flags.blink_visible, - t.flags.cursor_blink_visible, + t.flags.cursor_always_visible or t.flags.blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -573,28 +563,6 @@ fn blinkTimerCallback( _: *xev.Loop, _: *xev.Completion, r: xev.Timer.RunError!void, -) xev.CallbackAction { - _ = r catch unreachable; - - const t: *Thread = self_ orelse { - // This shouldn't happen so we log it. - log.warn("render callback fired without data set", .{}); - return .disarm; - }; - - t.flags.blink_visible = !t.flags.blink_visible; - t.wakeup.notify() catch {}; - - t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); - t.blink_h.run(&t.loop, &t.blink_c, CURSOR_BLINK_INTERVAL, Thread, t, blinkTimerCallback); - - return .disarm; -} -fn cursorTimerCallback( - self_: ?*Thread, - _: *xev.Loop, - _: *xev.Completion, - r: xev.Timer.RunError!void, ) xev.CallbackAction { _ = r catch |err| switch (err) { // This is sent when our timer is canceled. That's fine. @@ -612,12 +580,14 @@ fn cursorTimerCallback( return .disarm; }; - t.flags.cursor_blink_visible = !t.flags.blink_visible; - // We intentionally don't call `t.wakeup.notify()` here to avoid a double redraw. + t.flags.blink_visible = !t.flags.blink_visible; + t.wakeup.notify() catch {}; + t.blink_h.run(&t.loop, &t.blink_c, BLINK_INTERVAL, Thread, t, blinkTimerCallback); + return .disarm; } -fn cursorCancelCallback( +fn blinkCancelCallback( _: ?*void, _: *xev.Loop, _: *xev.Completion, @@ -635,7 +605,7 @@ fn cursorCancelCallback( error.Canceled => {}, // success error.NotFound => {}, // completed before it could cancel else => { - log.warn("error in cursor cancel callback err={}", .{err}); + log.warn("error in blink cancel callback err={}", .{err}); unreachable; }, }; @@ -643,6 +613,24 @@ fn cursorCancelCallback( return .disarm; } +fn resetCursorBlinkCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + + const t: *Thread = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + t.flags.cursor_always_visible = false; + return .disarm; +} + // fn prepFrameCallback(h: *libuv.Prepare) void { // _ = h; // From 789cec1b8b8bac70cc0b78640000804457cb8673 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 2 Sep 2024 22:22:29 +0200 Subject: [PATCH 08/10] config: add `text-blink` option to opt out of text blinking --- src/config/Config.zig | 7 +++++++ src/renderer/Thread.zig | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 1f9a78f31..59fa35f35 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -346,6 +346,13 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// This value does not apply to Emoji or images. @"minimum-contrast": f64 = 1, +/// Whether to enable blinking text. +/// Text can be made to blink via a SGR sequence, which is synchronized with +/// cursor blinking when that is enabled. Text blinking and cursor blinking +/// can be independently toggled on or off without interfering with each other: +/// to adjust cursor blinking settings, see `cursor-style-blink`. +@"text-blink": bool = true, + /// Color palette for the 256 color form that many terminal applications use. /// The syntax of this configuration is `N=HEXCODE` where `N` is 0 to 255 (for /// the 256 colors in the terminal color table) and `HEXCODE` is a typical RGB diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index a3ce8fc4c..1b1f33c7d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -93,7 +93,7 @@ flags: packed struct { /// This is true when blinking text should be visible and false /// when it should not be visible. This is toggled on a timer by the /// thread automatically. - blink_visible: bool = false, + blink_visible: bool = true, /// Whether the cursor should be forced into staying visible regardless /// of blinking. Used when the user is typing. @@ -109,10 +109,12 @@ flags: packed struct { pub const DerivedConfig = struct { custom_shader_animation: configpkg.CustomShaderAnimation, + text_blink: bool, pub fn init(config: *const configpkg.Config) DerivedConfig { return .{ .custom_shader_animation = config.@"custom-shader-animation", + .text_blink = config.@"text-blink", }; } }; @@ -547,7 +549,7 @@ fn renderCallback( t.renderer.updateFrame( t.surface, t.state, - t.flags.blink_visible, + !t.config.text_blink or t.flags.blink_visible, t.flags.cursor_always_visible or t.flags.blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); From 83bcf939fd880707f39f55646b262816e3dd9aa6 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 2 Sep 2024 23:21:07 +0200 Subject: [PATCH 09/10] config: make blink interval configurable --- src/config/Config.zig | 40 ++++++++++++++++++++++++----- src/renderer/Thread.zig | 57 ++++++++++++++++++++++++----------------- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 59fa35f35..ed1932f81 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -346,12 +346,40 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// This value does not apply to Emoji or images. @"minimum-contrast": f64 = 1, -/// Whether to enable blinking text. -/// Text can be made to blink via a SGR sequence, which is synchronized with -/// cursor blinking when that is enabled. Text blinking and cursor blinking -/// can be independently toggled on or off without interfering with each other: -/// to adjust cursor blinking settings, see `cursor-style-blink`. -@"text-blink": bool = true, +/// The amount of time it takes for blinking text and cursors to toggle between +/// being invisible and visible. +/// Any cell on the screen could be set to blink by setting SGR attribute 5, +/// while cursor blinking is controlled by the `cursor-style-blink` setting. +/// +/// The interval is specified as a series of numbers followed by time units. +/// Whitespace is allowed between numbers and units. Each number and unit will +/// be added together to form the total interval. +/// +/// Blinking is disabled when the interval is set to zero. +/// +/// The allowed time units are as follows: +/// +/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments +/// are made for leap years or leap seconds. +/// * `d` - one SI day, or 86400 seconds. +/// * `h` - one hour, or 3600 seconds. +/// * `m` - one minute, or 60 seconds. +/// * `s` - one second. +/// * `ms` - one millisecond, or 0.001 second. +/// * `us` or `µs` - one microsecond, or 0.000001 second. +/// * `ns` - one nanosecond, or 0.000000001 second. +/// +/// Examples: +/// * `1h30m` +/// * `45s` +/// +/// Units can be repeated and will be added together. This means that +/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided. +/// A future update may disallow this. +/// +/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any +/// value larger than this will be clamped to the maximum value. +@"blink-interval": Duration = .{ .duration = 600 * std.time.ns_per_ms }, /// Color palette for the 256 color form that many terminal applications use. /// The syntax of this configuration is `N=HEXCODE` where `N` is 0 to 255 (for diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 1b1f33c7d..6433fa1ee 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -16,7 +16,6 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); const DRAW_INTERVAL = 8; // 120 FPS -const BLINK_INTERVAL = 600; /// The type used for sending messages to the IO thread. For now this is /// hardcoded with a capacity. We can make this a comptime parameter in @@ -109,12 +108,18 @@ flags: packed struct { pub const DerivedConfig = struct { custom_shader_animation: configpkg.CustomShaderAnimation, - text_blink: bool, + blink_interval: u64, pub fn init(config: *const configpkg.Config) DerivedConfig { + const blink_interval = std.math.divTrunc( + u64, + config.@"blink-interval".duration, + std.time.ns_per_ms, + ) catch std.math.maxInt(u64); + return .{ .custom_shader_animation = config.@"custom-shader-animation", - .text_blink = config.@"text-blink", + .blink_interval = blink_interval, }; } }; @@ -240,14 +245,16 @@ fn threadMain_(self: *Thread) !void { try self.wakeup.notify(); // Start blinking the cursor and the text on screen. - self.blink_h.run( - &self.loop, - &self.blink_c, - BLINK_INTERVAL, - Thread, - self, - blinkTimerCallback, - ); + if (self.config.blink_interval > 0) { + self.blink_h.run( + &self.loop, + &self.blink_c, + self.config.blink_interval, + Thread, + self, + blinkTimerCallback, + ); + } // Start the draw timer self.startDrawTimer(); @@ -343,12 +350,12 @@ fn drainMailbox(self: *Thread) !void { // If we're focused, we immediately make blinking cells visible // again and then restart the timer. - if (self.blink_c.state() != .active) { + if (self.blink_c.state() != .active and self.config.blink_interval > 0) { self.flags.blink_visible = true; self.blink_h.run( &self.loop, &self.blink_c, - BLINK_INTERVAL, + self.config.blink_interval, Thread, self, blinkTimerCallback, @@ -360,15 +367,17 @@ fn drainMailbox(self: *Thread) !void { .reset_cursor_blink => { self.flags.cursor_always_visible = true; - self.cursor_h.reset( - &self.loop, - &self.cursor_c, - &self.cursor_c_cancel, - BLINK_INTERVAL, - Thread, - self, - resetCursorBlinkCallback, - ); + if (self.config.blink_interval > 0) { + self.cursor_h.reset( + &self.loop, + &self.cursor_c, + &self.cursor_c_cancel, + self.config.blink_interval, + Thread, + self, + resetCursorBlinkCallback, + ); + } }, .font_grid => |grid| { @@ -549,7 +558,7 @@ fn renderCallback( t.renderer.updateFrame( t.surface, t.state, - !t.config.text_blink or t.flags.blink_visible, + t.flags.blink_visible, t.flags.cursor_always_visible or t.flags.blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -584,7 +593,7 @@ fn blinkTimerCallback( t.flags.blink_visible = !t.flags.blink_visible; t.wakeup.notify() catch {}; - t.blink_h.run(&t.loop, &t.blink_c, BLINK_INTERVAL, Thread, t, blinkTimerCallback); + t.blink_h.run(&t.loop, &t.blink_c, t.config.blink_interval, Thread, t, blinkTimerCallback); return .disarm; } From f0db0c850b58d375515620a3cd5b1d695e41ac10 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Tue, 3 Sep 2024 20:41:11 +0200 Subject: [PATCH 10/10] renderer(metal): fix tests --- src/renderer/metal/cell.zig | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 94b8b39bb..70fe80193 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -125,7 +125,7 @@ pub const Contents = struct { const bg_cells = try alloc.alloc(mtl_shaders.CellBg, cell_count); errdefer alloc.free(bg_cells); - @memset(bg_cells, .{0, 0, 0, 0}); + @memset(bg_cells, .{ 0, 0, 0, 0 }); // The foreground lists can hold 3 types of items: // - Glyphs @@ -231,7 +231,7 @@ test Contents { for (0..rows) |y| { try testing.expect(c.fg_rows.lists[y + 1].items.len == 0); for (0..cols) |x| { - try testing.expectEqual(.{0, 0, 0, 0}, c.bgCell(y, x).*); + try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*); } } // And the cursor row should have a capacity of 1 and also be empty. @@ -241,7 +241,7 @@ test Contents { // Add some contents. const bg_cell: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell: mtl_shaders.CellText = .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -256,13 +256,16 @@ test Contents { for (0..rows) |y| { try testing.expect(c.fg_rows.lists[y + 1].items.len == 0); for (0..cols) |x| { - try testing.expectEqual(.{0, 0, 0, 0}, c.bgCell(y, x).*); + try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*); } } // Add a cursor. const cursor_cell: mtl_shaders.CellText = .{ - .mode = .cursor, + .mode = .{ + .fg = false, + .cursor = true, + }, .grid_pos = .{ 2, 3 }, .color = .{ 0, 0, 0, 1 }, }; @@ -289,7 +292,7 @@ test "Contents clear retains other content" { // bg and fg cells in row 1 const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_1: mtl_shaders.CellText = .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -298,7 +301,7 @@ test "Contents clear retains other content" { // bg and fg cells in row 2 const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_2: mtl_shaders.CellText = .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ 4, 2 }, .color = .{ 0, 0, 0, 1 }, }; @@ -329,7 +332,7 @@ test "Contents clear last added content" { // bg and fg cells in row 1 const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_1: mtl_shaders.CellText = .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ 4, 1 }, .color = .{ 0, 0, 0, 1 }, }; @@ -338,7 +341,7 @@ test "Contents clear last added content" { // bg and fg cells in row 2 const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 }; const fg_cell_2: mtl_shaders.CellText = .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ 4, 2 }, .color = .{ 0, 0, 0, 1 }, };