From 24291772def9555bafd40193e773a79927685543 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Mon, 8 Jul 2024 16:06:45 -0700 Subject: [PATCH] Added Window::on_close_requested Closes #161 --- CHANGELOG.md | 6 ++ examples/unsaved-changes.rs | 15 ++++ src/window.rs | 158 ++++++++++++++++++++++-------------- 3 files changed, 120 insertions(+), 59 deletions(-) create mode 100644 examples/unsaved-changes.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 93db39d..12b3074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 key press. - `AnimationRecorder::animate_mouse_button` is a new helper that animates a single mouse button press and release. +- `Window::on_close_requested` is a new function that allows providing a + callback that is invoked before the window is closed when the user or + operating system requests that a window is closed. If the callback returns + true, the window is allowed to be closed. If false is returned, the window + will remain open. This feature is most commonly used to prevent losing unsaved + changes. ## v0.3.0 (2024-05-12) diff --git a/examples/unsaved-changes.rs b/examples/unsaved-changes.rs new file mode 100644 index 0000000..b57198f --- /dev/null +++ b/examples/unsaved-changes.rs @@ -0,0 +1,15 @@ +use cushy::{ + value::{Dynamic, Source}, + widget::MakeWidget, + Run, +}; + +fn main() -> cushy::Result { + let has_unsaved_changes = Dynamic::new(true); + + "Prevent Closing" + .into_checkbox(has_unsaved_changes.clone()) + .into_window() + .on_close_requested(move |()| !has_unsaved_changes.get()) + .run() +} diff --git a/src/window.rs b/src/window.rs index 409564b..7c6ca8d 100644 --- a/src/window.rs +++ b/src/window.rs @@ -59,8 +59,8 @@ use crate::value::{ Destination, Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Source, Value, }; use crate::widget::{ - EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, WidgetId, WidgetInstance, - HANDLED, IGNORED, + Callback, EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, WidgetId, + WidgetInstance, HANDLED, IGNORED, }; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit}; @@ -260,6 +260,7 @@ pub struct RunningWindow { focused: Dynamic, occluded: Dynamic, inner_size: Dynamic>, + close_requested: Option>>>, } impl RunningWindow @@ -274,6 +275,7 @@ where focused: &Dynamic, occluded: &Dynamic, inner_size: &Dynamic>, + close_requested: &Option>>>, ) -> Self { Self { window, @@ -283,6 +285,7 @@ where focused: focused.clone(), occluded: occluded.clone(), inner_size: inner_size.clone(), + close_requested: close_requested.clone(), } } @@ -499,6 +502,7 @@ where occluded: Option>, focused: Option>, theme_mode: Option>, + close_requested: Option>, } impl Default for Window @@ -519,6 +523,62 @@ impl Window { { Self::new(widget.make_widget()) } +} + +impl Window +where + Behavior: WindowBehavior, +{ + /// Returns a new instance using `context` to initialize the window upon + /// opening. + pub fn new(context: Behavior::Context) -> Self { + Self::new_with_pending(context, PendingWindow::default()) + } + + fn new_with_pending(context: Behavior::Context, pending: PendingWindow) -> Self { + static EXECUTABLE_NAME: OnceLock = OnceLock::new(); + + let title = EXECUTABLE_NAME + .get_or_init(|| { + std::env::args_os() + .next() + .and_then(|path| { + Path::new(&path) + .file_name() + .and_then(OsStr::to_str) + .map(ToString::to_string) + }) + .unwrap_or_else(|| String::from("Cushy App")) + }) + .clone(); + Self { + pending, + title: Value::Constant(title), + attributes: WindowAttributes::default(), + on_closed: None, + context, + load_system_fonts: true, + theme: Value::default(), + occluded: None, + focused: None, + theme_mode: None, + inner_size: None, + serif_font_family: FontFamilyList::default(), + sans_serif_font_family: FontFamilyList::default(), + fantasy_font_family: FontFamilyList::default(), + monospace_font_family: FontFamilyList::default(), + cursive_font_family: FontFamilyList::default(), + fonts: { + let fonts = FontCollection::default(); + #[cfg(feature = "roboto-flex")] + fonts.push(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); + fonts + }, + multisample_count: NonZeroU32::new(4).assert("not 0"), + vsync: true, + close_requested: None, + } + } /// Sets `focused` to be the dynamic updated when this window's focus status /// is changed. @@ -604,6 +664,18 @@ impl Window { self } + /// Invokes `on_close_requested` when the window is requested to be closed. + /// + /// If the function returns true, the window is allowed to be closed, + /// otherwise the window remains open. + pub fn on_close_requested(mut self, on_close_requested: Function) -> Self + where + Function: FnMut(()) -> bool + Send + 'static, + { + self.close_requested = Some(Callback::new(on_close_requested)); + self + } + /// Sets the window's title. pub fn titled(mut self, title: impl IntoValue) -> Self { self.title = title.into_value(); @@ -611,61 +683,6 @@ impl Window { } } -impl Window -where - Behavior: WindowBehavior, -{ - /// Returns a new instance using `context` to initialize the window upon - /// opening. - pub fn new(context: Behavior::Context) -> Self { - Self::new_with_pending(context, PendingWindow::default()) - } - - fn new_with_pending(context: Behavior::Context, pending: PendingWindow) -> Self { - static EXECUTABLE_NAME: OnceLock = OnceLock::new(); - - let title = EXECUTABLE_NAME - .get_or_init(|| { - std::env::args_os() - .next() - .and_then(|path| { - Path::new(&path) - .file_name() - .and_then(OsStr::to_str) - .map(ToString::to_string) - }) - .unwrap_or_else(|| String::from("Cushy App")) - }) - .clone(); - Self { - pending, - title: Value::Constant(title), - attributes: WindowAttributes::default(), - on_closed: None, - context, - load_system_fonts: true, - theme: Value::default(), - occluded: None, - focused: None, - theme_mode: None, - inner_size: None, - serif_font_family: FontFamilyList::default(), - sans_serif_font_family: FontFamilyList::default(), - fantasy_font_family: FontFamilyList::default(), - monospace_font_family: FontFamilyList::default(), - cursive_font_family: FontFamilyList::default(), - fonts: { - let fonts = FontCollection::default(); - #[cfg(feature = "roboto-flex")] - fonts.push(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); - fonts - }, - multisample_count: NonZeroU32::new(4).assert("not 0"), - vsync: true, - } - } -} - impl Run for Window where Behavior: WindowBehavior, @@ -711,6 +728,7 @@ where cursive_font_family: self.cursive_font_family, vsync: self.vsync, multisample_count: self.multisample_count, + close_requested: self.close_requested.map(|cb| Arc::new(Mutex::new(cb))), }), }, )?; @@ -790,6 +808,7 @@ struct OpenWindow { cushy: Cushy, on_closed: Option, vsync: bool, + close_requested: Option>>>, } impl OpenWindow @@ -801,7 +820,11 @@ where behavior: &mut T, window: &mut RunningWindow>, ) -> bool { - *should_close |= behavior.close_requested(window); + *should_close |= behavior.close_requested(window) + && window + .close_requested + .as_mut() + .map_or(false, |close| close.lock().invoke(())); *should_close } @@ -1147,6 +1170,7 @@ where let theme = settings.theme.take().unwrap_or_default(); let inner_size = settings.inner_size.clone(); let on_closed = settings.on_closed.take(); + let close_requested = settings.close_requested.take(); let vsync = settings.vsync; inner_size.set(window.inner_size()); @@ -1204,6 +1228,7 @@ where cushy, on_closed, vsync, + close_requested, } } @@ -1234,6 +1259,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics); @@ -1332,6 +1358,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, )) { self.should_close = true; true @@ -1376,6 +1403,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let target = self.tree.focused_widget().unwrap_or(self.root.node_id); let Some(target) = self.tree.widget_from_node(target) else { @@ -1426,6 +1454,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let widget = self .tree @@ -1469,6 +1498,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let widget = self .tree @@ -1513,6 +1543,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let location = position.into(); @@ -1571,6 +1602,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); let mut context = EventContext::new( @@ -1609,6 +1641,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); match state { ElementState::Pressed => { @@ -1729,6 +1762,7 @@ where &settings.focused, &settings.occluded, &settings.inner_size, + &settings.close_requested, ); drop(settings); @@ -1815,6 +1849,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ), ) } @@ -1965,6 +2000,7 @@ where &self.focused, &self.occluded, &self.inner_size, + &self.close_requested, ); if self.behavior.close_requested(&mut window) { window.close(); @@ -2006,18 +2042,20 @@ pub(crate) struct CursorState { pub(crate) mod sealed { use std::cell::RefCell; use std::num::NonZeroU32; + use std::sync::Arc; use figures::units::UPx; use figures::{Point, Size}; use image::DynamicImage; use kludgine::Color; + use parking_lot::Mutex; use crate::app::Cushy; use crate::context::sealed::InvalidationStatus; use crate::fonts::FontCollection; use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; - use crate::widget::OnceCallback; + use crate::widget::{Callback, OnceCallback}; use crate::window::{ThemeMode, WindowAttributes}; pub struct Context { @@ -2045,6 +2083,7 @@ pub(crate) mod sealed { pub on_closed: Option, pub vsync: bool, pub multisample_count: NonZeroU32, + pub close_requested: Option>>>, } #[derive(Debug, Clone)] @@ -2571,6 +2610,7 @@ impl CushyWindowBuilder { on_closed: None, vsync: false, multisample_count: self.multisample_count, + close_requested: None, }, );