From bfbcdcbd0a4163dcf8d46dcca7ca758770cfe78e Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:36:45 +0800 Subject: [PATCH 01/14] Add `ElementSlot` derive macro --- Cargo.lock | 10 ++++++ Cargo.toml | 1 + crates/fynix/Cargo.toml | 1 + crates/fynix/src/lib.rs | 1 + crates/fynix_elements/src/lib.rs | 19 ++++-------- crates/fynix_macros/Cargo.toml | 17 ++++++++++ crates/fynix_macros/src/lib.rs | 53 ++++++++++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 crates/fynix_macros/Cargo.toml create mode 100644 crates/fynix_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fbd0ca2..66e0017 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,15 @@ dependencies = [ "winit", ] +[[package]] +name = "fynix_macros" +version = "0.1.0" +dependencies = [ + "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/lib.rs b/crates/fynix/src/lib.rs index 8e6e8e6..6fbdf84 100644 --- a/crates/fynix/src/lib.rs +++ b/crates/fynix/src/lib.rs @@ -13,6 +13,7 @@ use crate::element::{ElementGroup, ElementId, Elements}; use crate::resource::Resources; use crate::style::{StyleId, Styles}; +pub use fynix_macros::ElementSlot; pub use imaging; pub use rectree; diff --git a/crates/fynix_elements/src/lib.rs b/crates/fynix_elements/src/lib.rs index e0f4814..0e9751e 100644 --- a/crates/fynix_elements/src/lib.rs +++ b/crates/fynix_elements/src/lib.rs @@ -5,11 +5,10 @@ extern crate alloc; use alloc::string::String; use alloc::vec::Vec; +use fynix::ElementSlot; use fynix::Fynix; use fynix::element::meta::ElementMetas; -use fynix::element::{ - Element, ElementGroup, ElementId, ElementNodes, -}; +use fynix::element::{Element, ElementId, ElementNodes}; use fynix::imaging::kurbo::Affine; use fynix::imaging::peniko::{Brush, BrushRef, Color, Fill, Style}; use fynix::imaging::record::{Glyph, Scene, replay_transformed}; @@ -20,10 +19,7 @@ use parley::{ Alignment, AlignmentOptions, FontStyle, PositionedLayoutItem, }; use parley::{FontContext, LayoutContext}; -use typeslot::TypeSlot; - -#[derive(Default, Debug, Clone, Copy, TypeSlot)] -#[slot(ElementGroup)] +#[derive(ElementSlot, Default, Debug, Clone, Copy)] pub struct WindowSize { pub size: Size, child: Option, @@ -67,8 +63,7 @@ impl Element for WindowSize { } } -#[derive(Default, Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(ElementSlot, Default, Debug, Clone)] pub struct Horizontal { children: Vec, } @@ -120,8 +115,7 @@ impl Element for Horizontal { } } -#[derive(Default, Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(ElementSlot, Default, Debug, Clone)] pub struct Vertical { children: Vec, } @@ -168,8 +162,7 @@ impl Element for Vertical { } } -#[derive(Debug, Clone, TypeSlot)] -#[slot(ElementGroup)] +#[derive(ElementSlot, Debug, Clone)] pub struct Label { pub text: String, pub fill: Brush, diff --git a/crates/fynix_macros/Cargo.toml b/crates/fynix_macros/Cargo.toml new file mode 100644 index 0000000..6d89bde --- /dev/null +++ b/crates/fynix_macros/Cargo.toml @@ -0,0 +1,17 @@ +[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-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..924942b --- /dev/null +++ b/crates/fynix_macros/src/lib.rs @@ -0,0 +1,53 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::DeriveInput; +use syn::parse_macro_input; + +/// 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 name = &input.ident; + + quote! { + const _: () = { + static __SLOT: ::typeslot::AtomicSlot = + ::typeslot::AtomicSlot::new(); + + impl ::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() + } + } + + ::typeslot::inventory::submit! { + ::typeslot::TypeSlotEntry { + type_id: ::core::any::TypeId::of::<#name>(), + group_id: ::core::any::TypeId::of::< + ::fynix::element::ElementGroup, + >(), + slot: &__SLOT, + } + } + }; + } + .into() +} From ba58c1906d3d479427930ab589c4ab8c5a89d6f4 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:34:58 +0800 Subject: [PATCH 02/14] Add Element derive macro --- crates/fynix/src/element.rs | 116 +++++++++++++------------ crates/fynix/src/lib.rs | 2 +- crates/fynix_macros/src/lib.rs | 153 ++++++++++++++++++++++++++++++--- 3 files changed, 199 insertions(+), 72 deletions(-) diff --git a/crates/fynix/src/element.rs b/crates/fynix/src/element.rs index 265d2ef..47bce9c 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,63 @@ pub mod table; #[derive(SlotGroup)] pub struct ElementGroup; +/// 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, + ) { + } +} + /// Type-erased storage for all element instances. /// /// Internally holds one [`ElementTable`] column per element @@ -279,63 +338,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/lib.rs b/crates/fynix/src/lib.rs index 6fbdf84..e11f588 100644 --- a/crates/fynix/src/lib.rs +++ b/crates/fynix/src/lib.rs @@ -13,7 +13,7 @@ use crate::element::{ElementGroup, ElementId, Elements}; use crate::resource::Resources; use crate::style::{StyleId, Styles}; -pub use fynix_macros::ElementSlot; +pub use fynix_macros::{Element, ElementSlot}; pub use imaging; pub use rectree; diff --git a/crates/fynix_macros/src/lib.rs b/crates/fynix_macros/src/lib.rs index 924942b..2e8f05f 100644 --- a/crates/fynix_macros/src/lib.rs +++ b/crates/fynix_macros/src/lib.rs @@ -1,22 +1,13 @@ use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; +use syn::Data; use syn::DeriveInput; +use syn::Fields; +use syn::Ident; use syn::parse_macro_input; -/// 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 name = &input.ident; - +fn element_slot_tokens(name: &Ident) -> TokenStream2 { quote! { const _: () = { static __SLOT: ::typeslot::AtomicSlot = @@ -49,5 +40,139 @@ pub fn derive_element_slot(input: TokenStream) -> TokenStream { } }; } +} + +/// 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); + element_slot_tokens(&input.ident).into() +} + +/// Derives a default `fynix::element::Element` implementation +/// for a single-child element. +/// +/// Also derives `ElementSlot` - no need to add it separately. +/// +/// The struct must have exactly one field marked `#[child]`, +/// typed as `Option`. The generated impl: +/// +/// - `new` - constructs via `Default::default`. +/// - `children` - yields the `#[child]` field. +/// - `build` - returns the child's computed size, or +/// `Size::ZERO` when no child is set. +/// +/// ```ignore +/// #[derive(Element, Default)] +/// pub struct MyElement { +/// #[child] +/// child: Option, +/// } +/// ``` +#[proc_macro_derive(Element, attributes(child))] +pub fn derive_element(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let fields = match &input.data { + Data::Struct(s) => &s.fields, + _ => { + return syn::Error::new_spanned( + name, + "#[derive(Element)] only supports structs", + ) + .to_compile_error() + .into(); + } + }; + + let named = match fields { + Fields::Named(f) => &f.named, + _ => { + return syn::Error::new_spanned( + name, + "#[derive(Element)] requires named fields", + ) + .to_compile_error() + .into(); + } + }; + + let child_fields: Vec<_> = named + .iter() + .filter(|f| { + f.attrs.iter().any(|a| a.path().is_ident("child")) + }) + .collect(); + + let child_field = match child_fields.len() { + 1 => child_fields[0], + 0 => { + return syn::Error::new_spanned( + name, + "#[derive(Element)] requires exactly one \ + #[child] field", + ) + .to_compile_error() + .into(); + } + _ => { + return syn::Error::new_spanned( + child_fields[1], + "#[derive(Element)] found multiple #[child] \ + fields; only one is allowed", + ) + .to_compile_error() + .into(); + } + }; + + let child_ident = child_field.ident.as_ref().unwrap(); + let slot_tokens = element_slot_tokens(name); + + quote! { + #slot_tokens + + impl ::fynix::element::Element for #name { + fn new() -> Self + where + Self: Sized, + { + ::core::default::Default::default() + } + + fn children( + &self, + ) -> impl ::core::iter::IntoIterator< + Item = &::fynix::element::ElementId, + > + where + Self: Sized, + { + self.#child_ident.iter() + } + + fn build( + &self, + _id: &::fynix::element::ElementId, + constraint: ::fynix::rectree::Constraint, + nodes: &mut ::fynix::element::ElementNodes, + ) -> ::fynix::rectree::Size { + use ::fynix::rectree::NodeContext as _; + self.#child_ident + .as_ref() + .map(|c| nodes.get_size(c)) + .unwrap_or(::fynix::rectree::Size::ZERO) + } + } + } .into() } From 840cf31ca3e04f851fb1bb61ce5b952e095ed7be Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:47:41 +0800 Subject: [PATCH 03/14] Use `PLACEHOLDER` as `Default` for `ElementId` --- crates/fynix/src/id.rs | 6 ++++++ crates/fynix/src/lib.rs | 1 - crates/fynix_elements/src/lib.rs | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/fynix/src/id.rs b/crates/fynix/src/id.rs index 8717600..334d702 100644 --- a/crates/fynix/src/id.rs +++ b/crates/fynix/src/id.rs @@ -34,6 +34,12 @@ impl GenId { } } +impl Default for GenId { + fn default() -> Self { + Self::PLACEHOLDER + } +} + impl Display for GenId { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{}v{}", self.id, self.generation) diff --git a/crates/fynix/src/lib.rs b/crates/fynix/src/lib.rs index e11f588..8e6e8e6 100644 --- a/crates/fynix/src/lib.rs +++ b/crates/fynix/src/lib.rs @@ -13,7 +13,6 @@ use crate::element::{ElementGroup, ElementId, Elements}; use crate::resource::Resources; use crate::style::{StyleId, Styles}; -pub use fynix_macros::{Element, ElementSlot}; pub use imaging; pub use rectree; diff --git a/crates/fynix_elements/src/lib.rs b/crates/fynix_elements/src/lib.rs index 0e9751e..b50d9d4 100644 --- a/crates/fynix_elements/src/lib.rs +++ b/crates/fynix_elements/src/lib.rs @@ -5,10 +5,9 @@ extern crate alloc; use alloc::string::String; use alloc::vec::Vec; -use fynix::ElementSlot; use fynix::Fynix; use fynix::element::meta::ElementMetas; -use fynix::element::{Element, ElementId, ElementNodes}; +use fynix::element::{Element, ElementId, ElementNodes, ElementSlot}; use fynix::imaging::kurbo::Affine; use fynix::imaging::peniko::{Brush, BrushRef, Color, Fill, Style}; use fynix::imaging::record::{Glyph, Scene, replay_transformed}; From 3d897f84575a15c164ce458b59b5f1dfc81b8cc0 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sat, 18 Apr 2026 18:16:59 +0800 Subject: [PATCH 04/14] Auto-detect fynix crate path and unify child iteration --- Cargo.lock | 1 + crates/fynix/src/id.rs | 10 ++++++ crates/fynix/src/lib.rs | 1 + crates/fynix_macros/Cargo.toml | 1 + crates/fynix_macros/src/lib.rs | 64 ++++++++++++++++++++++------------ 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66e0017..5e800a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -619,6 +619,7 @@ dependencies = [ name = "fynix_macros" version = "0.1.0" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn", diff --git a/crates/fynix/src/id.rs b/crates/fynix/src/id.rs index 334d702..7e2fa26 100644 --- a/crates/fynix/src/id.rs +++ b/crates/fynix/src/id.rs @@ -1,6 +1,7 @@ use core::fmt::{Debug, Display, Formatter, Result}; use core::hash::Hash; use core::marker::PhantomData; +use core::slice::Iter; use alloc::vec::Vec; @@ -93,6 +94,15 @@ impl PartialOrd for GenId { impl Copy for GenId {} +impl<'a, T> IntoIterator for &'a GenId { + type Item = &'a GenId; + type IntoIter = Iter<'a, GenId>; + + fn into_iter(self) -> Self::IntoIter { + core::slice::from_ref(self).iter() + } +} + impl Clone for GenId { fn clone(&self) -> Self { *self diff --git a/crates/fynix/src/lib.rs b/crates/fynix/src/lib.rs index 8e6e8e6..f96851a 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; diff --git a/crates/fynix_macros/Cargo.toml b/crates/fynix_macros/Cargo.toml index 6d89bde..3f07b76 100644 --- a/crates/fynix_macros/Cargo.toml +++ b/crates/fynix_macros/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true 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 index 2e8f05f..d560dfd 100644 --- a/crates/fynix_macros/src/lib.rs +++ b/crates/fynix_macros/src/lib.rs @@ -1,20 +1,37 @@ 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::Fields; -use syn::Ident; use syn::parse_macro_input; -fn element_slot_tokens(name: &Ident) -> TokenStream2 { +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: ::typeslot::AtomicSlot = - ::typeslot::AtomicSlot::new(); + static __SLOT: #fynix::typeslot::AtomicSlot = + #fynix::typeslot::AtomicSlot::new(); - impl ::typeslot::TypeSlot< - ::fynix::element::ElementGroup, + impl #fynix::typeslot::TypeSlot< + #fynix::element::ElementGroup, > for #name { #[inline] fn try_slot() -> ::core::option::Option { @@ -29,11 +46,11 @@ fn element_slot_tokens(name: &Ident) -> TokenStream2 { } } - ::typeslot::inventory::submit! { - ::typeslot::TypeSlotEntry { + #fynix::typeslot::inventory::submit! { + #fynix::typeslot::TypeSlotEntry { type_id: ::core::any::TypeId::of::<#name>(), group_id: ::core::any::TypeId::of::< - ::fynix::element::ElementGroup, + #fynix::element::ElementGroup, >(), slot: &__SLOT, } @@ -54,7 +71,8 @@ fn element_slot_tokens(name: &Ident) -> TokenStream2 { #[proc_macro_derive(ElementSlot)] pub fn derive_element_slot(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - element_slot_tokens(&input.ident).into() + let fynix = fynix_crate(); + element_slot_tokens(&input.ident, &fynix).into() } /// Derives a default `fynix::element::Element` implementation @@ -81,6 +99,7 @@ pub fn derive_element_slot(input: TokenStream) -> TokenStream { pub fn derive_element(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; + let fynix = fynix_crate(); let fields = match &input.data { Data::Struct(s) => &s.fields, @@ -136,12 +155,12 @@ pub fn derive_element(input: TokenStream) -> TokenStream { }; let child_ident = child_field.ident.as_ref().unwrap(); - let slot_tokens = element_slot_tokens(name); + let slot_tokens = element_slot_tokens(name, &fynix); quote! { #slot_tokens - impl ::fynix::element::Element for #name { + impl #fynix::element::Element for #name { fn new() -> Self where Self: Sized, @@ -152,25 +171,26 @@ pub fn derive_element(input: TokenStream) -> TokenStream { fn children( &self, ) -> impl ::core::iter::IntoIterator< - Item = &::fynix::element::ElementId, + Item = &(#fynix::element::ElementId), > where Self: Sized, { - self.#child_ident.iter() + (&self.#child_ident).into_iter() } fn build( &self, - _id: &::fynix::element::ElementId, - constraint: ::fynix::rectree::Constraint, - nodes: &mut ::fynix::element::ElementNodes, - ) -> ::fynix::rectree::Size { - use ::fynix::rectree::NodeContext as _; - self.#child_ident - .as_ref() + _id: &(#fynix::element::ElementId), + constraint: #fynix::rectree::Constraint, + nodes: &mut #fynix::element::ElementNodes, + ) -> #fynix::rectree::Size { + use #fynix::rectree::NodeContext as _; + (&self.#child_ident) + .into_iter() .map(|c| nodes.get_size(c)) - .unwrap_or(::fynix::rectree::Size::ZERO) + .next() + .unwrap_or(#fynix::rectree::Size::ZERO) } } } From 33a367979f875812df825f66d96b35b975f6a3ca Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:31:35 +0800 Subject: [PATCH 05/14] Split Element trait and update derive macro Introduce ElementNew and ElementChildren as supertraits of Element. #[derive(Element)] now generates both, along with ElementSlot. build() remains a manual impl. Supports #[children] field attribute and #[element(new = fn, children = fn)] overrides. --- crates/fynix/src/ctx.rs | 18 +-- crates/fynix/src/element.rs | 33 +++-- crates/fynix_elements/src/lib.rs | 70 ++--------- crates/fynix_macros/src/lib.rs | 208 +++++++++++++++++++++---------- docs/PLANS.md | 70 ----------- 5 files changed, 180 insertions(+), 219 deletions(-) diff --git a/crates/fynix/src/ctx.rs b/crates/fynix/src/ctx.rs index 74f2952..7200198 100644 --- a/crates/fynix/src/ctx.rs +++ b/crates/fynix/src/ctx.rs @@ -103,23 +103,17 @@ mod tests { use field_path::field_accessor; use rectree::{Constraint, NodeContext, Size, Vec2}; - use typeslot::TypeSlot; - use crate::element::{ElementGroup, ElementNodes}; + use crate::element::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() - } - fn build( &self, _id: &ElementId, @@ -133,9 +127,9 @@ mod tests { } } - #[derive(Default, Clone, TypeSlot)] - #[slot(ElementGroup)] + #[derive(Element, Default, Clone)] struct Vertical { + #[children] children: Vec, } @@ -146,10 +140,6 @@ mod tests { } impl Element for Vertical { - fn new() -> Self { - Self::default() - } - fn build( &self, _id: &ElementId, diff --git a/crates/fynix/src/element.rs b/crates/fynix/src/element.rs index 47bce9c..d3bd13d 100644 --- a/crates/fynix/src/element.rs +++ b/crates/fynix/src/element.rs @@ -17,28 +17,41 @@ pub mod table; #[derive(SlotGroup)] pub struct ElementGroup; -/// 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. +/// Constructs a default (unstyled) instance of an element. /// -/// Styles are applied immediately after construction by the -/// build context. -pub trait Element: TypeSlot + 'static { +/// Implement this alongside [`Element`]. Styles are applied +/// immediately after construction by the build context. +pub trait ElementNew { fn new() -> Self where Self: Sized; +} +/// Enumerates the children of an element. +/// +/// The default implementation yields no children, suitable +/// for leaf elements. +pub trait ElementChildren { fn children(&self) -> impl IntoIterator where Self: Sized, { [] } +} +/// Trait for element types. +/// +/// Implement this for any type you want to add to the +/// element tree via +/// [`FynixCtx::add`](crate::ctx::FynixCtx::add). +/// +/// Use `#[derive(Element)]` to derive [`ElementNew`] and +/// [`ElementChildren`] automatically; implement `build` +/// manually. +pub trait Element: + ElementNew + ElementChildren + TypeSlot + 'static +{ fn constrain(&self, parent_constraint: Constraint) -> Constraint { parent_constraint } diff --git a/crates/fynix_elements/src/lib.rs b/crates/fynix_elements/src/lib.rs index b50d9d4..3f86716 100644 --- a/crates/fynix_elements/src/lib.rs +++ b/crates/fynix_elements/src/lib.rs @@ -7,7 +7,7 @@ use alloc::vec::Vec; use fynix::Fynix; use fynix::element::meta::ElementMetas; -use fynix::element::{Element, ElementId, ElementNodes, ElementSlot}; +use fynix::element::{Element, ElementId, ElementNodes}; use fynix::imaging::kurbo::Affine; use fynix::imaging::peniko::{Brush, BrushRef, Color, Fill, Style}; use fynix::imaging::record::{Glyph, Scene, replay_transformed}; @@ -18,9 +18,11 @@ use parley::{ Alignment, AlignmentOptions, FontStyle, PositionedLayoutItem, }; use parley::{FontContext, LayoutContext}; -#[derive(ElementSlot, Default, Debug, Clone, Copy)] + +#[derive(Element, Default, Debug, Clone, Copy)] pub struct WindowSize { pub size: Size, + #[children] child: Option, } @@ -31,20 +33,6 @@ 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() - } - fn constrain( &self, _parent_constraint: Constraint, @@ -62,8 +50,9 @@ impl Element for WindowSize { } } -#[derive(ElementSlot, Default, Debug, Clone)] +#[derive(Element, Default, Debug, Clone)] pub struct Horizontal { + #[children] children: Vec, } @@ -75,25 +64,6 @@ 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() - } - fn build( &self, _id: &ElementId, @@ -114,8 +84,9 @@ impl Element for Horizontal { } } -#[derive(ElementSlot, Default, Debug, Clone)] +#[derive(Element, Default, Debug, Clone)] pub struct Vertical { + #[children] children: Vec, } @@ -127,20 +98,6 @@ 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() - } - fn build( &self, _id: &ElementId, @@ -161,7 +118,7 @@ impl Element for Vertical { } } -#[derive(ElementSlot, Debug, Clone)] +#[derive(Element, Debug, Clone)] pub struct Label { pub text: String, pub fill: Brush, @@ -170,11 +127,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), @@ -183,7 +137,9 @@ impl Element for Label { alignment: Default::default(), } } +} +impl Element for Label { fn build( &self, id: &ElementId, diff --git a/crates/fynix_macros/src/lib.rs b/crates/fynix_macros/src/lib.rs index d560dfd..b7c80cb 100644 --- a/crates/fynix_macros/src/lib.rs +++ b/crates/fynix_macros/src/lib.rs @@ -7,6 +7,7 @@ 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; @@ -75,32 +76,88 @@ pub fn derive_element_slot(input: TokenStream) -> TokenStream { element_slot_tokens(&input.ident, &fynix).into() } -/// Derives a default `fynix::element::Element` implementation -/// for a single-child element. +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` and `ElementChildren` for the annotated +/// struct. Also derives `ElementSlot` — no need to add it +/// separately. +/// +/// ## `ElementNew` /// -/// Also derives `ElementSlot` - no need to add it separately. +/// By default calls `Default::default()`, requiring the struct to +/// also `#[derive(Default)]`. Override with: /// -/// The struct must have exactly one field marked `#[child]`, -/// typed as `Option`. The generated impl: +/// ```ignore +/// #[element(new = my_constructor_fn)] +/// ``` /// -/// - `new` - constructs via `Default::default`. -/// - `children` - yields the `#[child]` field. -/// - `build` - returns the child's computed size, or -/// `Size::ZERO` when no child is set. +/// where `my_constructor_fn` is a `fn() -> Self`. +/// +/// ## `ElementChildren` +/// +/// Mark one field `#[children]` for the standard iterator impl: /// /// ```ignore /// #[derive(Element, Default)] /// pub struct MyElement { -/// #[child] -/// child: Option, +/// #[children] +/// child: ElementId, // or Option, or Vec /// } /// ``` -#[proc_macro_derive(Element, attributes(child))] +/// +/// Or override entirely with: +/// +/// ```ignore +/// #[element(children = my_children_fn)] +/// ``` +/// +/// where `my_children_fn` is a `fn(&Self) -> impl IntoIterator`. +/// +/// If neither is present the default (no children) is used. +/// +/// ## `build` +/// +/// Not generated — implement `Element::build` 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 fields = match &input.data { Data::Struct(s) => &s.fields, _ => { @@ -125,74 +182,89 @@ pub fn derive_element(input: TokenStream) -> TokenStream { } }; - let child_fields: Vec<_> = named + let children_fields: Vec<_> = named .iter() .filter(|f| { - f.attrs.iter().any(|a| a.path().is_ident("child")) + f.attrs.iter().any(|a| a.path().is_ident("children")) }) .collect(); - let child_field = match child_fields.len() { - 1 => child_fields[0], - 0 => { - return syn::Error::new_spanned( - name, - "#[derive(Element)] requires exactly one \ - #[child] field", - ) - .to_compile_error() - .into(); - } - _ => { - return syn::Error::new_spanned( - child_fields[1], - "#[derive(Element)] found multiple #[child] \ - fields; only one is allowed", - ) - .to_compile_error() - .into(); - } - }; + 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 child_ident = child_field.ident.as_ref().unwrap(); let slot_tokens = element_slot_tokens(name, &fynix); - quote! { - #slot_tokens - - impl #fynix::element::Element for #name { - fn new() -> Self - where - Self: Sized, - { - ::core::default::Default::default() + let new_impl = match &attrs.new_fn { + Some(f) => quote! { + impl #fynix::element::ElementNew for #name { + fn new() -> Self + where + Self: ::core::marker::Sized, + { + #f() + } } - - fn children( - &self, - ) -> impl ::core::iter::IntoIterator< - Item = &(#fynix::element::ElementId), - > - where - Self: Sized, - { - (&self.#child_ident).into_iter() + }, + None => quote! { + impl #fynix::element::ElementNew for #name { + fn new() -> Self + where + Self: ::core::marker::Sized, + { + ::core::default::Default::default() + } } + }, + }; - fn build( - &self, - _id: &(#fynix::element::ElementId), - constraint: #fynix::rectree::Constraint, - nodes: &mut #fynix::element::ElementNodes, - ) -> #fynix::rectree::Size { - use #fynix::rectree::NodeContext as _; - (&self.#child_ident) - .into_iter() - .map(|c| nodes.get_size(c)) - .next() - .unwrap_or(#fynix::rectree::Size::ZERO) + let children_impl = if let Some(f) = &attrs.children_fn { + Some(quote! { + impl #fynix::element::ElementChildren for #name { + fn children( + &self, + ) -> impl ::core::iter::IntoIterator< + Item = &(#fynix::element::ElementId), + > + where + Self: ::core::marker::Sized, + { + #f(self) + } } - } + }) + } else if let Some(field) = children_fields.first() { + let ident = field.ident.as_ref().unwrap(); + Some(quote! { + impl #fynix::element::ElementChildren for #name { + fn children( + &self, + ) -> impl ::core::iter::IntoIterator< + Item = &(#fynix::element::ElementId), + > + where + Self: ::core::marker::Sized, + { + (&self.#ident).into_iter() + } + } + }) + } else { + Some(quote! { + impl #fynix::element::ElementChildren for #name {} + }) + }; + + quote! { + #slot_tokens + #new_impl + #children_impl } .into() } diff --git a/docs/PLANS.md b/docs/PLANS.md index aee2c78..60a3fce 100644 --- a/docs/PLANS.md +++ b/docs/PLANS.md @@ -3,7 +3,6 @@ | Area | Status | |--------------------------------------|-----------------------------------| | Unit system (`src/unit.rs`) | Planned, not started | -| `#[derive(Element)]` macro | Planned, not started | | Element composers | Planned, not started | | Interactions & Events | Planned, not started | | Reactivity (`Signals`) | Deferred until after first render | @@ -11,75 +10,6 @@ --- -## `#[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, -} -``` - -### `#[children]` field attribute - -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. - -An optional method chain can be specified for types that need one -to produce the iterator: - -```rust -// Default - calls into_iter() on the field. -#[children] -children: Vec, - -// With method chain - calls .keys() first. -#[children(.keys())] -children: BTreeMap, -``` - -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. - -```rust -#[derive(Element, Default)] -struct Button { - pub label: String, - #[child] - child: ElementId, -} -``` - -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) -} -``` - -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 From 78e4d9123c034c59af66b57d6e0ed0c7d365c260 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:41:28 +0800 Subject: [PATCH 06/14] Fix broken intra-doc links for ElementChildren::children --- crates/fynix/src/element/meta.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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, From 77a52af3665e638a0d4d2cc72fd8bfa24d255aac Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:48:27 +0800 Subject: [PATCH 07/14] Update Element composers plan --- docs/PLANS.md | 111 ++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 75 deletions(-) diff --git a/docs/PLANS.md b/docs/PLANS.md index 60a3fce..29962f3 100644 --- a/docs/PLANS.md +++ b/docs/PLANS.md @@ -3,7 +3,7 @@ | Area | Status | |--------------------------------------|-----------------------------------| | Unit system (`src/unit.rs`) | 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... | @@ -12,98 +12,59 @@ ## Element composers -Element compose behavior is registered externally via -`ElementComposers`, keeping element structs as pure data and -backends decoupled from `fynix_elements`. - -### Types +`Composer` is a trait separate from `Element`. Element structs +remain pure data; composition logic - how children are built for a +given world - lives in the `Composer` impl. This keeps +`fynix_elements` decoupled from any backend. ```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, +pub trait Composer { + fn compose(self, ctx: &mut FynixCtx) -> ElementId; } ``` -`UntypedElementComposer::new::(f)` is `const` - both -`TypeId::of` calls are const-stable when `T: 'static`. - -### Auto-registration via `linkme` - -A `linkme` distributed slice collects composers across crates: - -```rust -#[distributed_slice] -pub static ELEMENT_COMPOSERS: [UntypedElementComposer] = [..]; -``` - -At startup, `ElementComposers` is populated from the slice in -one pass. +`compose` takes ownership of `self`: the struct is the input data, +consumed to build the subtree. -### `#[fynix(compose)]` proc-macro - -A proc-macro attribute handles registration boilerplate. `E` -is inferred from the first parameter (`&mut E`), `W` from -`FynixCtx` in the second: +### Usage ```rust -#[fynix(compose)] -fn compose_hierarchy_bevy( - e: &mut Hierarchy, - ctx: &mut FynixCtx, -) { - let h = ctx.world.query(..); - ctx.add::