From bbbc8151c4ddd8ba4b1af2f69bd0babf211bd834 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 8 Sep 2024 09:31:35 -0700 Subject: [PATCH] cushy::main attribute macro --- CHANGELOG.md | 2 + cushy-macros/src/cushy_main.rs | 90 +++++++++++++++++++++++++++++++++ cushy-macros/src/lib.rs | 3 ++ examples/shared-switcher.rs | 15 +++--- examples/window-properties.rs | 77 ++++++++++++++-------------- src/app.rs | 31 ++++++++++-- src/lib.rs | 92 ++++++++++++++++++++++++++++++++++ 7 files changed, 260 insertions(+), 50 deletions(-) create mode 100644 cushy-macros/src/cushy_main.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 401d87b..2a9c848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `inner_position` - `outer_size` +- `#[cushy::main]` is a new attribute proc-macro that simplifies initializing + and running multi-window applications. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/cushy-macros/src/cushy_main.rs b/cushy-macros/src/cushy_main.rs new file mode 100644 index 0000000..f06fc0a --- /dev/null +++ b/cushy-macros/src/cushy_main.rs @@ -0,0 +1,90 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{FnArg, ItemFn, Type}; + +pub fn main(_attr: TokenStream, item: TokenStream) -> manyhow::Result { + let function = syn::parse2::(item)?; + + let mut inputs = function.sig.inputs.iter(); + let Some(FnArg::Typed(input)) = inputs.next() else { + manyhow::bail!( + "the cushy::main fn must accept one of `&mut cushy::PendingApp` or `&mut cushy::App`" + ) + }; + if inputs.next().is_some() { + manyhow::bail!("the cushy::main fn can have one input") + } + let Type::Reference(reference) = &*input.ty else { + manyhow::bail!( + "the cushy::main fn must accept one of `&mut cushy::PendingApp` or `&mut cushy::App`" + ); + }; + let Type::Path(path) = &*reference.elem else { + manyhow::bail!( + "the cushy::main fn must accept one of `&mut cushy::PendingApp` or `&mut cushy::App`" + ) + }; + + let body = function.block; + let (result, body) = match path.path.segments.last() { + Some(segment) if segment.ident == "App" => match function.sig.output { + syn::ReturnType::Default => ( + quote!(()), + quote!(::cushy::run(|#input| #body).expect("event loop startup")), + ), + syn::ReturnType::Type(_, ty) => { + let pat = &input.pat; + ( + ty.to_token_stream(), + quote!( + let mut app = ::cushy::PendingApp::default(); + app.on_startup(|#pat: &mut #path| -> #ty #body); + ::cushy::Run::run(app) + ), + ) + } + }, + Some(segment) if segment.ident == "PendingApp" => { + let pat = &input.pat; + let original_output = function.sig.output; + let (output, return_error) = match &original_output { + syn::ReturnType::Default => (quote!(::cushy::Result), TokenStream::default()), + syn::ReturnType::Type(_, ty) => (ty.to_token_stream(), quote!(?)), + }; + ( + output, + quote!( + let mut __pending_app = #path::default(); + let cushy = __pending_app.cushy().clone(); + let _guard = cushy.enter_runtime(); + let init = |#pat: &mut #path| #original_output #body; + init(&mut __pending_app)#return_error; + ::cushy::Run::run(__pending_app)?; + Ok(()) + ), + ) + } + _ => manyhow::bail!( + "the cushy::main fn must accept one of `&mut cushy::PendingApp` or `&mut cushy::App`" + ), + }; + + manyhow::ensure!( + function.sig.asyncness.is_none(), + "cushy::main does not support async" + ); + manyhow::ensure!( + function.sig.constness.is_none(), + "cushy::main does not support const" + ); + + let fn_token = function.sig.fn_token; + let name = function.sig.ident; + let unsafety = function.sig.unsafety; + + Ok(quote! { + #unsafety #fn_token #name() -> #result { + #body + } + }) +} diff --git a/cushy-macros/src/lib.rs b/cushy-macros/src/lib.rs index 2c793ce..713f353 100644 --- a/cushy-macros/src/lib.rs +++ b/cushy-macros/src/lib.rs @@ -24,6 +24,9 @@ macro_rules! expansion_snapshot { } mod animation; +mod cushy_main; #[manyhow(proc_macro_derive(LinearInterpolate))] pub use animation::linear_interpolate; +#[manyhow(proc_macro_attribute)] +pub use cushy_main::main; diff --git a/examples/shared-switcher.rs b/examples/shared-switcher.rs index 1762424..a0955ee 100644 --- a/examples/shared-switcher.rs +++ b/examples/shared-switcher.rs @@ -10,7 +10,7 @@ use cushy::value::{Dynamic, Switchable}; use cushy::widget::MakeWidget; use cushy::widgets::Custom; -use cushy::{Open, PendingApp, Run}; +use cushy::{Open, PendingApp}; #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum Contents { @@ -18,9 +18,8 @@ enum Contents { B, } -fn main() -> cushy::Result { - let mut app = PendingApp::default(); - +#[cushy::main] +fn main(app: &mut PendingApp) -> cushy::Result { let selected = Dynamic::new(Contents::A); // Open up another window containing our controls @@ -28,7 +27,7 @@ fn main() -> cushy::Result { .new_radio(Contents::A, "A") .and(selected.new_radio(Contents::B, "B")) .into_rows() - .open(&mut app)?; + .open(app)?; let display = selected .switcher(|contents, _| match contents { @@ -46,8 +45,8 @@ fn main() -> cushy::Result { .make_widget(); // Open two windows with the same switcher instance - display.to_window().open(&mut app)?; - display.to_window().open(&mut app)?; + display.to_window().open(app)?; + display.to_window().open(app)?; - app.run() + Ok(()) } diff --git a/examples/window-properties.rs b/examples/window-properties.rs index 5d92d83..f76a070 100644 --- a/examples/window-properties.rs +++ b/examples/window-properties.rs @@ -1,50 +1,49 @@ use cushy::figures::Size; use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; -use cushy::{run, App, Open}; +use cushy::{App, Open}; use figures::units::{Px, UPx}; use figures::{IntoSigned, Point, Px2D, UPx2D}; -fn main() -> cushy::Result { - run(|app| { - let focused = Dynamic::new(false); - let occluded = Dynamic::new(false); - let maximized = Dynamic::new(false); - let minimized = Dynamic::new(false); - let inner_size = Dynamic::new(Size::upx(0, 0)); - let outer_size = Dynamic::new(Size::upx(0, 0)); - let inner_position = Dynamic::new(Point::px(0, 0)); - let outer_position = Dynamic::new(Point::px(0, 0)); - let icon = image::load_from_memory(include_bytes!("assets/ferris-happy.png")) - .expect("valid image"); +#[cushy::main] +fn main(app: &mut App) { + let focused = Dynamic::new(false); + let occluded = Dynamic::new(false); + let maximized = Dynamic::new(false); + let minimized = Dynamic::new(false); + let inner_size = Dynamic::new(Size::upx(0, 0)); + let outer_size = Dynamic::new(Size::upx(0, 0)); + let inner_position = Dynamic::new(Point::px(0, 0)); + let outer_position = Dynamic::new(Point::px(0, 0)); + let icon = + image::load_from_memory(include_bytes!("assets/ferris-happy.png")).expect("valid image"); - let widgets = focused - .map_each(|v| format!("focused: {:?}", v)) - .and(occluded.map_each(|v| format!("occluded: {:?}", v))) - .and(maximized.map_each(|v| format!("maximized: {:?}", v))) - .and(minimized.map_each(|v| format!("minimized: {:?}", v))) - .and(inner_position.map_each(|v| format!("inner_position: {:?}", v))) - .and(outer_position.map_each(|v| format!("outer_position: {:?}", v))) - .and(inner_size.map_each(|v| format!("inner_size: {:?}", v))) - .and(outer_size.map_each(|v| format!("outer_size: {:?}", v))) - .and(center_window_button(app, &outer_position, &outer_size)) - .into_rows() - .centered(); + let widgets = focused + .map_each(|v| format!("focused: {:?}", v)) + .and(occluded.map_each(|v| format!("occluded: {:?}", v))) + .and(maximized.map_each(|v| format!("maximized: {:?}", v))) + .and(minimized.map_each(|v| format!("minimized: {:?}", v))) + .and(inner_position.map_each(|v| format!("inner_position: {:?}", v))) + .and(outer_position.map_each(|v| format!("outer_position: {:?}", v))) + .and(inner_size.map_each(|v| format!("inner_size: {:?}", v))) + .and(outer_size.map_each(|v| format!("outer_size: {:?}", v))) + .and(center_window_button(app, &outer_position, &outer_size)) + .into_rows() + .centered(); - widgets - .into_window() - .focused(focused) - .occluded(occluded) - .inner_size(inner_size) - .outer_size(outer_size) - .inner_position(inner_position) - .outer_position(outer_position) - .maximized(maximized) - .minimized(minimized) - .icon(Some(icon.into_rgba8())) - .open(app) - .expect("app running"); - }) + widgets + .into_window() + .focused(focused) + .occluded(occluded) + .inner_size(inner_size) + .outer_size(outer_size) + .inner_position(inner_position) + .outer_position(outer_position) + .maximized(maximized) + .minimized(minimized) + .icon(Some(icon.into_rgba8())) + .open(app) + .expect("app running"); } fn center_window_button( diff --git a/src/app.rs b/src/app.rs index b700baa..8ccfcf9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,10 @@ use std::marker::PhantomData; +use std::process::exit; use std::sync::Arc; use std::thread; use arboard::Clipboard; +use kludgine::app::winit::error::EventLoopError; use kludgine::app::{AppEvent, AsApplication, Monitors}; use parking_lot::{Mutex, MutexGuard}; @@ -37,9 +39,10 @@ impl PendingApp { /// Some APIs are not available until after the application has started /// running. For example, `App::monitors` requires the event loop to have /// been started. - pub fn on_startup(&mut self, on_startup: F) + pub fn on_startup(&mut self, on_startup: F) where - F: FnOnce(&mut App) + Send + 'static, + F: FnOnce(&mut App) -> R + Send + 'static, + R: StartupResult, { let mut app = self.as_app(); self.app.on_startup(move |_app| { @@ -51,7 +54,10 @@ impl PendingApp { thread::spawn(move || { let cushy = app.cushy.clone(); let _guard = cushy.enter_runtime(); - on_startup(&mut app); + if let Err(err) = on_startup(&mut app).into_result() { + eprintln!("error in on_startup: {err}"); + exit(-1); + } }); }); } @@ -84,6 +90,25 @@ impl AsApplication> for PendingApp { } } +pub trait StartupResult { + fn into_result(self) -> cushy::Result; +} + +impl StartupResult for () { + fn into_result(self) -> crate::Result { + Ok(()) + } +} + +impl StartupResult for Result<(), E> +where + E: Into, +{ + fn into_result(self) -> crate::Result { + self.map_err(Into::into) + } +} + /// A runtime associated with the Cushy application. /// /// This trait is how Cushy adds optional support for `tokio`. diff --git a/src/lib.rs b/src/lib.rs index 5287ad1..46280dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,98 @@ use std::ops::{Add, AddAssign, Sub, SubAssign}; #[cfg(feature = "tokio")] pub use app::TokioRuntime; pub use app::{App, AppRuntime, Application, Cushy, DefaultRuntime, Open, PendingApp, Run}; +/// A macro to create a `main()` function with less boilerplate. +/// +/// When creating applications that support multiple windows, this attribute +/// macro can be used to remove a few lines of code. +/// +/// The function body is executed during application startup, and the app will +/// continue running until the last window is closed. +/// +/// This attribute must be attached to a `main(&mut PendingApp)` or `main(&mut +/// App)` function. Either form supports a return type or no return type. +/// +/// ## `&mut PendingApp` +/// +/// When using a [`PendingApp`], the function body is invoked before the app is +/// run. While the example shown below does not require the runtime +/// initialization, some programs do and using the macro means the developer +/// will never forget to add the extra code. +/// +/// These two example programs are functionally identical: +/// +/// ### Without Macro +/// +/// ```rust +/// # fn test() { +/// use cushy::{Open, PendingApp, Run}; +/// +/// fn main() -> cushy::Result { +/// let mut app = PendingApp::default(); +/// let cushy = app.cushy().clone(); +/// let _guard = cushy.enter_runtime(); +/// +/// "Hello World".open(&mut app)?; +/// +/// app.run() +/// } +/// # } +/// ``` +/// +/// ### With Macro +/// +/// ```rust +/// # fn test() { +/// use cushy::{Open, PendingApp}; +/// +/// #[cushy::main] +/// fn main(app: &mut PendingApp) -> cushy::Result { +/// "Hello World".open(app)?; +/// Ok(()) +/// } +/// # } +/// ``` +/// +/// ## `&mut App` +/// +/// When using an [`App`], the function body is invoked after the app's event +/// loop has begun executing. This is important if the application wants to +/// access monitor information to either position windows precisely or use a +/// full screen video mode. +/// +/// These two example programs are functionally identical: +/// +/// ### Without Macro +/// +/// ```rust +/// # fn test() { +/// use cushy::{App, Open, PendingApp, Run}; +/// +/// fn main() -> cushy::Result { +/// let mut app = PendingApp::default(); +/// app.on_startup(|app| -> cushy::Result { +/// "Hello World".open(app)?; +/// Ok(()) +/// }); +/// app.run() +/// } +/// # } +/// ``` +/// +/// ### With Macro +/// +/// ```rust +/// # fn test() { +/// use cushy::{App, Open}; +/// +/// #[cushy::main] +/// fn main(app: &mut App) -> cushy::Result { +/// "Hello World".open(app)?; +/// Ok(()) +/// } +/// # } +/// ``` +pub use cushy_macros::main; use figures::units::UPx; use figures::{Fraction, ScreenUnit, Size, Zero}; use kludgine::app::winit::error::EventLoopError;