mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
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:
@ -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.cursor_h.reset(
|
||||
&self.loop,
|
||||
&self.cursor_c,
|
||||
&self.cursor_c_cancel,
|
||||
void,
|
||||
null,
|
||||
cursorCancelCallback,
|
||||
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;
|
||||
//
|
||||
|
Reference in New Issue
Block a user