mirror of
https://github.com/danbulant/cushy
synced 2026-07-09 13:10:57 +00:00
parent
6a1f7f5462
commit
24291772de
3 changed files with 120 additions and 59 deletions
|
|
@ -30,6 +30,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
key press.
|
key press.
|
||||||
- `AnimationRecorder::animate_mouse_button` is a new helper that animates a
|
- `AnimationRecorder::animate_mouse_button` is a new helper that animates a
|
||||||
single mouse button press and release.
|
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)
|
## v0.3.0 (2024-05-12)
|
||||||
|
|
||||||
|
|
|
||||||
15
examples/unsaved-changes.rs
Normal file
15
examples/unsaved-changes.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
158
src/window.rs
158
src/window.rs
|
|
@ -59,8 +59,8 @@ use crate::value::{
|
||||||
Destination, Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Source, Value,
|
Destination, Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Source, Value,
|
||||||
};
|
};
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, WidgetId, WidgetInstance,
|
Callback, EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, WidgetId,
|
||||||
HANDLED, IGNORED,
|
WidgetInstance, HANDLED, IGNORED,
|
||||||
};
|
};
|
||||||
use crate::window::sealed::WindowCommand;
|
use crate::window::sealed::WindowCommand;
|
||||||
use crate::{initialize_tracing, ConstraintLimit};
|
use crate::{initialize_tracing, ConstraintLimit};
|
||||||
|
|
@ -260,6 +260,7 @@ pub struct RunningWindow<W> {
|
||||||
focused: Dynamic<bool>,
|
focused: Dynamic<bool>,
|
||||||
occluded: Dynamic<bool>,
|
occluded: Dynamic<bool>,
|
||||||
inner_size: Dynamic<Size<UPx>>,
|
inner_size: Dynamic<Size<UPx>>,
|
||||||
|
close_requested: Option<Arc<Mutex<Callback<(), bool>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W> RunningWindow<W>
|
impl<W> RunningWindow<W>
|
||||||
|
|
@ -274,6 +275,7 @@ where
|
||||||
focused: &Dynamic<bool>,
|
focused: &Dynamic<bool>,
|
||||||
occluded: &Dynamic<bool>,
|
occluded: &Dynamic<bool>,
|
||||||
inner_size: &Dynamic<Size<UPx>>,
|
inner_size: &Dynamic<Size<UPx>>,
|
||||||
|
close_requested: &Option<Arc<Mutex<Callback<(), bool>>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
window,
|
window,
|
||||||
|
|
@ -283,6 +285,7 @@ where
|
||||||
focused: focused.clone(),
|
focused: focused.clone(),
|
||||||
occluded: occluded.clone(),
|
occluded: occluded.clone(),
|
||||||
inner_size: inner_size.clone(),
|
inner_size: inner_size.clone(),
|
||||||
|
close_requested: close_requested.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,6 +502,7 @@ where
|
||||||
occluded: Option<Dynamic<bool>>,
|
occluded: Option<Dynamic<bool>>,
|
||||||
focused: Option<Dynamic<bool>>,
|
focused: Option<Dynamic<bool>>,
|
||||||
theme_mode: Option<Value<ThemeMode>>,
|
theme_mode: Option<Value<ThemeMode>>,
|
||||||
|
close_requested: Option<Callback<(), bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Behavior> Default for Window<Behavior>
|
impl<Behavior> Default for Window<Behavior>
|
||||||
|
|
@ -519,6 +523,62 @@ impl Window<WidgetInstance> {
|
||||||
{
|
{
|
||||||
Self::new(widget.make_widget())
|
Self::new(widget.make_widget())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Behavior> Window<Behavior>
|
||||||
|
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<String> = 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
|
/// Sets `focused` to be the dynamic updated when this window's focus status
|
||||||
/// is changed.
|
/// is changed.
|
||||||
|
|
@ -604,6 +664,18 @@ impl Window<WidgetInstance> {
|
||||||
self
|
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<Function>(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.
|
/// Sets the window's title.
|
||||||
pub fn titled(mut self, title: impl IntoValue<String>) -> Self {
|
pub fn titled(mut self, title: impl IntoValue<String>) -> Self {
|
||||||
self.title = title.into_value();
|
self.title = title.into_value();
|
||||||
|
|
@ -611,61 +683,6 @@ impl Window<WidgetInstance> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Behavior> Window<Behavior>
|
|
||||||
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<String> = 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<Behavior> Run for Window<Behavior>
|
impl<Behavior> Run for Window<Behavior>
|
||||||
where
|
where
|
||||||
Behavior: WindowBehavior,
|
Behavior: WindowBehavior,
|
||||||
|
|
@ -711,6 +728,7 @@ where
|
||||||
cursive_font_family: self.cursive_font_family,
|
cursive_font_family: self.cursive_font_family,
|
||||||
vsync: self.vsync,
|
vsync: self.vsync,
|
||||||
multisample_count: self.multisample_count,
|
multisample_count: self.multisample_count,
|
||||||
|
close_requested: self.close_requested.map(|cb| Arc::new(Mutex::new(cb))),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -790,6 +808,7 @@ struct OpenWindow<T> {
|
||||||
cushy: Cushy,
|
cushy: Cushy,
|
||||||
on_closed: Option<OnceCallback>,
|
on_closed: Option<OnceCallback>,
|
||||||
vsync: bool,
|
vsync: bool,
|
||||||
|
close_requested: Option<Arc<Mutex<Callback<(), bool>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> OpenWindow<T>
|
impl<T> OpenWindow<T>
|
||||||
|
|
@ -801,7 +820,11 @@ where
|
||||||
behavior: &mut T,
|
behavior: &mut T,
|
||||||
window: &mut RunningWindow<kludgine::app::Window<'_, WindowCommand>>,
|
window: &mut RunningWindow<kludgine::app::Window<'_, WindowCommand>>,
|
||||||
) -> bool {
|
) -> 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
|
*should_close
|
||||||
}
|
}
|
||||||
|
|
@ -1147,6 +1170,7 @@ where
|
||||||
let theme = settings.theme.take().unwrap_or_default();
|
let theme = settings.theme.take().unwrap_or_default();
|
||||||
let inner_size = settings.inner_size.clone();
|
let inner_size = settings.inner_size.clone();
|
||||||
let on_closed = settings.on_closed.take();
|
let on_closed = settings.on_closed.take();
|
||||||
|
let close_requested = settings.close_requested.take();
|
||||||
let vsync = settings.vsync;
|
let vsync = settings.vsync;
|
||||||
|
|
||||||
inner_size.set(window.inner_size());
|
inner_size.set(window.inner_size());
|
||||||
|
|
@ -1204,6 +1228,7 @@ where
|
||||||
cushy,
|
cushy,
|
||||||
on_closed,
|
on_closed,
|
||||||
vsync,
|
vsync,
|
||||||
|
close_requested,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1234,6 +1259,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics);
|
let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics);
|
||||||
|
|
||||||
|
|
@ -1332,6 +1358,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
)) {
|
)) {
|
||||||
self.should_close = true;
|
self.should_close = true;
|
||||||
true
|
true
|
||||||
|
|
@ -1376,6 +1403,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
let target = self.tree.focused_widget().unwrap_or(self.root.node_id);
|
let target = self.tree.focused_widget().unwrap_or(self.root.node_id);
|
||||||
let Some(target) = self.tree.widget_from_node(target) else {
|
let Some(target) = self.tree.widget_from_node(target) else {
|
||||||
|
|
@ -1426,6 +1454,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
let widget = self
|
let widget = self
|
||||||
.tree
|
.tree
|
||||||
|
|
@ -1469,6 +1498,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
let widget = self
|
let widget = self
|
||||||
.tree
|
.tree
|
||||||
|
|
@ -1513,6 +1543,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
|
|
||||||
let location = position.into();
|
let location = position.into();
|
||||||
|
|
@ -1571,6 +1602,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut context = EventContext::new(
|
let mut context = EventContext::new(
|
||||||
|
|
@ -1609,6 +1641,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
match state {
|
match state {
|
||||||
ElementState::Pressed => {
|
ElementState::Pressed => {
|
||||||
|
|
@ -1729,6 +1762,7 @@ where
|
||||||
&settings.focused,
|
&settings.focused,
|
||||||
&settings.occluded,
|
&settings.occluded,
|
||||||
&settings.inner_size,
|
&settings.inner_size,
|
||||||
|
&settings.close_requested,
|
||||||
);
|
);
|
||||||
drop(settings);
|
drop(settings);
|
||||||
|
|
||||||
|
|
@ -1815,6 +1849,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1965,6 +2000,7 @@ where
|
||||||
&self.focused,
|
&self.focused,
|
||||||
&self.occluded,
|
&self.occluded,
|
||||||
&self.inner_size,
|
&self.inner_size,
|
||||||
|
&self.close_requested,
|
||||||
);
|
);
|
||||||
if self.behavior.close_requested(&mut window) {
|
if self.behavior.close_requested(&mut window) {
|
||||||
window.close();
|
window.close();
|
||||||
|
|
@ -2006,18 +2042,20 @@ pub(crate) struct CursorState {
|
||||||
pub(crate) mod sealed {
|
pub(crate) mod sealed {
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use figures::units::UPx;
|
use figures::units::UPx;
|
||||||
use figures::{Point, Size};
|
use figures::{Point, Size};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
use crate::app::Cushy;
|
use crate::app::Cushy;
|
||||||
use crate::context::sealed::InvalidationStatus;
|
use crate::context::sealed::InvalidationStatus;
|
||||||
use crate::fonts::FontCollection;
|
use crate::fonts::FontCollection;
|
||||||
use crate::styles::{FontFamilyList, ThemePair};
|
use crate::styles::{FontFamilyList, ThemePair};
|
||||||
use crate::value::{Dynamic, Value};
|
use crate::value::{Dynamic, Value};
|
||||||
use crate::widget::OnceCallback;
|
use crate::widget::{Callback, OnceCallback};
|
||||||
use crate::window::{ThemeMode, WindowAttributes};
|
use crate::window::{ThemeMode, WindowAttributes};
|
||||||
|
|
||||||
pub struct Context<C> {
|
pub struct Context<C> {
|
||||||
|
|
@ -2045,6 +2083,7 @@ pub(crate) mod sealed {
|
||||||
pub on_closed: Option<OnceCallback>,
|
pub on_closed: Option<OnceCallback>,
|
||||||
pub vsync: bool,
|
pub vsync: bool,
|
||||||
pub multisample_count: NonZeroU32,
|
pub multisample_count: NonZeroU32,
|
||||||
|
pub close_requested: Option<Arc<Mutex<Callback<(), bool>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -2571,6 +2610,7 @@ impl CushyWindowBuilder {
|
||||||
on_closed: None,
|
on_closed: None,
|
||||||
vsync: false,
|
vsync: false,
|
||||||
multisample_count: self.multisample_count,
|
multisample_count: self.multisample_count,
|
||||||
|
close_requested: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue