fix(ui): force resize-event size during post-resize draw (macOS Terminal.app, ConHost) (#993)

Some terminal emulators (macOS Terminal.app, Windows ConHost) briefly report stale dimensions via `crossterm::terminal::size()` after a resize. ratatui's `draw()` calls `autoresize()` internally, queries the backend, and shrinks the viewport back to the stale dimension — leaving the newly-expanded area filled with stale content from the previous frame (the duplicate-panel symptom users have reported).

The fix adds `force_size` / `clear_forced_size` to `ColorCompatBackend` and forces the resize-event size for the post-resize draw, then clears the forcing for subsequent frames. Same class of fix as #582 but covers an additional emulator family.

Thanks to @ArronAI007 for tracking down the autoresize→stale-size→short-buffer interaction.
This commit is contained in:
Arron
2026-05-08 01:44:12 +08:00
committed by GitHub
parent 379186d911
commit 81fe6de57a
2 changed files with 37 additions and 2 deletions
+18 -1
View File
@@ -21,6 +21,11 @@ pub(crate) struct ColorCompatBackend<W: Write> {
inner: CrosstermBackend<W>,
depth: ColorDepth,
palette_mode: PaletteMode,
/// During a resize event the terminal emulator may report stale dimensions
/// for a brief window (observed on macOS Terminal.app and Windows ConHost).
/// Forcing the expected size prevents ratatui's internal `autoresize` from
/// shrinking the viewport back to the stale dimension inside `draw()`.
forced_size: Option<Size>,
}
impl<W: Write> ColorCompatBackend<W> {
@@ -29,8 +34,17 @@ impl<W: Write> ColorCompatBackend<W> {
inner: CrosstermBackend::new(writer),
depth,
palette_mode,
forced_size: None,
}
}
pub(crate) fn force_size(&mut self, size: Size) {
self.forced_size = Some(size);
}
pub(crate) fn clear_forced_size(&mut self) {
self.forced_size = None;
}
}
impl<W: Write> Write for ColorCompatBackend<W> {
@@ -88,7 +102,10 @@ impl<W: Write> Backend for ColorCompatBackend<W> {
}
fn size(&self) -> io::Result<Size> {
self.inner.size()
match self.forced_size {
Some(size) => Ok(size),
None => self.inner.size(),
}
}
fn window_size(&mut self) -> io::Result<WindowSize> {
+19 -1
View File
@@ -18,7 +18,7 @@ use crossterm::{
};
use ratatui::{
Frame, Terminal,
layout::{Constraint, Direction, Layout, Rect},
layout::{Constraint, Direction, Layout, Rect, Size},
prelude::Widget,
style::Style,
text::Span,
@@ -1643,11 +1643,29 @@ async fn run_event_loop(
terminal.clear()?;
app.handle_resize(final_w, final_h);
// #macos-resize: some terminals (macOS Terminal.app, Windows
// ConHost) briefly report stale dimensions via
// `terminal::size()` after a resize. ratatui's `draw()` calls
// `autoresize()` internally, which queries the backend size;
// if it sees the old dimension it shrinks the viewport back,
// leaving the newly-expanded area filled with stale content
// from the previous frame (duplicate UI panels).
//
// We force the backend to report the resize-event size for
// this single draw so the buffer matches the real viewport.
{
let backend = terminal.backend_mut();
backend.force_size(Size::new(final_w, final_h));
}
// Draw immediately so the cleared screen gets repainted before
// any other events can interleave. Without this, the next
// iteration's draw can race against fast follow-up input and
// leave the user staring at a blank/partial frame.
terminal.draw(|f| render(f, app))?;
{
let backend = terminal.backend_mut();
backend.clear_forced_size();
}
app.needs_redraw = false;
continue;
}