ansi: Add support blinking text

Add support for the blinking text ansi character. Both "slow" (code 5)
and "fast" (code 6) blink at the same interval as the cursor, as it
seems implementations often do this.

Blinking of text stops when the screen loses focus, same as the cursor.
To keep blinking of text and cursor in sync, typing a character no
longer resets the cursor blinking timer. Instead of reseting the timer,
the cursor is immediately set to visible and will stay visible for at
least the length of one interval. This has the side-effect that at
startup, it takes two intervals until the first cursor blink happens.
This commit is contained in:
Charlie Jenkins
2025-01-11 20:51:38 -08:00
parent 72d085525b
commit 3929ee7a99
7 changed files with 116 additions and 54 deletions

View File

@ -92,6 +92,7 @@ pub const Shaper = struct {
grid: *SharedGrid, grid: *SharedGrid,
screen: *const terminal.Screen, screen: *const terminal.Screen,
row: terminal.Pin, row: terminal.Pin,
text_blink_visible: bool,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
@ -100,6 +101,7 @@ pub const Shaper = struct {
.grid = grid, .grid = grid,
.screen = screen, .screen = screen,
.row = row, .row = row,
.text_blink_visible = text_blink_visible,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
}; };
@ -229,6 +231,7 @@ test "run iterator" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -248,6 +251,7 @@ test "run iterator" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -268,6 +272,7 @@ test "run iterator" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -320,6 +325,7 @@ test "run iterator: empty cells with background set" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -357,6 +363,7 @@ test "shape" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -386,6 +393,7 @@ test "shape inconsolata ligs" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -411,6 +419,7 @@ test "shape inconsolata ligs" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -444,6 +453,7 @@ test "shape monaspace ligs" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -480,6 +490,7 @@ test "shape arabic forced LTR" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -517,6 +528,7 @@ test "shape emoji width" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -559,6 +571,7 @@ test "shape emoji width long" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -597,6 +610,7 @@ test "shape variation selector VS15" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -634,6 +648,7 @@ test "shape variation selector VS16" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -668,6 +683,7 @@ test "shape with empty cells in between" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -706,6 +722,7 @@ test "shape Chinese characters" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -746,6 +763,7 @@ test "shape box glyphs" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -783,6 +801,7 @@ test "shape selection boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
terminal.Selection.init( terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
@ -806,6 +825,7 @@ test "shape selection boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
terminal.Selection.init( terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
@ -829,6 +849,7 @@ test "shape selection boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
terminal.Selection.init( terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
@ -852,6 +873,7 @@ test "shape selection boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
terminal.Selection.init( terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
@ -875,6 +897,7 @@ test "shape selection boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
terminal.Selection.init( terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
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, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -930,6 +954,7 @@ test "shape cursor boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
0, 0,
); );
@ -949,6 +974,7 @@ test "shape cursor boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
1, 1,
); );
@ -968,6 +994,7 @@ test "shape cursor boundary" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
9, 9,
); );
@ -1000,6 +1027,7 @@ test "shape cursor boundary and colored emoji" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -1019,6 +1047,7 @@ test "shape cursor boundary and colored emoji" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
0, 0,
); );
@ -1036,6 +1065,7 @@ test "shape cursor boundary and colored emoji" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
1, 1,
); );
@ -1066,6 +1096,7 @@ test "shape cell attribute change" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -1090,6 +1121,7 @@ test "shape cell attribute change" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -1115,6 +1147,7 @@ test "shape cell attribute change" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -1140,6 +1173,7 @@ test "shape cell attribute change" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );
@ -1164,6 +1198,7 @@ test "shape cell attribute change" {
testdata.grid, testdata.grid,
&screen, &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
true,
null, null,
null, null,
); );

View File

@ -73,6 +73,7 @@ pub const Shaper = struct {
grid: *SharedGrid, grid: *SharedGrid,
screen: *const terminal.Screen, screen: *const terminal.Screen,
row: terminal.Pin, row: terminal.Pin,
text_blink_visible: bool,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
@ -81,6 +82,7 @@ pub const Shaper = struct {
.grid = grid, .grid = grid,
.screen = screen, .screen = screen,
.row = row, .row = row,
.text_blink_visible = text_blink_visible,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
}; };

View File

