cushy::main attribute macro

This commit is contained in:
Jonathan Johnson 2024-09-08 09:31:35 -07:00
parent 0953e5ab40
commit bbbc8151c4
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
7 changed files with 260 additions and 50 deletions

View file

@ -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

View file

@ -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::<ItemFn>(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
}
})
}

View file

@ -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;

View file

@ -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(())
}

View file

@ -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(

View file

@ -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<F>(&mut self, on_startup: F)
pub fn on_startup<F, R>(&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<AppEvent<WindowCommand>> for PendingApp {
}
}
pub trait StartupResult {
fn into_result(self) -> cushy::Result;
}
impl StartupResult for () {
fn into_result(self) -> crate::Result {
Ok(())
}
}
impl<E> StartupResult for Result<(), E>
where
E: Into<EventLoopError>,
{
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`.

View file

@ -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;