Skip to content

Commit

Permalink
cushy::main attribute macro
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Sep 8, 2024
1 parent 0953e5a commit bbbc815
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 50 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions cushy-macros/src/cushy_main.rs
Original file line number Diff line number Diff line change
@@ -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
}
})
}
3 changes: 3 additions & 0 deletions cushy-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
15 changes: 7 additions & 8 deletions examples/shared-switcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,24 @@
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 {
A,
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
selected
.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 {
Expand All @@ -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(())
}
77 changes: 38 additions & 39 deletions examples/window-properties.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
31 changes: 28 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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| {
Expand All @@ -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);
}
});
});
}
Expand Down Expand Up @@ -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`.
Expand Down
Loading

0 comments on commit bbbc815

Please sign in to comment.