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.
This commit is contained in:
Leah Amelia Chen
2024-09-02 20:16:24 +02:00
parent 7f22f15e5b
commit 7d150b050c

View File

@ -16,7 +16,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
@ -57,15 +57,15 @@ draw_now: xev.Async,
draw_now_c: xev.Completion = .{}, draw_now_c: xev.Completion = .{},
/// The timer used for text blinking. This timer will always run, uninterrupted by /// 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_h: xev.Timer,
blink_c: xev.Completion = .{}, blink_c: xev.Completion = .{},
blink_c_cancel: xev.Completion = .{}, blink_c_cancel: xev.Completion = .{},
/// The timer used for cursor blinking. This timer will get reset on user text input, /// The timer used to reset cursor blinking. When the cursor is set to always visible
/// ensuring the cursor will always remain visible while typing. /// (for example, while typing), this timer introduces a delay that synchronizes with
/// When the user stops typing, the timer will wait till the main blink timer fires, /// the main blink timer, to unset the always visible flag and allow the cursor to blink
/// ensuring that the cursor remains in sync with text blinking. /// in sync with the rest of the screen again.
cursor_h: xev.Timer, cursor_h: xev.Timer,
cursor_c: xev.Completion = .{}, cursor_c: xev.Completion = .{},
cursor_c_cancel: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{},
@ -95,10 +95,9 @@ flags: packed struct {
/// thread automatically. /// thread automatically.
blink_visible: bool = false, blink_visible: bool = false,
/// This is true when a blinking cursor should be visible and false /// Whether the cursor should be forced into staying visible regardless
/// when it should not be visible. This is toggled on a timer by the /// of blinking. Used when the user is typing.
/// thread automatically. cursor_always_visible: bool = false,
cursor_blink_visible: bool = false,
/// This is true when the inspector is active. /// This is true when the inspector is active.
has_inspector: bool = false, has_inspector: bool = false,
@ -242,19 +241,11 @@ fn threadMain_(self: *Thread) !void {
self.blink_h.run( self.blink_h.run(
&self.loop, &self.loop,
&self.blink_c, &self.blink_c,
CURSOR_BLINK_INTERVAL, BLINK_INTERVAL,
Thread, Thread,
self, self,
blinkTimerCallback, blinkTimerCallback,
); );
self.cursor_h.run(
&self.loop,
&self.cursor_c,
CURSOR_BLINK_INTERVAL,
Thread,
self,
cursorTimerCallback,
);
// Start the draw timer // Start the draw timer
self.startDrawTimer(); self.startDrawTimer();
@ -331,52 +322,51 @@ 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 make blinking cells visible
// and then restart the timer. // again and then restart the timer.
if (self.cursor_c.state() != .active) { if (self.blink_c.state() != .active) {
self.flags.cursor_blink_visible = true; self.flags.blink_visible = true;
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,
); );
} }
} }
}, },
.reset_cursor_blink => { .reset_cursor_blink => {
self.flags.cursor_blink_visible = true; self.flags.cursor_always_visible = true;
if (self.cursor_c.state() == .active) { self.cursor_h.reset(
self.cursor_h.cancel( &self.loop,
&self.loop, &self.cursor_c,
&self.cursor_c, &self.cursor_c_cancel,
&self.cursor_c_cancel, BLINK_INTERVAL,
void, Thread,
null, self,
cursorCancelCallback, resetCursorBlinkCallback,
); );
}
}, },
.font_grid => |grid| { .font_grid => |grid| {
@ -558,7 +548,7 @@ fn renderCallback(
t.surface, t.surface,
t.state, t.state,
t.flags.blink_visible, t.flags.blink_visible,
t.flags.cursor_blink_visible, t.flags.cursor_always_visible or t.flags.blink_visible,
) catch |err| ) catch |err|
log.warn("error rendering err={}", .{err}); log.warn("error rendering err={}", .{err});
@ -573,28 +563,6 @@ fn blinkTimerCallback(
_: *xev.Loop, _: *xev.Loop,
_: *xev.Completion, _: *xev.Completion,
r: xev.Timer.RunError!void, 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 { ) xev.CallbackAction {
_ = r catch |err| switch (err) { _ = r catch |err| switch (err) {
// This is sent when our timer is canceled. That's fine. // This is sent when our timer is canceled. That's fine.
@ -612,12 +580,14 @@ fn cursorTimerCallback(
return .disarm; return .disarm;
}; };
t.flags.cursor_blink_visible = !t.flags.blink_visible; t.flags.blink_visible = !t.flags.blink_visible;
// We intentionally don't call `t.wakeup.notify()` here to avoid a double redraw. t.wakeup.notify() catch {};
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,
@ -635,7 +605,7 @@ fn cursorCancelCallback(
error.Canceled => {}, // success error.Canceled => {}, // success
error.NotFound => {}, // completed before it could cancel error.NotFound => {}, // completed before it could cancel
else => { else => {
log.warn("error in cursor cancel callback err={}", .{err}); log.warn("error in blink cancel callback err={}", .{err});
unreachable; unreachable;
}, },
}; };
@ -643,6 +613,24 @@ fn cursorCancelCallback(
return .disarm; 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 { // fn prepFrameCallback(h: *libuv.Prepare) void {
// _ = h; // _ = h;
// //