diff --git a/CHANGELOG.md b/CHANGELOG.md index 674a71eb..d4674c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Replace the generic `From` impl for `BorderStyle` with `FromStr`. - Bump MSRV requirements: bump resolver and rust edition to 2024. - Fix `Dialog::important_area` to point at the focused button, so scroll views can reach it. +- Add `Printer::try_on_window`, a non-panicking alternative to `Printer::on_window` for when + the output window no longer fits the backend buffer (e.g. a terminal resize race). ## cursive-core 0.4.7 diff --git a/cursive-core/src/printer.rs b/cursive-core/src/printer.rs index 832355d0..3a535c32 100644 --- a/cursive-core/src/printer.rs +++ b/cursive-core/src/printer.rs @@ -299,16 +299,36 @@ impl<'a, 'b> Printer<'a, 'b> { } } + /// Calls a closure on the output window for this printer, if it fits in the backend. + /// + /// Returns `None` without calling `f` if the current output window no longer fits + /// in the backend buffer -- for instance if the terminal was resized smaller between + /// the last layout pass and this draw. This mirrors [`Self::on_window`], but degrades + /// gracefully instead of panicking: the caller can simply skip this frame's windowed + /// draw and let the next layout pass correct the sizes. + pub fn try_on_window(&self, f: F) -> Option + where + F: FnOnce(&mut Window<'_>) -> R, + { + let mut buffer = self.buffer.write(); + let mut window = buffer.window(self.output_window())?; + Some(f(&mut window)) + } + /// Calls a closure on the output window for this printer. + /// + /// # Panics + /// + /// Panics if the current output window doesn't fit in the backend buffer, which can + /// happen if the terminal is resized between the layout pass and the draw pass. Use + /// [`Self::try_on_window`] instead if you'd rather degrade gracefully (skip the draw + /// for this frame) than panic in that case. pub fn on_window(&self, f: F) -> R where F: FnOnce(&mut Window<'_>) -> R, { - let mut buffer = self.buffer.write(); - let mut window = buffer - .window(self.output_window()) - .expect("printer size exceeds backend size"); - f(&mut window) + self.try_on_window(f) + .expect("printer size exceeds backend size") } /// Prints a line using the given character. @@ -755,3 +775,61 @@ impl<'a, 'b> Printer<'a, 'b> { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "printer size exceeds backend size")] + fn on_window_panics_when_output_window_exceeds_backend() { + // Simulate a terminal shrinking between the layout pass and the draw + // pass: the printer was laid out for a 10x10 area, but the backend + // buffer has since shrunk to 5x5. + let mut backend_buffer = PrintBuffer::new(); + backend_buffer.resize(Vec2::new(5, 5)); + let backend_buffer = RwLock::new(backend_buffer); + let theme = Theme::default(); + + let printer = Printer::new((10, 10), &theme, &backend_buffer); + + printer.on_window(|_window| ()); + } + + #[test] + fn try_on_window_degrades_gracefully_when_output_window_exceeds_backend() { + // Same stale-size race as above, but using the non-panicking variant: the + // windowed draw should simply be skipped for this frame. + let mut backend_buffer = PrintBuffer::new(); + backend_buffer.resize(Vec2::new(5, 5)); + let backend_buffer = RwLock::new(backend_buffer); + let theme = Theme::default(); + + let printer = Printer::new((10, 10), &theme, &backend_buffer); + + let mut called = false; + let result = printer.try_on_window(|_window| { + called = true; + }); + + assert_eq!(result, None); + assert!( + !called, + "the closure should not run when the window doesn't fit" + ); + } + + #[test] + fn try_on_window_runs_closure_when_output_window_fits_backend() { + let mut backend_buffer = PrintBuffer::new(); + backend_buffer.resize(Vec2::new(10, 10)); + let backend_buffer = RwLock::new(backend_buffer); + let theme = Theme::default(); + + let printer = Printer::new((10, 10), &theme, &backend_buffer); + + let result = printer.try_on_window(|window| window.size()); + + assert_eq!(result, Some(Vec2::new(10, 10))); + } +} diff --git a/cursive-core/src/views/gradient_view.rs b/cursive-core/src/views/gradient_view.rs index 9553aead..5dee8280 100644 --- a/cursive-core/src/views/gradient_view.rs +++ b/cursive-core/src/views/gradient_view.rs @@ -112,7 +112,10 @@ where self.view.draw(printer); let buffer_size = printer.buffer_size(); - printer.on_window(|window| { + // Use the non-panicking variant: if the backend was resized smaller between the + // layout pass and this draw, just skip the gradient overlay for this frame instead + // of crashing the whole app (see #885). The next layout pass will correct the sizes. + printer.try_on_window(|window| { let viewport = window.viewport(); for y in 0..viewport.height() { for x in 0..viewport.width() {