fix: stage complete frames before suspending terminal presentation

This commit is contained in:
2026-10-10 01:54:11 +02:00
parent 6152ae6d36
commit 99028daa9d
5 changed files with 103 additions and 15 deletions
+70 -11
View File
@@ -241,31 +241,36 @@ impl BufferViewports {
}
fn draw_frame<W: io::Write>(
terminal: &mut Terminal<CrosstermBackend<W>>,
terminal: &mut Terminal<CrosstermBackend<Vec<u8>>>,
output: &mut W,
render: impl FnOnce(&mut Frame),
) -> io::Result<()> {
// Do not suspend the terminal's presentation while computing the next frame.
terminal.backend_mut().writer_mut().clear();
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
// Terminals without synchronized updates still hide the painting cursor.
let result = terminal
.hide_cursor()
.and_then(|()| terminal.draw(render).map(|_| ()));
let finish = execute!(terminal.backend_mut(), EndSynchronizedUpdate);
result.and(finish)
result.and(finish)?;
output.write_all(terminal.backend().writer())?;
output.flush()
}
/// Renders session state and translates terminal input; the session owns plugin
/// execution and retirement, so the adapter needs no lifecycle callback.
pub fn run(session: &mut EditorSession) -> io::Result<()> {
TerminalSession::run(|| {
let mut terminal: Terminal<CrosstermBackend<Stdout>> =
Terminal::new(CrosstermBackend::new(io::stdout()))?;
let mut output: Stdout = io::stdout();
let mut terminal = Terminal::new(CrosstermBackend::new(Vec::new()))?;
let mut viewports = BufferViewports::default();
let mut redraw = true;
let mut pending = None;
while !session.editor().should_quit() {
redraw |= session.poll();
if redraw {
draw_frame(&mut terminal, |frame| {
draw_frame(&mut terminal, &mut output, |frame| {
viewports.draw(frame, session.editor())
})?;
redraw = false;
@@ -5639,6 +5644,56 @@ end
);
}
#[test]
fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
use std::{cell::Cell, rc::Rc};
struct Output {
bytes: Vec<u8>,
synchronized: Rc<Cell<bool>>,
}
impl io::Write for Output {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
for command in self.bytes.split(|byte| *byte == b'\x1b') {
if command.starts_with(b"[?2026h") {
self.synchronized.set(true);
} else if command.starts_with(b"[?2026l") {
self.synchronized.set(false);
}
}
self.bytes.clear();
Ok(())
}
}
let synchronized = Rc::new(Cell::new(false));
let mut output = Output {
bytes: Vec::new(),
synchronized: Rc::clone(&synchronized),
};
let mut terminal = Terminal::with_options(
CrosstermBackend::new(Vec::new()),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
},
)
.unwrap();
for text in ["first frame", "second frame", "third frame"] {
draw_frame(&mut terminal, &mut output, |frame| {
assert!(
!synchronized.get(),
"terminal presentation suspended during frame computation"
);
frame.render_widget(Paragraph::new(text), frame.area());
})
.unwrap();
assert!(!synchronized.get());
}
}
#[test]
fn scrolling_never_paints_with_an_exposed_cursor_even_without_synchronized_updates() {
let mut editor = Editor::open(None, vec![]).unwrap();
@@ -5649,7 +5704,7 @@ end
.unwrap();
let mut output = Vec::new();
let mut terminal = Terminal::with_options(
CrosstermBackend::new(&mut output),
CrosstermBackend::new(Vec::new()),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
},
@@ -5658,7 +5713,10 @@ end
let mut viewports = BufferViewports::default();
for key in std::iter::repeat_n('j', 20).chain(std::iter::repeat_n('k', 20)) {
editor.handle(Input::Char(key));
draw_frame(&mut terminal, |frame| viewports.draw(frame, &editor)).unwrap();
draw_frame(&mut terminal, &mut output, |frame| {
viewports.draw(frame, &editor)
})
.unwrap();
}
drop(terminal);
let output = std::str::from_utf8(&output).unwrap();
@@ -5730,12 +5788,14 @@ end
"error" => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
"panic" => {
let mut terminal = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
CrosstermBackend::new(Vec::new()),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
},
)?;
draw_frame(&mut terminal, |_| panic::panic_any(73_u64))
draw_frame(&mut terminal, &mut io::stdout(), |_| {
panic::panic_any(73_u64)
})
}
"keys" => {
let mut editor = Editor::open(None, vec![]).unwrap();
@@ -5867,9 +5927,8 @@ end
);
}
if mode == "panic" {
let begin = before.find("\x1b[?2026h").unwrap();
let end = before.find("\x1b[?2026l").unwrap();
assert!(begin < end && end < before.find("CALLER_HOOK").unwrap());
assert!(end < before.find("CALLER_HOOK").unwrap());
assert!(
before.find("\x1b[?1049l").unwrap() < before.find("CALLER_HOOK").unwrap(),
"{before:?}",