mirror of
https://github.com/danbulant/cushy
synced 2026-05-19 04:08:38 +00:00
cushy::main attribute macro
This commit is contained in:
parent
0953e5ab40
commit
bbbc8151c4
7 changed files with 260 additions and 50 deletions
|
|
@ -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
|
||||
|
|
|
|||
90
cushy-macros/src/cushy_main.rs
Normal file
90
cushy-macros/src/cushy_main.rs
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
31
src/app.rs
31
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<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`.
|
||||
|
|
|
|||
92
src/lib.rs
92
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue