diff --git a/CHANGELOG.md b/CHANGELOG.md index 5279d87..df6f997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `RunningWindow::request_close()` requests that the window should close. This ensures `WindowBehavior::close_requested` is invoked before the window is closed. +- `PendingWindow` is a new type that can return a `WindowHandle` for a window + that hasn't opened yet. This can be used to allow a widget on a window to + close the window. [91]: https://github.com/khonsulabs/gooey/issues/91 [92]: https://github.com/khonsulabs/gooey/issues/92 diff --git a/examples/multi-window.rs b/examples/multi-window.rs index ff68f0d..3dfbd96 100644 --- a/examples/multi-window.rs +++ b/examples/multi-window.rs @@ -2,6 +2,7 @@ use gooey::kludgine::include_texture; use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; use gooey::widgets::Image; +use gooey::window::PendingWindow; use gooey::{Application, Open, PendingApp, Run}; use kludgine::LazyTexture; @@ -66,14 +67,26 @@ fn open_another_window( *count += 1; *count }); + let open_windows = open_windows.clone(); open_windows.map_mut(|open_windows| *open_windows += 1); - format!("This is window {my_number}") - .and(open_window_button(app, &open_windows, counter, texture)) - .and(Image::new(texture.clone())) - .into_rows() - .centered() - .into_window() + + let window = PendingWindow::default(); + let handle = window.handle(); + + window + .with_root( + format!("This is window {my_number}") + .and(open_window_button(app, &open_windows, counter, texture)) + .and(Image::new(texture.clone())) + .and( + "Close" + .into_button() + .on_click(move |()| handle.request_close()), + ) + .into_rows() + .centered(), + ) .on_close(move || open_windows.map_mut(|open_windows| *open_windows -= 1)) .open(app) .expect("error opening another window"); diff --git a/src/window.rs b/src/window.rs index 2e1dd71..b6a58bd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,7 +6,7 @@ use std::hash::Hash; use std::ops::{Deref, DerefMut, Not}; use std::path::Path; use std::string::ToString; -use std::sync::{MutexGuard, OnceLock}; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use ahash::AHashMap; use alot::LotId; @@ -38,8 +38,8 @@ use crate::tree::Tree; use crate::utils::ModifiersExt; use crate::value::{Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Value}; use crate::widget::{ - EventHandling, MountedWidget, OnceCallback, RootBehavior, Widget, WidgetId, WidgetInstance, - HANDLED, IGNORED, + EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, Widget, WidgetId, + WidgetInstance, HANDLED, IGNORED, }; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit}; @@ -144,6 +144,7 @@ where Behavior: WindowBehavior, { context: Behavior::Context, + pending: PendingWindow, /// The attributes of this window. pub attributes: WindowAttributes, /// The colors to use to theme the user interface. @@ -288,6 +289,10 @@ where /// 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 @@ -304,6 +309,7 @@ where }) .clone(); Self { + pending, attributes: WindowAttributes { title, ..WindowAttributes::default() @@ -350,14 +356,14 @@ where App: Application, { let gooey = app.gooey().clone(); - let redraw_status = InvalidationStatus::default(); + let handle = GooeyWindow::::open_with( app, sealed::Context { user: self.context, settings: RefCell::new(sealed::WindowSettings { gooey, - redraw_status: redraw_status.clone(), + redraw_status: self.pending.0.redraw_status.clone(), on_closed: self.on_closed, transparent: self.attributes.transparent, attributes: Some(self.attributes), @@ -376,7 +382,7 @@ where }, )?; - Ok(handle.map(|handle| WindowHandle::new(handle, redraw_status))) + Ok(handle.map(|handle| self.pending.opened(handle))) } fn run_in(self, app: PendingApp) -> crate::Result { @@ -1534,7 +1540,7 @@ fn default_family(query: Family<'_>) -> Option { /// A handle to an open Gooey window. #[derive(Clone)] pub struct WindowHandle { - pub(crate) kludgine: kludgine::app::WindowHandle, + inner: InnerWindowHandle, pub(crate) redraw_status: InvalidationStatus, } @@ -1544,23 +1550,30 @@ impl WindowHandle { redraw_status: InvalidationStatus, ) -> Self { Self { - kludgine, + inner: InnerWindowHandle::Known(kludgine), redraw_status, } } + fn pending() -> Self { + Self { + inner: InnerWindowHandle::Pending(Arc::default()), + redraw_status: InvalidationStatus::default(), + } + } + /// Request that the window closes. /// /// A window may disallow itself from being closed by customizing /// [`WindowBehavior::close_requested`]. pub fn request_close(&self) { - let _result = self.kludgine.send(sealed::WindowCommand::RequestClose); + self.inner.send(sealed::WindowCommand::RequestClose); } /// Requests that the window redraws. pub fn redraw(&self) { if self.redraw_status.should_send_refresh() { - let _result = self.kludgine.send(WindowCommand::Redraw); + self.inner.send(WindowCommand::Redraw); } } @@ -1585,3 +1598,96 @@ impl Hash for WindowHandle { self.redraw_status.hash(state); } } + +#[derive(Clone)] +enum InnerWindowHandle { + Pending(Arc), + Known(kludgine::app::WindowHandle), +} + +impl InnerWindowHandle { + fn send(&self, message: WindowCommand) { + match self { + InnerWindowHandle::Pending(pending) => { + if let Some(handle) = pending.handle.get() { + let _result = handle.send(message); + } else { + pending + .commands + .lock() + .expect("lock poisoned") + .push(message); + } + } + InnerWindowHandle::Known(handle) => { + let _result = handle.send(message); + } + }; + } +} + +/// A [`Window`] that doesn't have its root widget yet. +/// +/// [`PendingWindow::handle()`] returns a handle that allows code to interact +/// with a window before it has had its contents initialized. This is useful, +/// for example, for a button's `on_click` to be able to close the window that +/// contains it. +pub struct PendingWindow(WindowHandle); + +impl Default for PendingWindow { + fn default() -> Self { + Self(WindowHandle::pending()) + } +} + +impl PendingWindow { + /// Returns a [`Window`] using `context` to initialize its contents. + pub fn with(self, context: Behavior::Context) -> Window + where + Behavior: WindowBehavior, + { + Window::new_with_pending(context, self) + } + + /// Returns a [`Window`] containing `root`. + pub fn with_root(self, root: impl MakeWidget) -> Window { + Window::new_with_pending(root.make_widget(), self) + } + + /// Returns a [`Window`] using the default context to initialize its + /// contents. + pub fn using(self) -> Window + where + Behavior: WindowBehavior, + Behavior::Context: Default, + { + self.with(::default()) + } + + /// Returns a handle for this window. + #[must_use] + pub fn handle(&self) -> WindowHandle { + self.0.clone() + } + + fn opened(self, handle: kludgine::app::WindowHandle) -> WindowHandle { + let InnerWindowHandle::Pending(pending) = &self.0.inner else { + unreachable!("always pending") + }; + + let initialized = pending.handle.set(handle.clone()); + assert!(initialized.is_ok()); + + for command in pending.commands.lock().expect("poisoned").drain(..) { + let _result = handle.send(command); + } + + WindowHandle::new(handle, self.0.redraw_status.clone()) + } +} + +#[derive(Default)] +struct PendingWindowHandle { + handle: OnceLock>, + commands: Mutex>, +}