supercode_frontend_tui/terminal/frame_requester.rs
1// Derived from OpenAI Codex: codex-rs/tui/src/tui/frame_requester.rs
2// Pinned source: 8604689ec5e3437eb79802d8d72249b7722fbf5b
3// Copyright 2025 OpenAI
4// Licensed under the Apache License, Version 2.0.
5// Modified by the Supercode contributors; see docs/legal/codex-frontend-extraction.toml.
6
7//! Frame draw scheduling utilities for the TUI.
8//!
9//! This module exposes [`FrameRequester`], a lightweight handle that widgets and
10//! background tasks can clone to request future redraws of the TUI.
11//!
12//! Internally it spawns a [`FrameScheduler`] task that coalesces many requests
13//! into a single notification on a broadcast channel used by the main TUI event
14//! loop. This keeps animations and status updates smooth without redrawing more
15//! often than necessary.
16//!
17//! This follows the actor-style design from
18//! [“Actors with Tokio”](https://ryhl.io/blog/actors-with-tokio/), with a
19//! dedicated scheduler task and lightweight request handles.
20
21use std::time::Duration;
22use std::time::Instant;
23
24use tokio::sync::broadcast;
25use tokio::sync::mpsc;
26
27use super::frame_rate_limiter::FrameRateLimiter;
28
29/// A requester for scheduling future frame draws on the TUI event loop.
30///
31/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
32/// multiple frame requests into a single draw operation.
33///
34/// Clones of this type can be freely shared across tasks to make it possible to trigger frame draws
35/// from anywhere in the TUI code.
36#[derive(Clone, Debug)]
37pub struct FrameRequester {
38 frame_schedule_tx: mpsc::UnboundedSender<Instant>,
39}
40
41impl FrameRequester {
42 /// Create a new FrameRequester and spawn its associated FrameScheduler task.
43 ///
44 /// The provided `draw_tx` is used to notify the TUI event loop of scheduled draws.
45 pub fn new(draw_tx: broadcast::Sender<()>) -> Self {
46 let (tx, rx) = mpsc::unbounded_channel();
47 let scheduler = FrameScheduler::new(rx, draw_tx);
48 tokio::spawn(scheduler.run());
49 Self {
50 frame_schedule_tx: tx,
51 }
52 }
53
54 /// Schedule a frame draw as soon as possible.
55 pub fn schedule_frame(&self) {
56 let _ = self.frame_schedule_tx.send(Instant::now());
57 }
58
59 /// Schedule a frame draw to occur after the specified duration.
60 pub fn schedule_frame_in(&self, dur: Duration) {
61 let _ = self.frame_schedule_tx.send(Instant::now() + dur);
62 }
63}
64
65/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
66///
67/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
68///
69/// To avoid wasted redraw work, draw notifications are clamped to a maximum of 120 FPS (see
70/// [`FrameRateLimiter`]).
71struct FrameScheduler {
72 receiver: mpsc::UnboundedReceiver<Instant>,
73 draw_tx: broadcast::Sender<()>,
74 rate_limiter: FrameRateLimiter,
75}
76
77impl FrameScheduler {
78 /// Create a new FrameScheduler with the provided receiver and draw notification sender.
79 fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
80 Self {
81 receiver,
82 draw_tx,
83 rate_limiter: FrameRateLimiter::default(),
84 }
85 }
86
87 /// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
88 ///
89 /// This method runs indefinitely until all senders are dropped. A single draw notification
90 /// is sent for multiple requests scheduled before the next draw deadline.
91 async fn run(mut self) {
92 const ONE_YEAR: Duration = Duration::from_secs(60 * 60 * 24 * 365);
93 let mut next_deadline: Option<Instant> = None;
94 loop {
95 let target = next_deadline.unwrap_or_else(|| Instant::now() + ONE_YEAR);
96 let deadline = tokio::time::sleep_until(target.into());
97 tokio::pin!(deadline);
98
99 tokio::select! {
100 draw_at = self.receiver.recv() => {
101 let Some(draw_at) = draw_at else {
102 // All senders dropped; exit the scheduler.
103 break
104 };
105 let draw_at = self.rate_limiter.clamp_deadline(draw_at);
106 next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
107
108 // Do not send a draw immediately here. By continuing the loop,
109 // we recompute the sleep target so the draw fires once via the
110 // sleep branch, coalescing multiple requests into a single draw.
111 continue;
112 }
113 _ = &mut deadline => {
114 if next_deadline.is_some() {
115 next_deadline = None;
116 self.rate_limiter.mark_emitted(target);
117 let _ = self.draw_tx.send(());
118 }
119 }
120 }
121 }
122 }
123}