use crossterm::{
execute,
terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate},
};
use ratatui::{DefaultTerminal, layout::Rect};
use tokio::time::Instant;
use crate::tui::state::DashboardState;
use super::media_runtime::{
DashboardMediaRuntime, clear_image_surfaces_frame, draw_dashboard_frame,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct RedrawPlan {
pub(super) clear_stale_media: bool,
pub(super) synchronized: bool,
}
#[derive(Default)]
pub(super) struct DashboardRedrawState {
media_animation_pending: bool,
mouse_scroll_deadline: Option<Instant>,
}
impl DashboardRedrawState {
pub(super) fn request_mouse_scroll(&mut self, now: Instant) {
const FRAME_INTERVAL: std::time::Duration = std::time::Duration::from_millis(16);
self.mouse_scroll_deadline
.get_or_insert(now + FRAME_INTERVAL);
}
pub(super) fn mouse_scroll_deadline(&self) -> Option<Instant> {
self.mouse_scroll_deadline
}
pub(super) fn clear_mouse_scroll(&mut self) {
self.mouse_scroll_deadline = None;
}
pub(super) fn request_media_animation(&mut self) {
self.media_animation_pending = true;
}
pub(super) fn take_plan(&mut self, clear_stale_media: bool) -> RedrawPlan {
let media_animation_pending = std::mem::take(&mut self.media_animation_pending);
RedrawPlan {
clear_stale_media,
synchronized: clear_stale_media || media_animation_pending,
}
}
}
pub(super) fn draw_dashboard_transaction(
terminal: &mut DefaultTerminal,
state: &mut DashboardState,
media_runtime: &mut DashboardMediaRuntime,
last_frame_area: &mut Rect,
plan: RedrawPlan,
) -> std::io::Result<()> {
if plan.synchronized {
let _ = execute!(terminal.backend_mut(), BeginSynchronizedUpdate);
}
let draw_result = (|| -> std::io::Result<()> {
if plan.clear_stale_media {
terminal.draw(|frame| {
*last_frame_area = clear_image_surfaces_frame(frame, state, media_runtime);
})?;
}
terminal.draw(|frame| {
*last_frame_area = draw_dashboard_frame(frame, state, media_runtime);
})?;
Ok(())
})();
if plan.synchronized {
let _ = execute!(terminal.backend_mut(), EndSynchronizedUpdate);
}
draw_result
}
#[cfg(test)]
mod tests {
use super::{DashboardRedrawState, RedrawPlan};
use std::time::Duration;
use tokio::time::Instant;
#[test]
fn mouse_scroll_burst_keeps_one_frame_deadline() {
let started_at = Instant::now();
let mut state = DashboardRedrawState::default();
state.request_mouse_scroll(started_at);
let first_deadline = state
.mouse_scroll_deadline()
.expect("scroll requests a frame");
assert_eq!(first_deadline, started_at + Duration::from_millis(16));
for elapsed_ms in [2, 5, 10] {
state.request_mouse_scroll(started_at + Duration::from_millis(elapsed_ms));
assert_eq!(state.mouse_scroll_deadline(), Some(first_deadline));
}
state.clear_mouse_scroll();
assert_eq!(state.mouse_scroll_deadline(), None);
state.request_mouse_scroll(first_deadline);
assert!(
state
.mouse_scroll_deadline()
.expect("the next scroll requests a frame")
> first_deadline
);
}
#[test]
fn redraw_plan_synchronizes_media_animation_and_placement_cleanup() {
let cases = [
(
"static redraw",
false,
false,
RedrawPlan {
clear_stale_media: false,
synchronized: false,
},
),
(
"placement cleanup",
true,
false,
RedrawPlan {
clear_stale_media: true,
synchronized: true,
},
),
(
"media animation",
false,
true,
RedrawPlan {
clear_stale_media: false,
synchronized: true,
},
),
(
"animation with placement cleanup",
true,
true,
RedrawPlan {
clear_stale_media: true,
synchronized: true,
},
),
];
for (label, clear_stale_media, animated_media_advanced, expected) in cases {
let mut state = DashboardRedrawState::default();
if animated_media_advanced {
state.request_media_animation();
}
assert_eq!(state.take_plan(clear_stale_media), expected, "{label}");
assert_eq!(
state.take_plan(false),
RedrawPlan {
clear_stale_media: false,
synchronized: false,
},
"{label} should consume the pending animation redraw"
);
}
}
}