fix: stage complete frames before suspending terminal presentation
This commit is contained in:
+70
-11
@@ -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:?}",
|
||||
|
||||
Reference in New Issue
Block a user