diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 97292b9b0..efd69f288 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -92,6 +92,7 @@ pub const Shaper = struct { grid: *SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, + text_blink_visible: bool, selection: ?terminal.Selection, cursor_x: ?usize, ) font.shape.RunIterator { @@ -100,6 +101,7 @@ pub const Shaper = struct { .grid = grid, .screen = screen, .row = row, + .text_blink_visible = text_blink_visible, .selection = selection, .cursor_x = cursor_x, }; @@ -229,6 +231,7 @@ test "run iterator" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -248,6 +251,7 @@ test "run iterator" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -268,6 +272,7 @@ test "run iterator" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -320,6 +325,7 @@ test "run iterator: empty cells with background set" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -357,6 +363,7 @@ test "shape" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -386,6 +393,7 @@ test "shape inconsolata ligs" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -411,6 +419,7 @@ test "shape inconsolata ligs" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -444,6 +453,7 @@ test "shape monaspace ligs" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -480,6 +490,7 @@ test "shape arabic forced LTR" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -517,6 +528,7 @@ test "shape emoji width" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -559,6 +571,7 @@ test "shape emoji width long" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -597,6 +610,7 @@ test "shape variation selector VS15" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -634,6 +648,7 @@ test "shape variation selector VS16" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -668,6 +683,7 @@ test "shape with empty cells in between" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -706,6 +722,7 @@ test "shape Chinese characters" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -746,6 +763,7 @@ test "shape box glyphs" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -783,6 +801,7 @@ test "shape selection boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, @@ -806,6 +825,7 @@ test "shape selection boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, @@ -829,6 +849,7 @@ test "shape selection boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, @@ -852,6 +873,7 @@ test "shape selection boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, @@ -875,6 +897,7 @@ test "shape selection boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, @@ -911,6 +934,7 @@ test "shape cursor boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -930,6 +954,7 @@ test "shape cursor boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, 0, ); @@ -949,6 +974,7 @@ test "shape cursor boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, 1, ); @@ -968,6 +994,7 @@ test "shape cursor boundary" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, 9, ); @@ -1000,6 +1027,7 @@ test "shape cursor boundary and colored emoji" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -1019,6 +1047,7 @@ test "shape cursor boundary and colored emoji" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, 0, ); @@ -1036,6 +1065,7 @@ test "shape cursor boundary and colored emoji" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, 1, ); @@ -1066,6 +1096,7 @@ test "shape cell attribute change" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -1090,6 +1121,7 @@ test "shape cell attribute change" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -1115,6 +1147,7 @@ test "shape cell attribute change" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -1140,6 +1173,7 @@ test "shape cell attribute change" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); @@ -1164,6 +1198,7 @@ test "shape cell attribute change" { testdata.grid, &screen, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + true, null, null, ); diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig index f8988f4ee..7cbcb5a93 100644 --- a/src/font/shaper/noop.zig +++ b/src/font/shaper/noop.zig @@ -73,6 +73,7 @@ pub const Shaper = struct { grid: *SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, + text_blink_visible: bool, selection: ?terminal.Selection, cursor_x: ?usize, ) font.shape.RunIterator { @@ -81,6 +82,7 @@ pub const Shaper = struct { .grid = grid, .screen = screen, .row = row, + .text_blink_visible = text_blink_visible, .selection = selection, .cursor_x = cursor_x, }; diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 22d19979e..1321fd80b 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -38,6 +38,7 @@ pub const RunIterator = struct { grid: *font.SharedGrid, screen: *const terminal.Screen, row: terminal.Pin, + text_blink_visible: bool, selection: ?terminal.Selection = null, cursor_x: ?usize = null, i: usize = 0, @@ -58,7 +59,8 @@ pub const RunIterator = struct { // Invisible cells don't have any glyphs rendered, // so we explicitly skip them in the shaping process. while (self.i < max and - self.row.style(&cells[self.i]).flags.invisible) + (self.row.style(&cells[self.i]).flags.invisible or + (self.row.style(&cells[self.i]).flags.blink and !self.text_blink_visible))) { self.i += 1; } diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index f38ab885a..6245090eb 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -63,6 +63,7 @@ pub const Shaper = struct { self: *Shaper, group: *font.GroupCache, row: terminal.Screen.Row, + text_blink_visible: bool, selection: ?terminal.Selection, cursor_x: ?usize, ) font.shape.RunIterator { @@ -70,6 +71,7 @@ pub const Shaper = struct { .hooks = .{ .shaper = self }, .group = group, .row = row, + .text_blink_visible = text_blink_visible, .selection = selection, .cursor_x = cursor_x, }; @@ -295,7 +297,7 @@ pub const Wasm = struct { while (rowIter.next()) |row| { defer y += 1; - var iter = self.runIterator(group, row, null, null); + var iter = self.runIterator(group, row, true, null, null); while (try iter.next(alloc)) |run| { const cells = try self.shape(run); log.info("y={} run={d} shape={any} idx={}", .{ diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 45d8f84c2..73b08d0ff 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2377,6 +2377,7 @@ fn rebuildCells( preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, + text_blink_visible: bool, ) !void { // const start = try std.time.Instant.now(); // const start_micro = std.time.microTimestamp(); @@ -2495,6 +2496,7 @@ fn rebuildCells( screen, row, row_selection, + text_blink_visible, if (shape_cursor) screen.cursor.x else null, ); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); @@ -2694,7 +2696,7 @@ fn rebuildCells( // emulators, e.g. Alacritty, still render text decorations // and only make the text itself invisible. The decision // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { + if (style.flags.invisible or style.flags.blink) { continue; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e5dec6b2b..5545a12cc 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -689,6 +689,7 @@ pub fn updateFrame( surface: *apprt.Surface, state: *renderer.State, cursor_blink_visible: bool, + text_blink_visible: bool, ) !void { _ = surface; @@ -702,6 +703,7 @@ pub fn updateFrame( preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, + text_blink_visible: bool, }; // Update all our data as tightly as possible within the mutex. @@ -863,6 +865,7 @@ pub fn updateFrame( .preedit = preedit, .cursor_style = cursor_style, .color_palette = state.terminal.color_palette.colors, + .text_blink_visible = text_blink_visible, }; }; defer { @@ -887,6 +890,7 @@ pub fn updateFrame( critical.preedit, critical.cursor_style, &critical.color_palette, + critical.text_blink_visible, ); // Notify our shaper we're done for the frame. For some shapers like @@ -1217,6 +1221,7 @@ pub fn rebuildCells( preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, + text_blink_visible: bool, ) !void { _ = screen_type; @@ -1349,6 +1354,7 @@ pub fn rebuildCells( self.font_grid, screen, row, + text_blink_visible, row_selection, if (shape_cursor) screen.cursor.x else null, ); @@ -1576,7 +1582,7 @@ pub fn rebuildCells( // emulators, e.g. Alacritty, still render text decorations // and only make the text itself invisible. The decision // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { + if (style.flags.invisible or (style.flags.blink and !text_blink_visible)) { continue; } diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index cc63889fa..87bdeba93 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -18,7 +18,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 @@ -58,10 +58,15 @@ draw_active: bool = false, draw_now: xev.Async, draw_now_c: xev.Completion = .{}, -/// The timer used for cursor blinking -cursor_h: xev.Timer, -cursor_c: xev.Completion = .{}, -cursor_c_cancel: xev.Completion = .{}, +/// Timer that cursor and text blinking is built on +blink_h: xev.Timer, +blink_c: xev.Completion = .{}, +blink_c_cancel: xev.Completion = .{}, +blink: bool = true, + +cursor_blink_active: bool = true, +cursor_blink_buffer: bool = true, +text_blink_active: bool = true, /// The surface we're rendering to. surface: *apprt.Surface, @@ -88,6 +93,11 @@ flags: packed struct { /// thread automatically. cursor_blink_visible: bool = 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. + text_blink_visible: bool = true, + /// This is true when the inspector is active. has_inspector: bool = false, @@ -145,9 +155,9 @@ pub fn init( var draw_now = try xev.Async.init(); errdefer draw_now.deinit(); - // Setup a timer for blinking the cursor - var cursor_timer = try xev.Timer.init(); - errdefer cursor_timer.deinit(); + // Setup a timer for blinking the cursor and text + var blink_timer = try xev.Timer.init(); + errdefer blink_timer.deinit(); // The mailbox for messaging this thread var mailbox = try Mailbox.create(alloc); @@ -162,7 +172,7 @@ pub fn init( .render_h = render_h, .draw_h = draw_h, .draw_now = draw_now, - .cursor_h = cursor_timer, + .blink_h = blink_timer, .surface = surface, .renderer = renderer_impl, .state = state, @@ -179,7 +189,7 @@ pub fn deinit(self: *Thread) void { self.render_h.deinit(); self.draw_h.deinit(); self.draw_now.deinit(); - self.cursor_h.deinit(); + self.blink_h.deinit(); self.loop.deinit(); // Nothing can possibly access the mailbox anymore, destroy it. @@ -227,14 +237,14 @@ 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( + // Start the blinking timer. + self.blink_h.run( &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, + &self.blink_c, + BLINK_INTERVAL, Thread, self, - cursorTimerCallback, + blinkTimerCallback, ); // Start the draw timer @@ -357,52 +367,39 @@ 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( - &self.loop, - &self.cursor_c, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, - ); - } + // If we're focused, we immediately start blinking again + self.blink_h.run( + &self.loop, + &self.blink_c, + BLINK_INTERVAL, + Thread, + self, + blinkTimerCallback, + ); } }, .reset_cursor_blink => { self.flags.cursor_blink_visible = true; - if (self.cursor_c.state() == .active) { - self.cursor_h.reset( - &self.loop, - &self.cursor_c, - &self.cursor_c_cancel, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, - ); - } + self.cursor_blink_active = true; + self.cursor_blink_buffer = false; }, .font_grid => |grid| { @@ -585,6 +582,7 @@ fn renderCallback( t.surface, t.state, t.flags.cursor_blink_visible, + t.flags.text_blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -594,7 +592,7 @@ fn renderCallback( return .disarm; } -fn cursorTimerCallback( +fn blinkTimerCallback( self_: ?*Thread, _: *xev.Loop, _: *xev.Completion, @@ -616,14 +614,29 @@ fn cursorTimerCallback( return .disarm; }; - t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; + // Keep blinking sychronized + t.blink = !t.blink; + + // After cursor blink is reset, don't change state for one interval + if (t.cursor_blink_buffer) { + if (t.cursor_blink_active) { + t.flags.cursor_blink_visible = t.blink; + } + } else { + t.cursor_blink_buffer = true; + } + + if (t.text_blink_active) { + t.flags.text_blink_visible = t.blink; + } + 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, BLINK_INTERVAL, Thread, t, blinkTimerCallback); return .disarm; } -fn cursorCancelCallback( +fn blinkCancelCallback( _: ?*void, _: *xev.Loop, _: *xev.Completion,