diff --git a/Cargo.lock b/Cargo.lock index fbd0ca2..5e800a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,7 @@ name = "fynix" version = "0.1.0" dependencies = [ "field_path", + "fynix_macros", "hashbrown 0.16.1", "imaging", "rectree", @@ -614,6 +615,16 @@ dependencies = [ "winit", ] +[[package]] +name = "fynix_macros" +version = "0.1.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "gethostname" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index ecb19dd..99ba4a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/voxell-tech/fynix" [workspace.dependencies] fynix = { path = "crates/fynix" } +fynix_macros = { path = "crates/fynix_macros" } fynix_elements = { path = "crates/fynix_elements" } rectree = { git = "https://github.com/voxell-tech/rectree", rev = "08ddf2a" } diff --git a/crates/fynix/Cargo.toml b/crates/fynix/Cargo.toml index fbad7fc..ddae721 100644 --- a/crates/fynix/Cargo.toml +++ b/crates/fynix/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true repository.workspace = true [dependencies] +fynix_macros.workspace = true rectree.workspace = true hashbrown.workspace = true field_path.workspace = true diff --git a/crates/fynix/src/ctx.rs b/crates/fynix/src/ctx.rs index 74f2952..a3c5582 100644 --- a/crates/fynix/src/ctx.rs +++ b/crates/fynix/src/ctx.rs @@ -103,39 +103,30 @@ mod tests { use field_path::field_accessor; use rectree::{Constraint, NodeContext, Size, Vec2}; - use typeslot::TypeSlot; - use crate::element::{ElementGroup, ElementNodes}; + use crate::element::{ElementBuild, ElementNodes}; use super::*; - #[derive(Default, Clone, TypeSlot)] - #[slot(ElementGroup)] + #[derive(Element, Default, Clone)] struct Label { pub text: String, } - impl Element for Label { - fn new() -> Self { - Self::default() - } - + impl ElementBuild for Label { fn build( &self, _id: &ElementId, constraint: Constraint, _nodes: &mut ElementNodes, - ) -> rectree::Size - where - Self: Sized, - { + ) -> Size { constraint.min } } - #[derive(Default, Clone, TypeSlot)] - #[slot(ElementGroup)] + #[derive(Element, Default, Clone)] struct Vertical { + #[children] children: Vec, } @@ -145,20 +136,13 @@ mod tests { } } - impl Element for Vertical { - fn new() -> Self { - Self::default() - } - + impl ElementBuild for Vertical { fn build( &self, _id: &ElementId, constraint: Constraint, nodes: &mut ElementNodes, - ) -> Size - where - Self: Sized, - { + ) -> Size { let mut size = Size::ZERO; for child in self.children.iter() { diff --git a/crates/fynix/src/element.rs b/crates/fynix/src/element.rs index 265d2ef..eb90cd6 100644 --- a/crates/fynix/src/element.rs +++ b/crates/fynix/src/element.rs @@ -8,6 +8,8 @@ use crate::element::table::ElementTable; use crate::id::{GenId, IdGenerator}; use crate::resource::Resources; +pub use fynix_macros::{Element, ElementSlot}; + pub mod meta; pub mod table; @@ -15,6 +17,80 @@ pub mod table; #[derive(SlotGroup)] pub struct ElementGroup; +/// Constructs a default (unstyled) instance of an element. +/// +/// Derived by `#[derive(Element)]` - calls `Default::default()` unless +/// overridden with `#[element(new = my_fn)]`. +pub trait ElementNew { + fn new() -> Self + where + Self: Sized; +} + +/// Enumerates the children of an element. +/// +/// Derived by `#[derive(Element)]` - iterates the field tagged `#[children]`, +/// or the fn given in `#[element(children = my_fn)]`. Defaults to no children. +pub trait ElementChildren { + fn children(&self) -> impl IntoIterator + where + Self: Sized, + { + [] + } +} + +/// Layout and rendering protocol for element types. +/// +/// Implement this manually alongside `#[derive(Element)]`. +pub trait ElementBuild { + fn constrain(&self, parent_constraint: Constraint) -> Constraint { + parent_constraint + } + + fn build( + &self, + id: &ElementId, + constraint: Constraint, + nodes: &mut ElementNodes, + ) -> Size; + + /// Paints the element's own visual layer into `painter`. + /// + /// The element's world-space position and layout size can + /// be read from `metas` using `id`. Both are set by the + /// layout pass and are safe to use for rendering + /// coordinates. + /// + /// Child elements are rendered by the tree walker after + /// this method returns - do not recurse into children + /// here. + /// + /// The default implementation is a no-op, suitable for + /// purely structural elements that have no visual of + /// their own. + #[expect(unused_variables)] + fn render( + &self, + id: &ElementId, + painter: &mut dyn PaintSink, + metas: &ElementMetas, + ) { + } +} + +/// Marker trait for element types. Use `#[derive(Element)]` to implement +/// this alongside [`ElementNew`] and [`ElementChildren`] automatically. +/// Implement [`ElementBuild`] manually. +pub trait Element: + ElementNew + + ElementChildren + + ElementBuild + + TypeSlot + + 'static +{ +} + /// Type-erased storage for all element instances. /// /// Internally holds one [`ElementTable`] column per element @@ -102,7 +178,7 @@ impl Elements { /// Renders the subtree rooted at `id` into `sink`. /// /// Each element's own visual layer is painted via - /// [`Element::render`] before its children are visited, + /// [`ElementBuild::render`] before its children are visited, /// so parents always draw behind their children. /// /// Layout must be complete before calling this - @@ -279,63 +355,6 @@ impl<'a> Rectree for ElementTree<'a> { } } -/// Trait for element types. -/// -/// Implement this for any type you want to add to the -/// element tree via -/// [`FynixCtx::add`](crate::ctx::FynixCtx::add). The single -/// required method, `new`, must return a default (unstyled) -/// instance. -/// -/// Styles are applied immediately after construction by the -/// build context. -pub trait Element: TypeSlot + 'static { - fn new() -> Self - where - Self: Sized; - - fn children(&self) -> impl IntoIterator - where - Self: Sized, - { - [] - } - - fn constrain(&self, parent_constraint: Constraint) -> Constraint { - parent_constraint - } - - fn build( - &self, - id: &ElementId, - constraint: Constraint, - nodes: &mut ElementNodes, - ) -> Size; - - /// Paints the element's own visual layer into `painter`. - /// - /// The element's world-space position and layout size can - /// be read from `metas` using `id`. Both are set by the - /// layout pass and are safe to use for rendering - /// coordinates. - /// - /// Child elements are rendered by the tree walker after - /// this method returns - do not recurse into children - /// here. - /// - /// The default implementation is a no-op, suitable for - /// purely structural elements that have no visual of - /// their own. - #[expect(unused_variables)] - fn render( - &self, - id: &ElementId, - painter: &mut dyn PaintSink, - metas: &ElementMetas, - ) { - } -} - /// Generational ID for element instances. pub type ElementId = GenId<_ElementMarker>; pub type ElementIdGenerator = IdGenerator<_ElementMarker>; diff --git a/crates/fynix/src/element/meta.rs b/crates/fynix/src/element/meta.rs index 94ea6c9..131104a 100644 --- a/crates/fynix/src/element/meta.rs +++ b/crates/fynix/src/element/meta.rs @@ -155,12 +155,14 @@ pub fn get_dyn_element<'a, E: Element>( /// Visits each child of an element by calling `f` for every /// [`ElementId`] the element yields from -/// [`Element::children`]. +/// [`ElementChildren::children`]. /// /// Using a visitor avoids the need to name the concrete -/// iterator type returned by [`Element::children`], which +/// iterator type returned by [`ElementChildren::children`], which /// differs per `E` and cannot be expressed in a /// function-pointer signature. +/// +/// [`ElementChildren::children`]: super::ElementChildren::children pub type ChildrenElementFn = fn( table: &ElementTable, id: &ElementId, diff --git a/crates/fynix/src/lib.rs b/crates/fynix/src/lib.rs index 8e6e8e6..3b9c12a 100644 --- a/crates/fynix/src/lib.rs +++ b/crates/fynix/src/lib.rs @@ -15,6 +15,7 @@ use crate::style::{StyleId, Styles}; pub use imaging; pub use rectree; +pub use typeslot; pub mod ctx; pub mod element; @@ -27,7 +28,7 @@ mod id; /// Initializes the Fynix framework. /// /// Must be called before any element is added to a [`Fynix`] -/// instance. Safe to call more than once — subsequent calls +/// instance. Safe to call more than once - subsequent calls /// are no-ops. pub fn init() { static INITIALIZED: AtomicBool = AtomicBool::new(false); diff --git a/crates/fynix_elements/src/lib.rs b/crates/fynix_elements/src/lib.rs index e0f4814..e408832 100644 --- a/crates/fynix_elements/src/lib.rs +++ b/crates/fynix_elements/src/lib.rs @@ -8,7 +8,7 @@ use alloc::vec::Vec; use fynix::Fynix; use fynix::element::meta::ElementMetas; use fynix::element::{ - Element, ElementGroup, ElementId, ElementNodes, + Element, ElementBuild, ElementId, ElementNodes, }; use fynix::imaging::kurbo::Affine; use fynix::imaging::peniko::{Brush, BrushRef, Color, Fill, Style}; @@ -20,12 +20,11 @@ use parley::{ Alignment, AlignmentOptions, FontStyle, PositionedLayoutItem, }; use parley::{FontContext, LayoutContext}; -use typeslot::TypeSlot; -#[derive(Default, Debug, Clone, Copy, TypeSlot)] -#[slot(ElementGroup)] +#[derive(Element, Default, Debug, Clone, Copy)] pub struct WindowSize { pub size: Size, + #[children] child: Option, } @@ -35,21 +34,7 @@ impl WindowSize { } } -impl Element for WindowSize { - fn new() -> Self - where - Self: Sized, - { - Self::default() - } - - fn children(&self) -> impl IntoIterator - where - Self: Sized, - { - self.child.iter() - } - +impl ElementBuild for WindowSize { fn constrain( &self, _parent_constraint: Constraint, @@ -67,9 +52,9 @@ impl Element for WindowSize { } } -#[derive(Default, Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(Element, Default, Debug, Clone)] pub struct Horizontal { + #[children] children: Vec, } @@ -80,26 +65,7 @@ impl Horizontal { } } -impl Element for Horizontal { - fn new() -> Self - where - Self: Sized, - { - Self::default() - } - - fn children(&self) -> impl IntoIterator - where - Self: Sized, - { - // TODO: Refer to this when creating the #[derive(Element)] - // macro. And remove it after that. - - // Showcasing the generic way of doing it. - #[allow(clippy::into_iter_on_ref)] - (&self.children).into_iter() - } - +impl ElementBuild for Horizontal { fn build( &self, _id: &ElementId, @@ -120,9 +86,9 @@ impl Element for Horizontal { } } -#[derive(Default, Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(Element, Default, Debug, Clone)] pub struct Vertical { + #[children] children: Vec, } @@ -133,21 +99,7 @@ impl Vertical { } } -impl Element for Vertical { - fn new() -> Self - where - Self: Sized, - { - Self::default() - } - - fn children(&self) -> impl IntoIterator - where - Self: Sized, - { - self.children.iter() - } - +impl ElementBuild for Vertical { fn build( &self, _id: &ElementId, @@ -168,8 +120,7 @@ impl Element for Vertical { } } -#[derive(Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(Element, Debug, Clone)] pub struct Label { pub text: String, pub fill: Brush, @@ -178,11 +129,8 @@ pub struct Label { pub alignment: Alignment, } -impl Element for Label { - fn new() -> Self - where - Self: Sized, - { +impl Default for Label { + fn default() -> Self { Self { text: String::new(), fill: Brush::Solid(Color::WHITE), @@ -191,7 +139,9 @@ impl Element for Label { alignment: Default::default(), } } +} +impl ElementBuild for Label { fn build( &self, id: &ElementId, diff --git a/crates/fynix_macros/Cargo.toml b/crates/fynix_macros/Cargo.toml new file mode 100644 index 0000000..3f07b76 --- /dev/null +++ b/crates/fynix_macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fynix_macros" +description = "Proc-macro derives for the Fynix UI framework." +keywords = ["ui", "gui", "fynix", "macro"] +categories = ["gui"] +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro-crate = "3" +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["derive"] } diff --git a/crates/fynix_macros/src/lib.rs b/crates/fynix_macros/src/lib.rs new file mode 100644 index 0000000..f25ef0b --- /dev/null +++ b/crates/fynix_macros/src/lib.rs @@ -0,0 +1,225 @@ +use proc_macro::TokenStream; +use proc_macro_crate::FoundCrate; +use proc_macro_crate::crate_name; +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::Data; +use syn::DeriveInput; +use syn::Expr; +use syn::Fields; +use syn::parse_macro_input; + +fn fynix_crate() -> TokenStream2 { + match crate_name("fynix") { + Ok(FoundCrate::Itself) => quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = Ident::new(&name, Span::call_site()); + quote!(::#ident) + } + Err(_) => quote!(::fynix), + } +} + +fn element_slot_tokens( + name: &Ident, + fynix: &TokenStream2, +) -> TokenStream2 { + quote! { + const _: () = { + static __SLOT: #fynix::typeslot::AtomicSlot = + #fynix::typeslot::AtomicSlot::new(); + + impl #fynix::typeslot::TypeSlot< + #fynix::element::ElementGroup, + > for #name { + #[inline] + fn try_slot() -> ::core::option::Option { + __SLOT.get() + } + + #[inline] + fn dyn_try_slot( + &self, + ) -> ::core::option::Option { + __SLOT.get() + } + } + + #fynix::typeslot::inventory::submit! { + #fynix::typeslot::TypeSlotEntry { + type_id: ::core::any::TypeId::of::<#name>(), + group_id: ::core::any::TypeId::of::< + #fynix::element::ElementGroup, + >(), + slot: &__SLOT, + } + } + }; + } +} + +/// Derives `typeslot::TypeSlot` +/// for the annotated type. +/// +/// Equivalent to writing: +/// +/// ```ignore +/// #[derive(::typeslot::TypeSlot)] +/// #[slot(::fynix::element::ElementGroup)] +/// ``` +#[proc_macro_derive(ElementSlot)] +pub fn derive_element_slot(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let fynix = fynix_crate(); + element_slot_tokens(&input.ident, &fynix).into() +} + +struct ElementAttrs { + new_fn: Option, + children_fn: Option, +} + +fn parse_element_attrs( + attrs: &[syn::Attribute], +) -> syn::Result { + let mut new_fn = None; + let mut children_fn = None; + + for attr in attrs { + if attr.path().is_ident("element") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("new") { + new_fn = Some(meta.value()?.parse::()?); + } else if meta.path.is_ident("children") { + children_fn = + Some(meta.value()?.parse::()?); + } + Ok(()) + })?; + } + } + + Ok(ElementAttrs { + new_fn, + children_fn, + }) +} + +/// Derives `ElementNew`, `ElementChildren`, `ElementSlot`, and `Element` for the annotated struct. +/// Implement `ElementBuild` manually. +#[proc_macro_derive(Element, attributes(children, element))] +pub fn derive_element(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let fynix = fynix_crate(); + + let attrs = match parse_element_attrs(&input.attrs) { + Ok(a) => a, + Err(e) => return e.to_compile_error().into(), + }; + + let Data::Struct(s) = &input.data else { + return syn::Error::new_spanned( + name, + "#[derive(Element)] only supports structs", + ) + .to_compile_error() + .into(); + }; + + let Fields::Named(f) = &s.fields else { + return syn::Error::new_spanned( + name, + "#[derive(Element)] requires named fields", + ) + .to_compile_error() + .into(); + }; + + let children_fields: Vec<_> = f + .named + .iter() + .filter(|f| { + f.attrs.iter().any(|a| a.path().is_ident("children")) + }) + .collect(); + + if children_fields.len() > 1 { + return syn::Error::new_spanned( + children_fields[1], + "#[derive(Element)] found multiple #[children] \ + fields; only one is allowed", + ) + .to_compile_error() + .into(); + } + + let slot_tokens = element_slot_tokens(name, &fynix); + + let new_body = attrs + .new_fn + .as_ref() + .map(|f| quote! { #f() }) + .unwrap_or_else( + || quote! { ::core::default::Default::default() }, + ); + + let new_impl = quote! { + impl #fynix::element::ElementNew for #name { + fn new() -> Self + where + Self: ::core::marker::Sized, + { + #new_body + } + } + }; + + let children_body = attrs + .children_fn + .as_ref() + .map(|f| quote! { #f(self) }) + .or_else(|| { + children_fields.first().map(|field| { + let ident = field.ident.as_ref().unwrap(); + quote! { (&self.#ident).into_iter() } + }) + }); + + let children_fn = children_body + .map(|body| { + quote! { + fn children( + &self, + ) -> impl ::core::iter::IntoIterator< + Item = &(#fynix::element::ElementId), + > + where + Self: ::core::marker::Sized, + { + #body + } + } + }) + .unwrap_or_default(); + + let children_impl = quote! { + impl #fynix::element::ElementChildren for #name { + #children_fn + } + }; + + let element_impl = quote! { + impl #fynix::element::Element for #name {} + }; + + quote! { + #slot_tokens + #new_impl + #children_impl + #element_impl + } + .into() +} diff --git a/docs/PLANS.md b/docs/PLANS.md index aee2c78..4b13859 100644 --- a/docs/PLANS.md +++ b/docs/PLANS.md @@ -3,177 +3,71 @@ | Area | Status | |--------------------------------------|-----------------------------------| | Unit system (`src/unit.rs`) | Planned, not started | -| `#[derive(Element)]` macro | Planned, not started | -| Element composers | Planned, not started | +| Element composers | Ready to start | | Interactions & Events | Planned, not started | | Reactivity (`Signals`) | Deferred until after first render | | `TypeSlot` / typed table opt. | In progress... | --- -## `#[derive(Element)]` macro - -A derive macro that implements the `Element` trait automatically. -Requires the struct to implement `Default`, which provides `new()`. - -```rust -#[derive(Element, Default)] -struct Horizontal { - #[children] - children: Vec, - gap: f32, -} -``` +## Element composers -### `#[children]` field attribute +`Composer` is a trait separate from `Element`. It separates +required caller inputs from styleable element data: -Fields marked `#[children]` are auto-registered as the element's -child list. The macro implements `Element::children()` by calling -`into_iter()` on the marked field by default. +- The **element** (`Hierarchy`) is a normal `Element` - pure data, + styleable via `ctx.set`. +- The **composer** (`HierarchyComposer`) holds required inputs and + builds the element. It is not an `Element` itself. -An optional method chain can be specified for types that need one -to produce the iterator: +`ctx.compose()` calls `compose`, applies the style chain to the +returned element, and inserts it into the tree. ```rust -// Default - calls into_iter() on the field. -#[children] -children: Vec, - -// With method chain - calls .keys() first. -#[children(.keys())] -children: BTreeMap, +pub trait Composer { + type Element: Element; + fn compose(self, ctx: &mut FynixCtx) -> Self::Element; +} ``` -One element can have at most one `#[children]` field. - -### `#[child]` field attribute - -Fields marked `#[child]` hold a single `ElementId`. Use this for -wrapper elements that contain exactly one child and need no custom -layout logic. The macro implements both `Element::children()` and -`Element::build()` automatically. +### Usage ```rust #[derive(Element, Default)] -struct Button { - pub label: String, - #[child] - child: ElementId, +struct Hierarchy { + font_size: f32, + #[children] + children: Vec, } -``` -Generated `build` delegates sizing to the child: - -```rust -fn build(&self, id: &ElementId, constraint: Constraint, nodes: &mut ElementNodes) -> Size { - nodes.get_size(&self.child) - .map(|s| constraint.constrain(s)) - .unwrap_or(constraint.min) +struct HierarchyComposer<'a> { + filter: &'a str, } -``` - -Override `build` manually when the wrapper needs custom sizing on -top of its child. - -One element can have at most one `#[child]` or `#[children]` field, -not both. ---- - -## Element composers - -Element compose behavior is registered externally via -`ElementComposers`, keeping element structs as pure data and -backends decoupled from `fynix_elements`. - -### Types - -```rust -// Typed composer function for element E in world W. -pub type ElementComposerFn = fn(&mut E, &mut FynixCtx); - -// Typed wrapper - monomorphized for (E, W). -pub struct ElementComposer { ... } - -// Fully type-erased - stores TypeId for both E and W -// alongside a *const () function pointer. -// unsafe impl Sync - the pointer is always a fn pointer. -pub struct UntypedElementComposer { - element_id: TypeId, - world_id: TypeId, - compose_fn: *const (), -} - -// Non-generic registry keyed on element TypeId. -pub struct ElementComposers { - composers: HashMap, +impl Composer for HierarchyComposer<'_> { + type Element = Hierarchy; + fn compose(self, ctx: &mut FynixCtx) -> Hierarchy { + let mut h = Hierarchy::default(); + for _ in ctx.world.query_filtered(self.filter) { + h.children.push(ctx.add::