From 7f22f15e5b8be9437de10506d04bf1480c5b6bbd Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 2 Sep 2024 00:14:16 +0200 Subject: [PATCH] renderer: decouple text blink from cursor blink Coupling the text blink to the cursor blink leads to interesting problems, such as the text blink stopping when the user is typing, as the cursor is coded to stop blinking during typing. We now make the text blink on a separate timer that drives the cursor blink, so that text input events can easily cancel the cursor blink, waiting for it to be re-synchronized to the text blink. --- src/renderer/Metal.zig | 3 +- src/renderer/OpenGL.zig | 3 +- src/renderer/Thread.zig | 71 ++++++++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ae9868db5..991e7a298 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -873,6 +873,7 @@ pub fn updateFrame( surface: *apprt.Surface, state: *renderer.State, blink_visible: bool, + cursor_blink_visible: bool, ) !void { _ = surface; @@ -955,7 +956,7 @@ pub fn updateFrame( const cursor_style = renderer.cursorStyle( state, self.focused, - blink_visible, + cursor_blink_visible, ); // Get our preedit state diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 94216c3aa..1d96a17ae 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -705,6 +705,7 @@ pub fn updateFrame( surface: *apprt.Surface, state: *renderer.State, blink_visible: bool, + cursor_blink_visible: bool, ) !void { _ = surface; @@ -773,7 +774,7 @@ pub fn updateFrame( const cursor_style = renderer.cursorStyle( state, self.focused, - blink_visible, + cursor_blink_visible, ); // Get our preedit state diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index b3e54262d..a460b4594 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -56,7 +56,16 @@ draw_active: bool = false, draw_now: xev.Async, draw_now_c: xev.Completion = .{}, -/// The timer used for cursor blinking +/// 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. +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. cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, cursor_c_cancel: xev.Completion = .{}, @@ -81,6 +90,11 @@ app_mailbox: App.Mailbox, config: DerivedConfig, flags: packed struct { + /// 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. + 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. @@ -139,6 +153,10 @@ pub fn init( var draw_now = try xev.Async.init(); errdefer draw_now.deinit(); + // Setup a timer for blinking the text + var blink_timer = try xev.Timer.init(); + errdefer blink_timer.deinit(); + // Setup a timer for blinking the cursor var cursor_timer = try xev.Timer.init(); errdefer cursor_timer.deinit(); @@ -156,6 +174,7 @@ pub fn init( .render_h = render_h, .draw_h = draw_h, .draw_now = draw_now, + .blink_h = blink_timer, .cursor_h = cursor_timer, .surface = surface, .renderer = renderer_impl, @@ -173,6 +192,7 @@ pub fn deinit(self: *Thread) void { self.render_h.deinit(); self.draw_h.deinit(); self.draw_now.deinit(); + self.blink_h.deinit(); self.cursor_h.deinit(); self.loop.deinit(); @@ -218,7 +238,15 @@ fn threadMain_(self: *Thread) !void { // Send an initial wakeup message so that we render right away. try self.wakeup.notify(); - // Start blinking the cursor. + // Start blinking the cursor and the text on screen. + self.blink_h.run( + &self.loop, + &self.blink_c, + CURSOR_BLINK_INTERVAL, + Thread, + self, + blinkTimerCallback, + ); self.cursor_h.run( &self.loop, &self.cursor_c, @@ -338,15 +366,15 @@ fn drainMailbox(self: *Thread) !void { .reset_cursor_blink => { self.flags.cursor_blink_visible = true; + if (self.cursor_c.state() == .active) { - self.cursor_h.reset( + self.cursor_h.cancel( &self.loop, &self.cursor_c, &self.cursor_c_cancel, - CURSOR_BLINK_INTERVAL, - Thread, - self, - cursorTimerCallback, + void, + null, + cursorCancelCallback, ); } }, @@ -529,6 +557,7 @@ fn renderCallback( t.renderer.updateFrame( t.surface, t.state, + t.flags.blink_visible, t.flags.cursor_blink_visible, ) catch |err| log.warn("error rendering err={}", .{err}); @@ -539,6 +568,28 @@ fn renderCallback( return .disarm; } +fn blinkTimerCallback( + 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.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, @@ -561,10 +612,8 @@ fn cursorTimerCallback( return .disarm; }; - t.flags.cursor_blink_visible = !t.flags.cursor_blink_visible; - t.wakeup.notify() catch {}; - - t.cursor_h.run(&t.loop, &t.cursor_c, CURSOR_BLINK_INTERVAL, Thread, t, cursorTimerCallback); + t.flags.cursor_blink_visible = !t.flags.blink_visible; + // We intentionally don't call `t.wakeup.notify()` here to avoid a double redraw. return .disarm; }