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 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,
|
||||||
void,
|
BLINK_INTERVAL,
|
||||||
null,
|
Thread,
|
||||||
cursorCancelCallback,
|
self,
|
||||||
|
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;
|
||||||
//
|
//
|
||||||
|
Reference in New Issue
Block a user