diff --git a/src/config/Config.zig b/src/config/Config.zig index efa741307..6efc929d4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -346,6 +346,41 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// This value does not apply to Emoji or images. @"minimum-contrast": f64 = 1, +/// 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 /// the 256 colors in the terminal color table) and `HEXCODE` is a typical RGB diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index bb2a27f44..167994a49 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, @@ -618,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 @@ -631,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 @@ -867,6 +872,7 @@ pub fn updateFrame( self: *Metal, surface: *apprt.Surface, state: *renderer.State, + blink_visible: bool, cursor_blink_visible: bool, ) !void { _ = surface; @@ -1030,6 +1036,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); @@ -1104,6 +1113,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); @@ -1606,6 +1617,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, @@ -2034,6 +2050,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. @@ -2542,15 +2559,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, @@ -2589,7 +2610,7 @@ fn updateCell( const color = style.underlineColor(palette) orelse colors.fg; try self.cells.add(self.alloc, .underline, .{ - .mode = .fg, + .mode = .{ .fg = true }, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .constraint_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, @@ -2614,7 +2635,7 @@ fn updateCell( ); try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .fg, + .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 }, @@ -2704,7 +2725,7 @@ fn addCursor( }; self.cells.setCursor(.{ - .mode = .cursor, + .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 }, @@ -2753,7 +2774,7 @@ fn addPreeditCell( // Add our text try self.cells.add(self.alloc, .text, .{ - .mode = .fg, + .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/OpenGL.zig b/src/renderer/OpenGL.zig index a8f7c385c..9c720619c 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,6 +704,7 @@ pub fn updateFrame( self: *OpenGL, surface: *apprt.Surface, state: *renderer.State, + blink_visible: bool, cursor_blink_visible: bool, ) !void { _ = surface; @@ -857,6 +861,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); @@ -1282,7 +1289,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; } @@ -1419,7 +1426,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; @@ -1439,8 +1446,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); } } @@ -1472,7 +1479,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, @@ -1494,7 +1501,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, @@ -1589,7 +1596,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, @@ -1733,7 +1740,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(), @@ -1774,16 +1781,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, @@ -1830,7 +1841,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(), @@ -1863,7 +1874,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(), @@ -2187,6 +2198,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/Thread.zig b/src/renderer/Thread.zig index b3e54262d..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 CURSOR_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 @@ -56,7 +55,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. +blink_h: xev.Timer, +blink_c: xev.Completion = .{}, +blink_c_cancel: xev.Completion = .{}, + +/// 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 = .{}, @@ -81,10 +89,14 @@ app_mailbox: App.Mailbox, config: DerivedConfig, flags: packed struct { - /// This is true when a blinking cursor should be visible and false + /// 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. - cursor_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. + cursor_always_visible: bool = false, /// This is true when the inspector is active. has_inspector: bool = false, @@ -96,10 +108,18 @@ flags: packed struct { pub const DerivedConfig = struct { custom_shader_animation: configpkg.CustomShaderAnimation, + 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", + .blink_interval = blink_interval, }; } }; @@ -139,6 +159,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 +180,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 +198,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,15 +244,17 @@ fn threadMain_(self: *Thread) !void { // Send an initial wakeup message so that we render right away. try self.wakeup.notify(); - // Start blinking the cursor. - self.cursor_h.run( - &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, - ); + // Start blinking the cursor and the text on screen. + 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(); @@ -303,50 +331,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 and self.config.blink_interval > 0) { + self.flags.blink_visible = true; + self.blink_h.run( &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, + &self.blink_c, + self.config.blink_interval, Thread, self, - cursorTimerCallback, + blinkTimerCallback, ); } } }, .reset_cursor_blink => { - self.flags.cursor_blink_visible = true; - if (self.cursor_c.state() == .active) { + self.flags.cursor_always_visible = true; + + if (self.config.blink_interval > 0) { self.cursor_h.reset( &self.loop, &self.cursor_c, &self.cursor_c_cancel, - CURSOR_BLINK_INTERVAL, + self.config.blink_interval, Thread, self, - cursorTimerCallback, + resetCursorBlinkCallback, ); } }, @@ -529,7 +558,8 @@ fn renderCallback( t.renderer.updateFrame( t.surface, t.state, - t.flags.cursor_blink_visible, + t.flags.blink_visible, + t.flags.cursor_always_visible or t.flags.blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -539,7 +569,7 @@ fn renderCallback( return .disarm; } -fn cursorTimerCallback( +fn blinkTimerCallback( self_: ?*Thread, _: *xev.Loop, _: *xev.Completion, @@ -561,14 +591,14 @@ fn cursorTimerCallback( return .disarm; }; - t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; + t.flags.blink_visible = !t.flags.blink_visible; t.wakeup.notify() catch {}; + t.blink_h.run(&t.loop, &t.blink_c, t.config.blink_interval, Thread, t, blinkTimerCallback); - t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); return .disarm; } -fn cursorCancelCallback( +fn blinkCancelCallback( _: ?*void, _: *xev.Loop, _: *xev.Completion, @@ -586,7 +616,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; }, }; @@ -594,6 +624,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; // 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 }, }; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 2a202de30..4ab9655dd 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 = false, + fg_color: bool = false, + cursor: bool = false, + fg_powerline: bool = false, + fg_blink: bool = false, + + _padding: u2 = 0, }; test { @@ -662,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, }; 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.metal b/src/renderer/shaders/cell.metal index 734608c76..b030072ee 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,10 @@ 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, + MODE_TEXT_BLINK = 32u, }; struct CellTextVertexIn { @@ -257,7 +259,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 +287,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 && !(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); } // 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 +307,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 +316,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 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; } }