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 99a7e5bb..c4af9d2a 100644 --- a/cursive-core/src/printer.rs +++ b/cursive-core/src/printer.rs @@ -330,16 +330,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. @@ -826,6 +846,39 @@ mod tests { ); } + #[test] + #[should_panic(expected = "printer size exceeds backend size")] + fn on_window_panics_when_backend_shrinks_after_printer_creation() { + // Simulate a terminal shrinking between the layout pass and the draw + // pass: the printer was built for a 10x10 buffer, which has since + // shrunk to 5x5. + let (theme, buffer) = buffer_10x10(); + let printer = Printer::new((10, 10), &theme, &buffer); + buffer.write().resize(Vec2::new(5, 5)); + + printer.on_window(|_window| ()); + } + + #[test] + fn try_on_window_degrades_gracefully_when_backend_shrinks() { + // Same stale-size race as above, but using the non-panicking variant: + // the windowed draw is skipped for this frame. + let (theme, buffer) = buffer_10x10(); + let printer = Printer::new((10, 10), &theme, &buffer); + buffer.write().resize(Vec2::new(5, 5)); + + 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 clamped_printer_runs_on_window_on_empty_viewport() { let (theme, buffer) = buffer_10x10(); @@ -867,4 +920,14 @@ mod tests { let layout = FixedLayout::new().child(Rect::from_size((50, 50), (5, 5)), WindowUser); layout.draw(&printer); } + + #[test] + fn try_on_window_runs_closure_when_output_window_fits_backend() { + let (theme, buffer) = buffer_10x10(); + let printer = Printer::new((10, 10), &theme, &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 c46f3a27..87ca145d 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() {