pub mod cursor;
pub mod cursor_slot;
pub mod diff_backend;
pub use cursor::CursorState;
pub use cursor_slot::CursorSlot;
pub use diff_backend::DiffBackend;
use ratatui::Terminal;
use ratatui::backend::Backend;
use crate::theme::{TerminalCaps, Theme};
use crate::widget::{FocusTarget, RenderCtx, RetainedTree};
pub fn draw_frame<B: Backend>(
term: &mut Terminal<B>,
tree: &mut RetainedTree,
cursor: &mut CursorState,
focus: FocusTarget,
theme: &Theme,
caps: &TerminalCaps,
) -> Result<FrameOutcome, B::Error> {
let prev_size = term.size()?;
term.autoresize()?;
let resized = term.size()? != prev_size;
if !tree.any_hash_changed() && !resized {
return Ok(FrameOutcome::Idle);
}
let want = {
let mut frame = term.get_frame();
let mut ctx = RenderCtx::new(&mut frame, theme, caps);
ctx.focus = focus;
tree.render(&mut ctx)
};
term.flush()?;
cursor.reconcile(want, term)?;
term.swap_buffers();
term.backend_mut().flush()?;
Ok(FrameOutcome::Rendered)
}
pub fn draw_frame_closure<B, F>(
term: &mut Terminal<B>,
cursor: &mut CursorState,
focus: FocusTarget,
theme: &Theme,
caps: &TerminalCaps,
render_fn: F,
) -> Result<FrameOutcome, B::Error>
where
B: Backend,
F: FnOnce(&mut RenderCtx<'_, '_>),
{
term.autoresize()?;
let want = {
let mut frame = term.get_frame();
let mut ctx = RenderCtx::new(&mut frame, theme, caps);
ctx.focus = focus;
render_fn(&mut ctx);
ctx.take_cursor_slot().resolve(None)
};
term.flush()?;
cursor.reconcile(want, term)?;
term.swap_buffers();
term.backend_mut().flush()?;
Ok(FrameOutcome::Rendered)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FrameOutcome {
#[default]
Idle,
Rendered,
}
#[cfg(test)]
mod draw_frame_tests {
use super::*;
use crate::widget::{RetainedTree, Text};
use ratatui::layout::Position;
use ratatui::{Terminal, backend::TestBackend};
fn make_term() -> Terminal<TestBackend> {
Terminal::new(TestBackend::new(40, 10)).unwrap()
}
#[test]
fn first_frame_is_rendered() {
let mut term = make_term();
let mut tree = RetainedTree::new(Box::new(Text::new("hello")));
let mut cursor = CursorState::new();
let theme = Theme::dark();
let caps = TerminalCaps::default();
let outcome = draw_frame(
&mut term,
&mut tree,
&mut cursor,
crate::widget::FocusTarget::None,
&theme,
&caps,
)
.unwrap();
assert_eq!(outcome, FrameOutcome::Rendered);
}
#[test]
fn second_frame_with_unchanged_hash_is_idle() {
let mut term = make_term();
let mut tree = RetainedTree::new(Box::new(Text::new("hello")));
let mut cursor = CursorState::new();
let theme = Theme::dark();
let caps = TerminalCaps::default();
let _ = draw_frame(
&mut term,
&mut tree,
&mut cursor,
crate::widget::FocusTarget::None,
&theme,
&caps,
)
.unwrap();
let outcome = draw_frame(
&mut term,
&mut tree,
&mut cursor,
crate::widget::FocusTarget::None,
&theme,
&caps,
)
.unwrap();
assert_eq!(outcome, FrameOutcome::Idle);
}
#[test]
fn draw_frame_closure_renders_via_fn() {
let mut term = make_term();
let mut cursor = CursorState::new();
let theme = Theme::dark();
let caps = TerminalCaps::default();
let mut rendered_with = None;
let outcome = draw_frame_closure(
&mut term,
&mut cursor,
crate::widget::FocusTarget::None,
&theme,
&caps,
|ctx| {
rendered_with = Some(ctx.area().width);
ctx.set_cursor(Position { x: 0, y: 0 });
},
)
.unwrap();
assert_eq!(outcome, FrameOutcome::Rendered);
assert_eq!(rendered_with, Some(40));
}
#[test]
fn draw_frame_closure_keeps_compiling_without_retained_tree() {
let mut term = make_term();
let mut cursor = CursorState::new();
let theme = Theme::dark();
let caps = TerminalCaps::default();
let outcome = draw_frame_closure(
&mut term,
&mut cursor,
crate::widget::FocusTarget::None,
&theme,
&caps,
|_ctx| {},
)
.unwrap();
assert_eq!(outcome, FrameOutcome::Rendered);
}
}