From d6f25a54bab19bc95105cfe5415c41a6a9d007f2 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sat, 26 Sep 2026 21:45:03 +0000 Subject: [PATCH] Fade blossom replays back to the idle state (#48547) ## Why Clicking the welcome blossom replays its animation, but completion abruptly switches from full color to the dim idle frame. ## What changed Fade the blossom back to idle opacity over 400 ms after the replay finishes spinning. Keep scheduling redraws until the fade completes, then clear the replay state. ## Testing Extend the replay test with color snapshots at the start, midpoint, and end of the fade, and assertions that redraws stop and the original idle frame is restored. GitOrigin-RevId: ec3fafb6766533d9c221913e8a3be03b8a8036f7 --- codex-rs/tui/src/empty_state_animation.rs | 14 ++++-- .../tui/src/empty_state_animation_tests.rs | 46 +++++++++++++++++++ ...tion__tests__first_screen_replay_fade.snap | 8 ++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 codex-rs/tui/src/snapshots/codex_tui__empty_state_animation__tests__first_screen_replay_fade.snap diff --git a/codex-rs/tui/src/empty_state_animation.rs b/codex-rs/tui/src/empty_state_animation.rs index a51264be5f..1d57c87618 100644 --- a/codex-rs/tui/src/empty_state_animation.rs +++ b/codex-rs/tui/src/empty_state_animation.rs @@ -175,7 +175,12 @@ impl EmptyStateAnimation { let phase = self.spin_elapsed.min(sequence::SPIN_DURATION).as_secs_f64() / sequence::LOOP_SECONDS; let settling = !finished && static_mark && self.fade_elapsed < sequence::STATIC_FADE; - self.opacity = if finished { + self.opacity = if finished && self.replaying { + sequence::static_opacity( + self.spin_elapsed - sequence::SPIN_DURATION, + /*from*/ 1.0, + ) + } else if finished { 1.0 } else if static_mark { sequence::static_opacity(self.fade_elapsed, self.fade_from) @@ -188,7 +193,10 @@ impl EmptyStateAnimation { * sequence::progress(self.fade_elapsed, sequence::STATIC_FADE) as f32; } self.paint_frame(area, buffer, phase, self.opacity); - (!finished && (!static_mark || settling)).then_some(FRAME_INTERVAL) + ((!finished && (!static_mark || settling)) + || (self.replaying + && self.spin_elapsed < sequence::SPIN_DURATION + sequence::STATIC_FADE)) + .then_some(FRAME_INTERVAL) } /// Draw only in space the caller has cleared and owns. The centered blossom returns @@ -228,7 +236,7 @@ impl EmptyStateAnimation { if let Some(delay) = self.render_in(stage, buffer, Presentation::Animated) { return Some(delay); } - // Paint the idle pose over the final full-color frame in the same update. + // The replay has faded back to the idle pose and no longer needs redraws. self.replaying = false; } self.paint_frame(stage, buffer, SETTLED_BLOSSOM, sequence::STATIC_OPACITY); diff --git a/codex-rs/tui/src/empty_state_animation_tests.rs b/codex-rs/tui/src/empty_state_animation_tests.rs index 2769ea9ba0..496116a5e1 100644 --- a/codex-rs/tui/src/empty_state_animation_tests.rs +++ b/codex-rs/tui/src/empty_state_animation_tests.rs @@ -134,6 +134,52 @@ fn first_screen_single_click_replays_the_visible_blossom() { Some(FRAME_INTERVAL) ); assert_eq!(buffer, static_frame); + let finished_at = + animation.last_frame.unwrap() + sequence::SPIN_DURATION - animation.spin_elapsed; + let mut fade_frames = Vec::new(); + for elapsed in [ + Duration::ZERO, + sequence::STATIC_FADE / 2, + sequence::STATIC_FADE, + ] { + buffer.reset(); + crate::terminal_palette::with_test_default_colors( + crate::terminal_probe::DefaultColors { + fg: (210, 221, 235), + bg: (15, 20, 37), + }, + || { + assert_eq!( + animation.render_in_at( + stage, + &mut buffer, + Presentation::Animated, + finished_at + elapsed, + ), + (elapsed < sequence::STATIC_FADE).then_some(FRAME_INTERVAL) + ); + }, + ); + let painted = buffer + .content + .iter() + .find(|cell| cell.symbol() != " ") + .unwrap(); + fade_frames.push(format!( + "{}ms: {} {:?}", + elapsed.as_millis(), + painted.symbol(), + painted.fg, + )); + } + insta::assert_snapshot!("first_screen_replay_fade", fade_frames.join("\n")); + assert_eq!( + draw(&mut animation, &mut buffer, Some(ComposerState::Empty)), + None + ); + assert_eq!(buffer, static_frame); + assert!(!animation.replaying); + assert!(animation.handle_mouse(click)); assert_eq!( draw(&mut animation, &mut buffer, Some(ComposerState::Draft)), None diff --git a/codex-rs/tui/src/snapshots/codex_tui__empty_state_animation__tests__first_screen_replay_fade.snap b/codex-rs/tui/src/snapshots/codex_tui__empty_state_animation__tests__first_screen_replay_fade.snap new file mode 100644 index 0000000000..4e97439310 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__empty_state_animation__tests__first_screen_replay_fade.snap @@ -0,0 +1,8 @@ +--- +source: tui/src/empty_state_animation_tests.rs +assertion_line: 171 +expression: "fade_frames.join(\"\\n\")" +--- +0ms: ⣀ Rgb(195, 201, 206) +200ms: ⣀ Rgb(121, 126, 136) +400ms: ⣀ Rgb(47, 52, 67)