@ -38,6 +38,7 @@ pub const RunIterator = struct {
grid: *font.SharedGrid, grid: *font.SharedGrid,
screen: *const terminal.Screen, screen: *const terminal.Screen,
row: terminal.Pin, row: terminal.Pin,
text_blink_visible: bool,
selection: ?terminal.Selection = null, selection: ?terminal.Selection = null,
cursor_x: ?usize = null, cursor_x: ?usize = null,
i: usize = 0, i: usize = 0,
@ -58,7 +59,8 @@ pub const RunIterator = struct {
// Invisible cells don't have any glyphs rendered, // Invisible cells don't have any glyphs rendered,
// so we explicitly skip them in the shaping process. // so we explicitly skip them in the shaping process.
while (self.i < max and 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; self.i += 1;
} }

View File

@ -63,6 +63,7 @@ pub const Shaper = struct {
self: *Shaper, self: *Shaper,
group: *font.GroupCache, group: *font.GroupCache,
row: terminal.Screen.Row, row: terminal.Screen.Row,
text_blink_visible: bool,
selection: ?terminal.Selection, selection: ?terminal.Selection,
cursor_x: ?usize, cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
@ -70,6 +71,7 @@ pub const Shaper = struct {
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
.group = group, .group = group,
.row = row, .row = row,
.text_blink_visible = text_blink_visible,
.selection = selection, .selection = selection,
.cursor_x = cursor_x, .cursor_x = cursor_x,
}; };
@ -295,7 +297,7 @@ pub const Wasm = struct {
while (rowIter.next()) |row| { while (rowIter.next()) |row| {
defer y += 1; 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| { while (try iter.next(alloc)) |run| {
const cells = try self.shape(run); const cells = try self.shape(run);
log.info("y={} run={d} shape={any} idx={}", .{ log.info("y={} run={d} shape={any} idx={}", .{

View File

@ -2377,6 +2377,7 @@ fn rebuildCells(
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle, cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette, color_palette: *const terminal.color.Palette,
text_blink_visible: bool,
) !void { ) !void {
// const start = try std.time.Instant.now(); // const start = try std.time.Instant.now();
// const start_micro = std.time.microTimestamp(); // const start_micro = std.time.microTimestamp();
@ -2495,6 +2496,7 @@ fn rebuildCells(
screen, screen,
row, row,
row_selection, row_selection,
text_blink_visible,
if (shape_cursor) screen.cursor.x else null, if (shape_cursor) screen.cursor.x else null,
); );
var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); 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 // emulators, e.g. Alacritty, still render text decorations
// and only make the text itself invisible. The decision // and only make the text itself invisible. The decision
// has been made here to match xterm's behavior for this. // has been made here to match xterm's behavior for this.
if (style.flags.invisible) { if (style.flags.invisible or style.flags.blink) {
continue; continue;
} }

View File

@ -689,6 +689,7 @@ pub fn updateFrame(
surface: *apprt.Surface, surface: *apprt.Surface,
state: *renderer.State, state: *renderer.State,
cursor_blink_visible: bool, cursor_blink_visible: bool,
text_blink_visible: bool,
) !void { ) !void {
_ = surface; _ = surface;
@ -702,6 +703,7 @@ pub fn updateFrame(
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle, cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette, color_palette: terminal.color.Palette,
text_blink_visible: bool,
}; };
// Update all our data as tightly as possible within the mutex. // Update all our data as tightly as possible within the mutex.
@ -863,6 +865,7 @@ pub fn updateFrame(
.preedit = preedit, .preedit = preedit,
.cursor_style = cursor_style, .cursor_style = cursor_style,
.color_palette = state.terminal.color_palette.colors, .color_palette = state.terminal.color_palette.colors,
.text_blink_visible = text_blink_visible,
}; };
}; };
defer { defer {
@ -887,6 +890,7 @@ pub fn updateFrame(
critical.preedit, critical.preedit,
critical.cursor_style, critical.cursor_style,
&critical.color_palette, &critical.color_palette,
critical.text_blink_visible,
); );
// Notify our shaper we're done for the frame. For some shapers like // Notify our shaper we're done for the frame. For some shapers like
@ -1217,6 +1221,7 @@ pub fn rebuildCells(
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle, cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette, color_palette: *const terminal.color.Palette,
text_blink_visible: bool,
) !void { ) !void {
_ = screen_type; _ = screen_type;
@ -1349,6 +1354,7 @@ pub fn rebuildCells(
self.font_grid, self.font_grid,
screen, screen,
row, row,
text_blink_visible,
row_selection, row_selection,
if (shape_cursor) screen.cursor.x else null, if (shape_cursor) screen.cursor.x else null,
); );
@ -1576,7 +1582,7 @@ pub fn rebuildCells(
// emulators, e.g. Alacritty, still render text decorations // emulators, e.g. Alacritty, still render text decorations
// and only make the text itself invisible. The decision // and only make the text itself invisible. The decision
// has been made here to match xterm's behavior for this. // 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; continue;
} }

View File

@ -18,7 +18,7 @@ const Allocator = std.mem.Allocator;
const log = std.log.scoped(.renderer_thread); const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 8; // 120 FPS 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 /// 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 /// 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: xev.Async,
draw_now_c: xev.Completion = .{}, draw_now_c: xev.Completion = .{},
/// The timer used for cursor blinking /// Timer that cursor and text blinking is built on
cursor_h: xev.Timer, blink_h: xev.Timer,
cursor_c: xev.Completion = .{}, blink_c: xev.Completion = .{},
cursor_c_cancel: 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. /// The surface we're rendering to.
surface: *apprt.Surface, surface: *apprt.Surface,
@ -88,6 +93,11 @@ flags: packed struct {
/// thread automatically. /// thread automatically.
cursor_blink_visible: bool = false, 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. /// This is true when the inspector is active.
has_inspector: bool = false, has_inspector: bool = false,
@ -145,9 +155,9 @@ pub fn init(
var draw_now = try xev.Async.init(); var draw_now = try xev.Async.init();
errdefer draw_now.deinit(); errdefer draw_now.deinit();
// Setup a timer for blinking the cursor // Setup a timer for blinking the cursor and text
var cursor_timer = try xev.Timer.init(); var blink_timer = try xev.Timer.init();
errdefer cursor_timer.deinit(); errdefer blink_timer.deinit();
// The mailbox for messaging this thread // The mailbox for messaging this thread
var mailbox = try Mailbox.create(alloc); var mailbox = try Mailbox.create(alloc);
@ -162,7 +172,7 @@ pub fn init(
.render_h = render_h, .render_h = render_h,
.draw_h = draw_h, .draw_h = draw_h,
.draw_now = draw_now, .draw_now = draw_now,
.cursor_h = cursor_timer, .blink_h = blink_timer,
.surface = surface, .surface = surface,
.renderer = renderer_impl, .renderer = renderer_impl,
.state = state, .state = state,
@ -179,7 +189,7 @@ pub fn deinit(self: *Thread) void {
self.render_h.deinit(); self.render_h.deinit();
self.draw_h.deinit(); self.draw_h.deinit();
self.draw_now.deinit(); self.draw_now.deinit();
self.cursor_h.deinit(); self.blink_h.deinit();
self.loop.deinit(); self.loop.deinit();
// Nothing can possibly access the mailbox anymore, destroy it. // 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. // Send an initial wakeup message so that we render right away.
try self.wakeup.notify(); try self.wakeup.notify();
// Start blinking the cursor. // Start the blinking timer.
self.cursor_h.run( self.blink_h.run(
&self.loop, &self.loop,
&self.cursor_c, &self.blink_c,
CURSOR_BLINK_INTERVAL, BLINK_INTERVAL,
Thread, Thread,
self, self,
cursorTimerCallback, blinkTimerCallback,
); );
// Start the draw timer // Start the draw timer
@ -357,52 +367,39 @@ fn drainMailbox(self: *Thread) !void {
self.stopDrawTimer(); self.stopDrawTimer();
} }
// If we're not focused, then we stop the cursor blink // If we're not focused, then we stop the blink
if (self.cursor_c.state() == .active and if (self.blink_c.state() == .active and
self.cursor_c_cancel.state() == .dead) self.blink_c_cancel.state() == .dead)
{ {
self.cursor_h.cancel( self.blink_h.cancel(
&self.loop, &self.loop,
&self.cursor_c, &self.blink_c,
&self.cursor_c_cancel, &self.blink_c_cancel,
void, void,
null, null,
cursorCancelCallback, blinkCancelCallback,
); );
} }
} else { } else {
// Start the draw timer // Start the draw timer
self.startDrawTimer(); self.startDrawTimer();
// If we're focused, we immediately show the cursor again // If we're focused, we immediately start blinking again
// and then restart the timer. self.blink_h.run(
if (self.cursor_c.state() != .active) { &self.loop,
self.flags.cursor_blink_visible = true; &self.blink_c,
self.cursor_h.run( BLINK_INTERVAL,
&self.loop, Thread,
&self.cursor_c, self,
CURSOR_BLINK_INTERVAL, blinkTimerCallback,
Thread, );
self,
cursorTimerCallback,
);
}
} }
}, },
.reset_cursor_blink => { .reset_cursor_blink => {
self.flags.cursor_blink_visible = true; self.flags.cursor_blink_visible = true;
if (self.cursor_c.state() == .active) { self.cursor_blink_active = true;
self.cursor_h.reset( self.cursor_blink_buffer = false;
&self.loop,
&self.cursor_c,
&self.cursor_c_cancel,
CURSOR_BLINK_INTERVAL,
Thread,
self,
cursorTimerCallback,
);
}
}, },
.font_grid => |grid| { .font_grid => |grid| {
@ -585,6 +582,7 @@ fn renderCallback(
t.surface, t.surface,
t.state, t.state,
t.flags.cursor_blink_visible, t.flags.cursor_blink_visible,
t.flags.text_blink_visible,
) catch |err| ) catch |err|
log.warn("error rendering err={}", .{err}); log.warn("error rendering err={}", .{err});
@ -594,7 +592,7 @@ fn renderCallback(
return .disarm; return .disarm;
} }
fn cursorTimerCallback( fn blinkTimerCallback(
self_: ?*Thread, self_: ?*Thread,
_: *xev.Loop, _: *xev.Loop,
_: *xev.Completion, _: *xev.Completion,
@ -616,14 +614,29 @@ fn cursorTimerCallback(
return .disarm; 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.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; return .disarm;
} }
fn cursorCancelCallback( fn blinkCancelCallback(
_: ?*void, _: ?*void,
_: *xev.Loop, _: *xev.Loop,
_: *xev.Completion, _: *xev.Completion,