Centralize TUI loading glyphs and frame scheduling (#50112)

## What changed

Move the voice connection spinner into shared helpers in `codex-rs/tui/src/motion.rs`, keeping its 100 ms animation cadence and static reduced-motion glyph. The helpers handle frame scheduling, and `loading_glyph_with_delay` supports hiding the animated spinner until a specified delay expires.

GitOrigin-RevId: c4ebe5e6a33239999aab2f4432c9145432c8f4fd
This commit is contained in:
Ian MacLeod
2026-10-01 22:17:51 +00:00
committed by copyberry
parent 6ece7bfc21
commit a0a7a63002
2 changed files with 42 additions and 19 deletions
+8 -19
View File
@@ -4,6 +4,7 @@
use crate::key_hint::ShortcutHint;
use crate::motion::MotionMode;
use crate::motion::loading_glyph;
use crate::render::renderable::Renderable;
use crate::tui::FrameRequester;
use ratatui::buffer::Buffer;
@@ -15,7 +16,6 @@ use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use std::time::Duration;
use std::time::Instant;
use unicode_width::UnicodeWidthStr;
@@ -61,16 +61,6 @@ impl VoiceStrip {
}
}
pub(super) fn loading_glyph(started_at: Instant, mode: MotionMode) -> &'static str {
const FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
if mode == MotionMode::Reduced {
"◌"
} else {
let frame = started_at.elapsed().as_millis() / 100;
FRAMES[usize::try_from(frame).unwrap_or_default() % FRAMES.len()]
}
}
impl Renderable for VoiceStrip {
fn desired_height(&self, width: u16) -> u16 {
if width == 0 {
@@ -92,14 +82,13 @@ impl Renderable for VoiceStrip {
let connecting = self.state.phase == VoiceStripPhase::Connecting;
let mode =
MotionMode::from_animations_enabled(self.state.animations && self.state.progress);
if connecting && mode == MotionMode::Animated {
self.frame_requester
.schedule_frame_in(Duration::from_millis(100));
}
let marker = if connecting && self.state.microphone_live && !self.state.microphone_muted {
loading_glyph(self.started_at, mode).red().bold()
} else if connecting {
loading_glyph(self.started_at, mode).cyan()
let marker = if connecting {
let glyph = loading_glyph(self.started_at, mode, &self.frame_requester);
if self.state.microphone_live && !self.state.microphone_muted {
glyph.red().bold()
} else {
glyph.cyan()
}
} else if self.state.microphone_live && !self.state.microphone_muted {
"●".red().bold()
} else {
+34
View File
@@ -3,8 +3,10 @@
//! Callers choose an explicit reduced-motion fallback here instead of reaching
//! directly for time-varying spinner or shimmer helpers.
use std::time::Duration;
use std::time::Instant;
use crate::tui::FrameRequester;
use ratatui::style::Stylize;
use ratatui::text::Span;
@@ -35,6 +37,38 @@ pub(crate) enum ReducedMotionIndicator {
StaticBullet,
}
/// Show the current loading glyph immediately and schedule the next frame.
pub(crate) fn loading_glyph(
started_at: Instant,
mode: MotionMode,
requester: &FrameRequester,
) -> &'static str {
loading_glyph_with_delay(started_at, Duration::ZERO, mode, requester)
}
/// Show nothing during the animation delay, or a static glyph in reduced motion.
/// Schedule the frame that makes the spinner visible and each animation frame.
pub(crate) fn loading_glyph_with_delay(
started_at: Instant,
delay: Duration,
mode: MotionMode,
requester: &FrameRequester,
) -> &'static str {
const FRAME_DURATION: Duration = Duration::from_millis(100);
const FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
if mode == MotionMode::Reduced {
return "◌";
}
let elapsed = started_at.elapsed();
if elapsed < delay {
requester.schedule_frame_in(delay - elapsed);
return "";
}
requester.schedule_frame_in(FRAME_DURATION);
let frame = elapsed.as_millis() / FRAME_DURATION.as_millis();
FRAMES[usize::try_from(frame).unwrap_or_default() % FRAMES.len()]
}
pub(crate) fn activity_indicator(
start_time: Option<Instant>,
motion_mode: MotionMode,