diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 0a789085..78626b91 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -11,7 +11,7 @@ use crossterm::{ Attribute as CAttribute, Color as CColor, Colors, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor, }, - terminal::{self, Clear, ClearType}, + terminal::{self, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate}, Command, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; @@ -291,7 +291,7 @@ where } fn hide_cursor(&mut self) -> io::Result<()> { - execute!(self.buffer, Hide) + queue!(self.buffer, Hide) } fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { @@ -301,15 +301,23 @@ where CursorKind::Underline => SetCursorStyle::SteadyUnderScore, CursorKind::Hidden => unreachable!(), }; - execute!(self.buffer, Show, shape) + queue!(self.buffer, Show, shape) } fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - execute!(self.buffer, MoveTo(x, y)) + queue!(self.buffer, MoveTo(x, y)) } fn clear(&mut self) -> io::Result<()> { - execute!(self.buffer, Clear(ClearType::All)) + queue!(self.buffer, Clear(ClearType::All)) + } + + fn start_sync(&mut self) -> io::Result<()> { + queue!(self.buffer, BeginSynchronizedUpdate) + } + + fn end_sync(&mut self) -> io::Result<()> { + queue!(self.buffer, EndSynchronizedUpdate) } fn size(&self) -> io::Result { diff --git a/helix-tui/src/backend/mod.rs b/helix-tui/src/backend/mod.rs index 2810477d..87da8bb8 100644 --- a/helix-tui/src/backend/mod.rs +++ b/helix-tui/src/backend/mod.rs @@ -42,6 +42,12 @@ pub trait Backend { fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>; /// Clears the terminal fn clear(&mut self) -> Result<(), io::Error>; + /// Begins a synchronized-output frame (if the terminal supports it), so the + /// draw and cursor updates between `start_sync` and `end_sync` present as one + /// frame instead of flickering. + fn start_sync(&mut self) -> Result<(), io::Error>; + /// Ends the synchronized-output frame opened by `start_sync`. + fn end_sync(&mut self) -> Result<(), io::Error>; /// Gets the size of the terminal in cells fn size(&self) -> Result; /// Flushes the terminal buffer diff --git a/helix-tui/src/backend/termina.rs b/helix-tui/src/backend/termina.rs index a5850edd..7c39cc96 100644 --- a/helix-tui/src/backend/termina.rs +++ b/helix-tui/src/backend/termina.rs @@ -474,8 +474,6 @@ impl Backend for TerminaBackend { where I: Iterator, { - self.start_synchronized_render()?; - let mut fg = Color::Reset; let mut bg = Color::Reset; let mut underline_color = Color::Reset; @@ -553,14 +551,11 @@ impl Backend for TerminaBackend { write!(self.terminal, "{}", Csi::Sgr(csi::Sgr::Reset))?; - self.end_sychronized_render()?; - Ok(()) } fn hide_cursor(&mut self) -> io::Result<()> { - write!(self.terminal, "{}", decreset!(ShowCursor))?; - self.flush() + write!(self.terminal, "{}", decreset!(ShowCursor)) } fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { @@ -575,8 +570,7 @@ impl Backend for TerminaBackend { "{}{}", decset!(ShowCursor), Csi::Cursor(csi::Cursor::CursorStyle(style)), - )?; - self.flush() + ) } fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { @@ -586,18 +580,23 @@ impl Backend for TerminaBackend { self.terminal, "{}", Csi::Cursor(csi::Cursor::Position { line, col }) - )?; - self.flush() + ) } fn clear(&mut self) -> io::Result<()> { - self.start_synchronized_render()?; write!( self.terminal, "{}", Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)) - )?; - self.flush() + ) + } + + fn start_sync(&mut self) -> io::Result<()> { + self.start_synchronized_render() + } + + fn end_sync(&mut self) -> io::Result<()> { + self.end_sychronized_render() } fn size(&self) -> io::Result { diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs index 0c45e455..4fae2030 100644 --- a/helix-tui/src/backend/test.rs +++ b/helix-tui/src/backend/test.rs @@ -149,6 +149,14 @@ impl Backend for TestBackend { Ok(()) } + fn start_sync(&mut self) -> Result<(), io::Error> { + Ok(()) + } + + fn end_sync(&mut self) -> Result<(), io::Error> { + Ok(()) + } + fn size(&self) -> Result { Ok(Rect::new(0, 0, self.width, self.height)) } diff --git a/helix-tui/src/terminal.rs b/helix-tui/src/terminal.rs index 5e4007fc..20287c18 100644 --- a/helix-tui/src/terminal.rs +++ b/helix-tui/src/terminal.rs @@ -71,6 +71,9 @@ where cursor_kind: CursorKind, /// Viewport viewport: Viewport, + /// Set to request a full clear. The erase is deferred to the next `flush` so it is emitted + /// inside the same synchronized-output frame as the repaint to avoid painting blank frames + force_clear: bool, } /// Default terminal size: 80 columns, 24 lines @@ -111,6 +114,7 @@ where current: 0, cursor_kind: CursorKind::Block, viewport: options.viewport, + force_clear: false, }) } @@ -149,6 +153,10 @@ where /// Obtains a difference between the previous and the current buffer and passes it to the /// current backend for drawing. pub fn flush(&mut self) -> io::Result<()> { + if self.force_clear { + self.backend.clear()?; + self.force_clear = false; + } let previous_buffer = &self.buffers[1 - self.current]; let current_buffer = &self.buffers[self.current]; let updates = previous_buffer.diff(current_buffer); @@ -157,7 +165,6 @@ where /// Updates the Terminal so that internal buffers match the requested size. Requested size will /// be saved so the size can remain consistent when rendering. - /// This leads to a full clear of the screen. pub fn resize(&mut self, area: Rect) -> io::Result<()> { self.buffers[self.current].resize(area); self.buffers[1 - self.current].resize(area); @@ -192,6 +199,9 @@ where // // Terminal. Thus, we're taking the important data out of the Frame and dropping it. // let cursor_position = frame.cursor_position; + // One synchronized frame for the whole draw + self.backend.start_sync()?; + // Draw to stdout self.flush()?; @@ -204,6 +214,8 @@ where kind => self.show_cursor(kind)?, } + self.backend.end_sync()?; + // Swap buffers self.buffers[1 - self.current].reset(); self.current = 1 - self.current; @@ -235,8 +247,11 @@ where } /// Clear the terminal and force a full redraw on the next draw call. + /// + /// The physical erase is deferred to the next `flush` so it shares a + /// synchronized frame with the repaint. pub fn clear(&mut self) -> io::Result<()> { - self.backend.clear()?; + self.force_clear = true; // Reset the back buffer to make sure the next update will redraw everything. self.buffers[1 - self.current].reset(); Ok(())