From 61bde544665455ed79b77aa8f0b5ed0306f34ff9 Mon Sep 17 00:00:00 2001
From: MrGVSV <gino.valente.code@gmail.com>
Date: Thu, 3 Feb 2022 12:12:34 -0800
Subject: [PATCH] Integrated props into functional components

This code doesn't work though. Running it shows that widgets are not
aligned/sized properly anymore. Will need fixing.
---
 examples/counter.rs                           |   7 +-
 kayak_core/src/lib.rs                         |   4 +-
 kayak_render_macros/src/arc_function.rs       |  14 -
 kayak_render_macros/src/child.rs              |   2 +
 kayak_render_macros/src/children.rs           |   6 +-
 kayak_render_macros/src/function_component.rs | 345 ++++++++++--------
 kayak_render_macros/src/lib.rs                |   4 +-
 kayak_render_macros/src/widget.rs             |  59 ++-
 kayak_render_macros/src/widget_attributes.rs  | 101 ++++-
 kayak_render_macros/src/widget_builder.rs     |  32 ++
 src/widgets/app.rs                            |  24 +-
 src/widgets/background.rs                     |  27 +-
 src/widgets/button.rs                         |  28 +-
 src/widgets/clip.rs                           |  24 +-
 src/widgets/element.rs                        |  27 +-
 src/widgets/fold.rs                           |  39 +-
 src/widgets/if_element.rs                     |  30 +-
 src/widgets/image.rs                          |  30 +-
 src/widgets/inspector.rs                      |  12 +-
 src/widgets/nine_patch.rs                     |  33 +-
 src/widgets/text.rs                           |  34 +-
 src/widgets/text_box.rs                       |  35 +-
 src/widgets/tooltip.rs                        |  57 ++-
 src/widgets/window.rs                         |  38 +-
 24 files changed, 711 insertions(+), 301 deletions(-)
 delete mode 100644 kayak_render_macros/src/arc_function.rs
 create mode 100644 kayak_render_macros/src/widget_builder.rs

diff --git a/examples/counter.rs b/examples/counter.rs
index 670f14a..a301e4c 100644
--- a/examples/counter.rs
+++ b/examples/counter.rs
@@ -5,14 +5,17 @@ use bevy::{
 };
 use kayak_ui::bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle};
 use kayak_ui::core::{
-    render, rsx,
+    render, rsx, WidgetProps,
     styles::{Style, StyleProp, Units},
     use_state, widget, EventType, Index, OnEvent,
 };
 use kayak_ui::widgets::{App, Button, Text, Window};
 
+#[derive(WidgetProps, Default, Debug, PartialEq, Clone)]
+pub struct CounterProps {}
+
 #[widget]
-fn Counter(context: &mut KayakContext) {
+fn Counter(props: CounterProps) {
     let text_styles = Style {
         bottom: StyleProp::Value(Units::Stretch(1.0)),
         left: StyleProp::Value(Units::Stretch(0.1)),
diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs
index b7c6f77..94f06d1 100644
--- a/kayak_core/src/lib.rs
+++ b/kayak_core/src/lib.rs
@@ -33,14 +33,14 @@ pub use context_ref::KayakContextRef;
 pub use cursor::PointerEvents;
 pub use event::*;
 pub use focus_tree::FocusTree;
-pub use fragment::Fragment;
+pub use fragment::{Fragment, FragmentProps};
 pub use generational_arena::{Arena, Index};
 pub use input_event::*;
 pub use keyboard::{KeyboardEvent, KeyboardModifiers};
 pub use keys::KeyCode;
 pub use resources::Resources;
 pub use tree::{Tree, WidgetTree};
-pub use vec::VecTracker;
+pub use vec::{VecTracker, VecTrackerProps};
 pub use widget::{BaseWidget, Widget, WidgetProps};
 
 pub mod derivative {
diff --git a/kayak_render_macros/src/arc_function.rs b/kayak_render_macros/src/arc_function.rs
deleted file mode 100644
index e8bbd5a..0000000
--- a/kayak_render_macros/src/arc_function.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use proc_macro2::TokenStream;
-use quote::quote;
-
-pub fn build_arc_function(
-    widget_name: TokenStream,
-    children_quotes: TokenStream,
-    index: usize,
-) -> TokenStream {
-    quote! {
-        let children = children.clone();
-        let #widget_name = #children_quotes;
-        context.add_widget(#widget_name, #index);
-    }
-}
diff --git a/kayak_render_macros/src/child.rs b/kayak_render_macros/src/child.rs
index f832af6..04f7d15 100644
--- a/kayak_render_macros/src/child.rs
+++ b/kayak_render_macros/src/child.rs
@@ -1,5 +1,7 @@
+use proc_macro_error::emit_error;
 use quote::{quote, ToTokens};
 use syn::parse::{Parse, ParseStream, Result};
+use syn::spanned::Spanned;
 
 use crate::widget::Widget;
 
diff --git a/kayak_render_macros/src/children.rs b/kayak_render_macros/src/children.rs
index fb79524..baa33e1 100644
--- a/kayak_render_macros/src/children.rs
+++ b/kayak_render_macros/src/children.rs
@@ -1,6 +1,6 @@
 use std::collections::HashSet;
 
-use crate::{arc_function::build_arc_function, attribute::Attribute, child::{walk_block_to_variable, Child}, get_core_crate};
+use crate::{widget_builder::build_widget_stream, attribute::Attribute, child::{walk_block_to_variable, Child}, get_core_crate};
 use quote::{quote, ToTokens};
 use syn::parse::{Parse, ParseStream, Result};
 
@@ -80,7 +80,7 @@ impl Children {
                             #(#children_quotes)*.clone()
                         }
                     } else {
-                        let children_builder = build_arc_function(
+                        let children_builder = build_widget_stream(
                             quote! { child_widget },
                             quote! { #(#children_quotes),* },
                             0,
@@ -144,7 +144,7 @@ impl Children {
                 for i in 0..children_quotes.len() {
                     output.push(quote! { #base_clones_inner });
                     let name: proc_macro2::TokenStream = format!("child{}", i).parse().unwrap();
-                    let child = build_arc_function(quote! { #name }, children_quotes[i].clone(), i);
+                    let child = build_widget_stream(quote! { #name }, children_quotes[i].clone(), i);
                     output.push(quote! { #child });
                 }
 
diff --git a/kayak_render_macros/src/function_component.rs b/kayak_render_macros/src/function_component.rs
index 021c300..b94e91a 100644
--- a/kayak_render_macros/src/function_component.rs
+++ b/kayak_render_macros/src/function_component.rs
@@ -1,6 +1,7 @@
 use proc_macro::TokenStream;
-use proc_macro_error::emit_error;
+use proc_macro_error::{emit_error, emit_warning};
 use quote::{quote, ToTokens};
+use syn::{FnArg, Pat, Type};
 use syn::spanned::Spanned;
 use crate::get_core_crate;
 
@@ -15,191 +16,231 @@ impl Default for WidgetArguments {
 }
 
 pub fn create_function_widget(f: syn::ItemFn, widget_arguments: WidgetArguments) -> TokenStream {
-    let struct_name = f.sig.ident;
+    let struct_name = f.sig.ident.clone();
     let (impl_generics, ty_generics, where_clause) = f.sig.generics.split_for_impl();
-    let inputs = f.sig.inputs;
-    let block = f.block;
-    let vis = f.vis;
 
-    let kayak_core = get_core_crate();
+    if f.sig.inputs.len() != 1 {
+        let span = if f.sig.inputs.len() > 0 {
+            f.sig.inputs.span()
+        } else {
+            f.sig.span()
+        };
+        emit_error!(span, "Functional widgets expect exactly one argument (their props), but was given {}", f.sig.inputs.len());
+    }
 
-    let mut input_names: Vec<_> = inputs
-        .iter()
-        .filter_map(|argument| match argument {
-            syn::FnArg::Typed(typed) => {
-                let typed_info = typed.ty.to_token_stream().to_string();
-                let attr_info = typed.pat.to_token_stream().to_string();
-                if (typed_info.contains("KayakContext") && !typed_info.contains("Fn"))
-                    || (attr_info.contains("styles") && typed_info.contains("Style"))
-                {
-                    None
-                } else {
-                    Some(typed)
+    let (props, prop_type) = match f.sig.inputs.first().unwrap() {
+        FnArg::Typed(typed) => {
+            let ident = match *typed.pat.clone() {
+                Pat::Ident(ident) => {
+                    ident.ident
+                }
+                err => {
+                    emit_error!(err.span(), "Expected identifier, but got {:?}", err);
+                    return TokenStream::new()
                 }
-            }
-            syn::FnArg::Receiver(rec) => {
-                emit_error!(rec.span(), "Don't use `self` on widgets");
-                None
-            }
-        })
-        .map(|value| {
-            let pat = &value.pat;
-            quote!(#pat)
-        })
-        .collect();
-
-    let mut input_block_names: Vec<_> = inputs
-        .iter()
-        .filter(|input| {
-            let input = (quote! { #input }).to_string();
-            !(input.contains("parent_styles")
-                || (input.contains("KayakContext") && !input.contains("Fn")))
-        })
-        .map(|item| quote! { #item })
-        .collect();
-    input_block_names.iter_mut().for_each(|input| {
-        let input_string = (quote! { #input }).to_string();
-        if input_string.contains("children : Children") {
-            *input = quote! {
-                #[derivative(Debug = "ignore", PartialEq = "ignore")]
-                pub children: Children
             };
-        } else if input_string.contains("on_event : Option < OnEvent >") {
-            *input = quote! {
-                #[derivative(Debug = "ignore", PartialEq = "ignore")]
-                pub on_event: Option<#kayak_core::OnEvent>
+
+            let ty = match *typed.ty.clone() {
+                Type::Path(type_path) => {
+                    type_path.path
+                },
+                err => {
+                    emit_error!(err.span(), "Invalid widget prop type: {:?}", err);
+                    return TokenStream::new()
+                }
             };
-        } else {
-            *input = quote! {
-                pub #input
-            }
-        }
-    });
 
-    let focusable_default = if widget_arguments.focusable {
-        "Some(true)"
-    } else {
-        "None"
+            (ident, ty)
+        },
+        FnArg::Receiver(receiver) => {
+            emit_error!(receiver.span(), "Functional widget cannot use 'self'");
+            return TokenStream::new()
+        }
     };
 
-    let missing_struct_inputs = vec![
-        (
-            vec![
-                "styles : Option < Style >",
-                "styles : Option< kayak_ui :: core :: styles :: Style >",
-            ],
-            quote! {
-                #[derivative(Default(value="None"))]
-                pub styles: Option<#kayak_core::styles::Style>
-            },
-        ),
-        (
-            vec!["children : Children"],
-            quote! {
-                #[derivative(Default(value="None"), Debug = "ignore", PartialEq = "ignore")]
-                pub children: #kayak_core::Children
-            },
-        ),
-        (
-            vec![
-                "on_event : Option < OnEvent >",
-                "on_event : Option < kayak_ui :: core :: OnEvent >",
-                "on_event : Option <\nkayak_ui :: core :: OnEvent >",
-            ],
-            quote! {
-                #[derivative(Default(value="None"), Debug = "ignore", PartialEq = "ignore")]
-                pub on_event: Option<#kayak_core::OnEvent>
-            },
-        ),
-        (
-            vec!["focusable : Option < bool >"],
-            quote! {
-                #[derivative(Default(value=#focusable_default))]
-                pub focusable: Option<bool>
-            },
-        ),
-    ];
-
-    for (names, token) in missing_struct_inputs {
-        if !input_block_names.iter().any(|block_name| {
-            names
-                .iter()
-                .any(|name| block_name.to_string().contains(name))
-        }) {
-            input_block_names.push(token);
-        } else {
-        }
-    }
+    let block = f.block;
+    let vis = f.vis;
 
-    let inputs_block = quote!(
-        #(#input_block_names),*
-    );
-
-    if !input_names
-        .iter()
-        .any(|item_name| item_name.to_string().contains("children"))
-    {
-        input_names.push(quote! {
-            children
-        });
-    }
+    let kayak_core = get_core_crate();
 
-    let inputs_reading_ref = if inputs.len() == 0 {
-        quote! {
-            let #struct_name { children, styles, .. } = self;
-        }
-    } else {
-        quote!(
-            let #struct_name { #(#input_names),*, styles, .. } = self;
-            #(let #input_names = #input_names.clone();)*
-        )
-    };
 
-    TokenStream::from(quote! {
-        use #kayak_core::derivative::*;
+    // TODO: See if this is still needed. If not, remove it
+    // let mut input_names: Vec<_> = inputs
+    //     .iter()
+    //     .filter_map(|argument| match argument {
+    //         syn::FnArg::Typed(typed) => {
+    //             let typed_info = typed.ty.to_token_stream().to_string();
+    //             let attr_info = typed.pat.to_token_stream().to_string();
+    //             if (typed_info.contains("KayakContext") && !typed_info.contains("Fn"))
+    //                 || (attr_info.contains("styles") && typed_info.contains("Style"))
+    //             {
+    //                 None
+    //             } else {
+    //                 Some(typed)
+    //             }
+    //         }
+    //         syn::FnArg::Receiver(rec) => {
+    //             emit_error!(rec.span(), "Don't use `self` on widgets");
+    //             None
+    //         }
+    //     })
+    //     .map(|value| {
+    //         let pat = &value.pat;
+    //         quote!(#pat)
+    //     })
+    //     .collect();
+    //
+    // let mut input_block_names: Vec<_> = inputs
+    //     .iter()
+    //     .filter(|input| {
+    //         let input = (quote! { #input }).to_string();
+    //         !(input.contains("parent_styles")
+    //             || (input.contains("KayakContext") && !input.contains("Fn")))
+    //     })
+    //     .map(|item| quote! { #item })
+    //     .collect();
+    // input_block_names.iter_mut().for_each(|input| {
+    //     let input_string = (quote! { #input }).to_string();
+    //     if input_string.contains("children : Children") {
+    //         *input = quote! {
+    //             #[derivative(Debug = "ignore", PartialEq = "ignore")]
+    //             pub children: Children
+    //         };
+    //     } else if input_string.contains("on_event : Option < OnEvent >") {
+    //         *input = quote! {
+    //             #[derivative(Debug = "ignore", PartialEq = "ignore")]
+    //             pub on_event: Option<#kayak_core::OnEvent>
+    //         };
+    //     } else {
+    //         *input = quote! {
+    //             pub #input
+    //         }
+    //     }
+    // });
+
+    // let focusable_default = if widget_arguments.focusable {
+    //     "Some(true)"
+    // } else {
+    //     "None"
+    // };
+    //
+    // let missing_struct_inputs = vec![
+    //     (
+    //         vec![
+    //             "styles : Option < Style >",
+    //             "styles : Option< kayak_ui :: core :: styles :: Style >",
+    //         ],
+    //         quote! {
+    //             #[derivative(Default(value="None"))]
+    //             pub styles: Option<#kayak_core::styles::Style>
+    //         },
+    //     ),
+    //     (
+    //         vec!["children : Children"],
+    //         quote! {
+    //             #[derivative(Default(value="None"), Debug = "ignore", PartialEq = "ignore")]
+    //             pub children: #kayak_core::Children
+    //         },
+    //     ),
+    //     (
+    //         vec![
+    //             "on_event : Option < OnEvent >",
+    //             "on_event : Option < kayak_ui :: core :: OnEvent >",
+    //             "on_event : Option <\nkayak_ui :: core :: OnEvent >",
+    //         ],
+    //         quote! {
+    //             #[derivative(Default(value="None"), Debug = "ignore", PartialEq = "ignore")]
+    //             pub on_event: Option<#kayak_core::OnEvent>
+    //         },
+    //     ),
+    //     (
+    //         vec!["focusable : Option < bool >"],
+    //         quote! {
+    //             #[derivative(Default(value=#focusable_default))]
+    //             pub focusable: Option<bool>
+    //         },
+    //     ),
+    // ];
+
+    // for (names, token) in missing_struct_inputs {
+    //     if !input_block_names.iter().any(|block_name| {
+    //         names
+    //             .iter()
+    //             .any(|name| block_name.to_string().contains(name))
+    //     }) {
+    //         input_block_names.push(token);
+    //     } else {
+    //     }
+    // }
+
+    // let inputs_block = quote!(
+    //     #(#input_block_names),*
+    // );
+    //
+    // if !input_names
+    //     .iter()
+    //     .any(|item_name| item_name.to_string().contains("children"))
+    // {
+    //     input_names.push(quote! {
+    //         children
+    //     });
+    // }
+
+    // let inputs_reading_ref = if inputs.len() == 0 {
+    //     quote! {
+    //         let #struct_name { children, styles, .. } = self;
+    //     }
+    // } else {
+    //     quote!(
+    //         let #struct_name { #(#input_names),*, styles, .. } = self;
+    //         #(let #input_names = #input_names.clone();)*
+    //     )
+    // };
 
-        #[derive(Derivative)]
-        #[derivative(Default, Debug, PartialEq, Clone)]
+    TokenStream::from(quote! {
+        #[derive(Default, Debug, PartialEq, Clone)]
         #vis struct #struct_name #impl_generics {
             pub id: #kayak_core::Index,
-            #inputs_block
+            pub #props: #prop_type
         }
 
         impl #impl_generics #kayak_core::Widget for #struct_name #ty_generics #where_clause {
-            fn get_id(&self) -> #kayak_core::Index {
-                self.id
+
+            type Props = #prop_type;
+
+            fn constructor(props: Self::Props) -> Self where Self: Sized {
+                Self {
+                    id: #kayak_core::Index::default(),
+                    #props: props,
+                }
             }
 
-            fn focusable(&self) -> Option<bool> {
-                self.focusable
+            fn get_id(&self) -> #kayak_core::Index {
+                self.id
             }
 
             fn set_id(&mut self, id: #kayak_core::Index) {
                 self.id = id;
             }
 
-            fn get_styles(&self) -> Option<#kayak_core::styles::Style> {
-                self.styles.clone()
+            fn get_props(&self) -> &Self::Props {
+                &self.#props
             }
 
-            fn get_name(&self) -> String {
-                String::from(stringify!(#struct_name))
-            }
-
-            fn on_event(&mut self, context: &mut #kayak_core::context::KayakContext, event: &mut #kayak_core::Event) {
-                if let Some(on_event) = self.on_event.as_ref() {
-                    if let Ok(mut on_event) = on_event.0.write() {
-                        on_event(context, event);
-                    }
-                }
+            fn get_props_mut(&mut self) -> &mut Self::Props {
+                &mut self.#props
             }
 
             fn render(&mut self, context: &mut #kayak_core::KayakContextRef) {
+                use #kayak_core::WidgetProps;
+
                 let parent_id = Some(self.get_id());
-                #inputs_reading_ref
-                let children = children.clone();
+                let children = self.#props.get_children();
+                let mut #props = self.#props.clone();
+
                 #block
 
+                self.#props = #props;
                 context.commit();
             }
         }
diff --git a/kayak_render_macros/src/lib.rs b/kayak_render_macros/src/lib.rs
index 729d047..9a2634e 100644
--- a/kayak_render_macros/src/lib.rs
+++ b/kayak_render_macros/src/lib.rs
@@ -3,7 +3,7 @@ extern crate proc_macro;
 mod function_component;
 mod tags;
 
-mod arc_function;
+mod widget_builder;
 mod attribute;
 mod child;
 mod children;
@@ -23,7 +23,7 @@ use use_effect::UseEffect;
 use widget::ConstructedWidget;
 
 use crate::widget::Widget;
-use crate::widget_props::impl_widget_props;
+use crate::widget_props::{add_widget_props, impl_widget_props};
 
 #[proc_macro]
 #[proc_macro_error]
diff --git a/kayak_render_macros/src/widget.rs b/kayak_render_macros/src/widget.rs
index 7421899..a19cd42 100644
--- a/kayak_render_macros/src/widget.rs
+++ b/kayak_render_macros/src/widget.rs
@@ -1,12 +1,16 @@
 use proc_macro2::TokenStream;
-use quote::quote;
+use proc_macro_error::{emit_error, emit_warning};
+use quote::{format_ident, quote};
 use quote::ToTokens;
 use syn::parse::{Parse, ParseStream, Result};
+use syn::Path;
+use syn::spanned::Spanned;
 
-use crate::arc_function::build_arc_function;
+use crate::widget_builder::build_widget_stream;
 use crate::children::Children;
 use crate::tags::ClosingTag;
-use crate::{tags::OpenTag, widget_attributes::WidgetAttributes};
+use crate::{get_core_crate, tags::OpenTag, widget_attributes::WidgetAttributes};
+use crate::widget_attributes::CustomWidgetAttributes;
 
 #[derive(Clone)]
 pub struct Widget {
@@ -61,17 +65,19 @@ impl Widget {
         let name = open_tag.name;
         let declaration = if Self::is_custom_element(&name) {
             let attrs = &open_tag.attributes.for_custom_element(&children);
-            let attrs = attrs.to_token_stream();
+            let (props, constructor) = Self::construct(&name, attrs);
             if !as_prop {
-                let attrs = quote! { #name #attrs };
-                let widget_block = build_arc_function(quote! { built_widget }, attrs, 0);
-                quote! {
+                let widget_block = build_widget_stream(quote! { built_widget }, constructor, 0);
+                quote! {{
+                    #props
                     #widget_block
-                }
+                }}
             } else {
-                quote! {
-                    #name #attrs
-                }
+                quote! {{
+                    #props
+                    let widget = #constructor;
+                    widget
+                }}
             }
         } else {
             panic!("Couldn't find widget!");
@@ -83,6 +89,37 @@ impl Widget {
             declaration,
         })
     }
+
+
+    /// Constructs a widget and its props
+    ///
+    /// The returned tuple contains:
+    /// 1. The props constructor and assignment
+    /// 2. The widget constructor
+    ///
+    /// # Arguments
+    ///
+    /// * `name`: The full-path name of the widget
+    /// * `attrs`: The attributes (props) to apply to this widget
+    ///
+    /// returns: (TokenStream, TokenStream)
+    fn construct(name: &Path, attrs: &CustomWidgetAttributes) -> (TokenStream, TokenStream) {
+        let kayak_core = get_core_crate();
+
+        let prop_ident = format_ident!("props");
+        let attrs = attrs.assign_attributes(&prop_ident);
+
+        let props = quote! {
+            let mut #prop_ident = <#name as kayak_core::Widget>::Props::default();
+            #attrs
+        };
+
+        let constructor = quote! {
+            <#name as kayak_core::Widget>::constructor(#prop_ident)
+        };
+
+        (props, constructor)
+    }
 }
 
 impl ToTokens for Widget {
diff --git a/kayak_render_macros/src/widget_attributes.rs b/kayak_render_macros/src/widget_attributes.rs
index b69c454..e4f04b9 100644
--- a/kayak_render_macros/src/widget_attributes.rs
+++ b/kayak_render_macros/src/widget_attributes.rs
@@ -1,6 +1,7 @@
-use proc_macro_error::emit_error;
+use proc_macro_error::{emit_error, emit_warning};
 use quote::{quote, ToTokens};
 use std::collections::HashSet;
+use proc_macro2::{Ident, TokenStream};
 use syn::{
     ext::IdentExt,
     parse::{Parse, ParseStream, Result},
@@ -8,6 +9,8 @@ use syn::{
 };
 
 use crate::{attribute::Attribute, children::Children};
+use crate::attribute::AttributeKey;
+use crate::child::Child;
 
 #[derive(Clone)]
 pub struct WidgetAttributes {
@@ -68,6 +71,7 @@ pub struct CustomWidgetAttributes<'a, 'c> {
     children: &'c Children,
 }
 
+// TODO: This impl may not be needed anymore. If so, it should be removed
 impl<'a, 'c> ToTokens for CustomWidgetAttributes<'a, 'c> {
     fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
         let mut attrs: Vec<_> = self
@@ -90,22 +94,22 @@ impl<'a, 'c> ToTokens for CustomWidgetAttributes<'a, 'c> {
             });
         }
 
-        let missing = vec![
-            ("styles", quote! { styles: None }),
-            ("on_event", quote! { on_event: None }),
-        ];
-
-        for missed in missing {
-            if !self.attributes.iter().any(|attribute| {
-                attribute
-                    .ident()
-                    .to_token_stream()
-                    .to_string()
-                    .contains(missed.0)
-            }) {
-                attrs.push(missed.1);
-            }
-        }
+        // let missing = vec![
+        //     ("styles", quote! { styles: None }),
+        //     ("on_event", quote! { on_event: None }),
+        // ];
+        //
+        // for missed in missing {
+        //     if !self.attributes.iter().any(|attribute| {
+        //         attribute
+        //             .ident()
+        //             .to_token_stream()
+        //             .to_string()
+        //             .contains(missed.0)
+        //     }) {
+        //         attrs.push(missed.1);
+        //     }
+        // }
 
         let quoted = if attrs.len() == 0 {
             quote!({ ..Default::default() })
@@ -124,3 +128,66 @@ impl<'a, 'c> ToTokens for CustomWidgetAttributes<'a, 'c> {
         quoted.to_tokens(tokens);
     }
 }
+
+impl<'a, 'c> CustomWidgetAttributes<'a, 'c> {
+
+    /// Assign this widget's attributes to the given ident
+    ///
+    /// This takes the form: `IDENT.ATTR_NAME = ATTR_VALUE;`
+    ///
+    /// # Arguments
+    ///
+    /// * `ident`: The ident to assign to (i.e. "props")
+    ///
+    /// returns: TokenStream
+    pub fn assign_attributes(&self, ident: &Ident) -> TokenStream {
+        let mut attrs = self
+            .attributes
+            .iter()
+            .map(|attribute| {
+                let key = attribute.ident();
+                let value = attribute.value_tokens();
+
+                quote! {
+                    #ident.#key = #value;
+                }
+            })
+            .collect::<Vec<_>>();
+
+        // If this widget contains children, add it (should result in error if widget does not accept children)
+        if self.should_add_children() {
+            let children_tuple = self.children.as_option_of_tuples_tokens();
+            attrs.push(quote! {
+                let children = children.clone();
+                #ident.children = #children_tuple;
+            });
+        }
+
+        let result = quote! {
+            #( #attrs )*
+        };
+
+        result
+    }
+
+    /// Determines whether `children` should be added to this widget or not
+    fn should_add_children(&self) -> bool {
+        if self.children.nodes.len() == 0 {
+            // No children
+            false
+        } else if self.children.nodes.len() == 1 {
+            let child = self.children.nodes.first().unwrap();
+            match child {
+                Child::RawBlock(block) => {
+                    // Is child NOT an empty block? (`<Foo>{}</Foo>`)
+                    block.stmts.len() > 0
+                }
+                // Child is a widget
+                _ => true
+            }
+        } else {
+            // Multiple children
+            true
+        }
+    }
+}
\ No newline at end of file
diff --git a/kayak_render_macros/src/widget_builder.rs b/kayak_render_macros/src/widget_builder.rs
new file mode 100644
index 0000000..cc9928d
--- /dev/null
+++ b/kayak_render_macros/src/widget_builder.rs
@@ -0,0 +1,32 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+
+/// Creates a token stream for building a widget
+///
+/// # Arguments
+///
+/// * `widget_name`: The name of the widget to build
+/// * `widget_constructor`: The widget constructor token stream
+/// * `index`: The sibling index of this widget (starting from 0)
+///
+/// returns: TokenStream
+///
+/// # Examples
+///
+/// ```
+/// build_widget(quote! { my_widget }, quote!{ <MyWidget as Widget>::constructor(props) }, 0);
+/// // Outputs token stream:
+/// //   let my_widget = <MyWidget as Widget>::constructor(props);
+/// //   context.add_widget(my_widget, 0);
+/// ```
+pub fn build_widget_stream(
+    widget_name: TokenStream,
+    widget_constructor: TokenStream,
+    index: usize,
+) -> TokenStream {
+    quote! {
+        let #widget_name = #widget_constructor;
+        context.add_widget(#widget_name, #index);
+    }
+}
diff --git a/src/widgets/app.rs b/src/widgets/app.rs
index ac3b363..bfa99d9 100644
--- a/src/widgets/app.rs
+++ b/src/widgets/app.rs
@@ -1,15 +1,31 @@
 use crate::core::derivative::*;
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
     widget, Children,
 };
 
 use crate::widgets::Clip;
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct AppProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn App(children: Children) {
+pub fn App(props: AppProps) {
     #[cfg(feature = "bevy_renderer")]
     {
         use crate::bevy::WindowSize;
@@ -27,11 +43,11 @@ pub fn App(children: Children) {
 
         context.bind(&window_size);
         let window_size = window_size.get();
-        *styles = Some(Style {
+        props.styles = Some(Style {
             render_command: StyleProp::Value(RenderCommand::Layout),
             width: StyleProp::Value(Units::Pixels(window_size.0)),
             height: StyleProp::Value(Units::Pixels(window_size.1)),
-            ..styles.clone().unwrap_or_default()
+            ..props.styles.clone().unwrap_or_default()
         });
     }
 
diff --git a/src/widgets/background.rs b/src/widgets/background.rs
index abc0a7b..b43034e 100644
--- a/src/widgets/background.rs
+++ b/src/widgets/background.rs
@@ -1,16 +1,33 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
     widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct BackgroundProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn Background(children: Children, styles: Option<Style>) {
-    if styles.is_none() {
-        *styles = Some(Style::default())
+pub fn Background(props: BackgroundProps) {
+    if props.styles.is_none() {
+        props.styles = Some(Style::default())
     }
-    styles.as_mut().unwrap().render_command = StyleProp::Value(RenderCommand::Quad);
+    props.styles.as_mut().unwrap().render_command = StyleProp::Value(RenderCommand::Quad);
     rsx! {
         <Fragment>
             {children}
diff --git a/src/widgets/button.rs b/src/widgets/button.rs
index cfe2050..02f311b 100644
--- a/src/widgets/button.rs
+++ b/src/widgets/button.rs
@@ -1,15 +1,31 @@
 use crate::core::{
-    color::Color,
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    Color, OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp, Units},
     widget, Children, Fragment,
 };
 
-#[widget(focusable)]
-pub fn Button(children: Children, styles: Option<Style>) {
-    let base_styles = styles.clone().unwrap_or_default();
-    *styles = Some(Style {
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct ButtonProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "Some(true)"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
+#[widget]
+pub fn Button(props: ButtonProps) {
+    let base_styles = props.styles.clone().unwrap_or_default();
+    props.styles = Some(Style {
         render_command: StyleProp::Value(RenderCommand::Quad),
         border_radius: StyleProp::Value((5.0, 5.0, 5.0, 5.0)),
         height: if base_styles.height == StyleProp::Default {
diff --git a/src/widgets/clip.rs b/src/widgets/clip.rs
index 6868f89..a064606 100644
--- a/src/widgets/clip.rs
+++ b/src/widgets/clip.rs
@@ -1,14 +1,28 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp, Units},
-    widget, Children,
+    widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct ClipProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+}
+
 #[widget]
-pub fn Clip(children: Children, styles: Option<Style>) {
-    let incoming_styles = styles.clone().unwrap_or_default();
-    *styles = Some(Style {
+pub fn Clip(props: ClipProps) {
+    let incoming_styles = props.styles.clone().unwrap_or_default();
+    props.styles = Some(Style {
         render_command: StyleProp::Value(RenderCommand::Clip),
         width: if matches!(incoming_styles.width, StyleProp::Value(..)) {
             incoming_styles.width
diff --git a/src/widgets/element.rs b/src/widgets/element.rs
index e5fdadc..40ccd50 100644
--- a/src/widgets/element.rs
+++ b/src/widgets/element.rs
@@ -1,15 +1,32 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
-    widget, Children,
+    widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct ElementProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn Element(children: Children) {
-    *styles = Some(Style {
+pub fn Element(props: ElementProps) {
+    props.styles = Some(Style {
         render_command: StyleProp::Value(RenderCommand::Layout),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
     rsx! {
diff --git a/src/widgets/fold.rs b/src/widgets/fold.rs
index 2125f27..f4d99d5 100644
--- a/src/widgets/fold.rs
+++ b/src/widgets/fold.rs
@@ -1,12 +1,33 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp, Units},
-    use_state, widget, Children, EventType, Handler, OnEvent,
+    use_state, widget, Children, EventType, Handler,
 };
 
 use crate::widgets::{Background, Clip, If, Text};
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct FoldProps {
+    pub label: String,
+    pub open: Option<bool>,
+    pub on_change: Option<Handler<bool>>,
+    pub default_open: bool,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "Some(true)"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 /// A widget container that toggles its content between visible and hidden when clicked
 ///
 /// If `open` is set to `None`, then the toggle state will be automatically handled by
@@ -38,13 +59,9 @@ use crate::widgets::{Background, Clip, If, Text};
 /// }
 /// ```
 #[widget]
-pub fn Fold(
-    label: String,
-    children: Children,
-    open: Option<bool>,
-    on_change: Option<Handler<bool>>,
-    default_open: bool,
-) {
+pub fn Fold(props: FoldProps) {
+    let FoldProps {default_open, label, on_change, open, ..} = props.clone();
+
     // === State === //
     let initial = default_open || open.unwrap_or_default();
     let (is_open, set_is_open, ..) = use_state!(initial);
@@ -67,10 +84,10 @@ pub fn Fold(
     });
 
     // === Styles === //
-    *styles = Some(Style {
+    props.styles = Some(Style {
         height: StyleProp::Value(Units::Auto),
         render_command: StyleProp::Value(RenderCommand::Layout),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
     let background_styles = Style {
diff --git a/src/widgets/if_element.rs b/src/widgets/if_element.rs
index dfe49a9..6997ef9 100644
--- a/src/widgets/if_element.rs
+++ b/src/widgets/if_element.rs
@@ -1,13 +1,35 @@
-use crate::core::{rsx, widget, Children};
+use crate::core::{
+    render_command::RenderCommand,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
+    styles::{Style, StyleProp},
+    widget, Children, Fragment,
+};
+
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct IfProps {
+    pub condition: bool,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
 
 #[widget]
-pub fn If(children: Children, condition: bool) {
-    if condition {
+pub fn If(props: IfProps) {
+    if props.condition {
         rsx! {
             <>
                 {children}
             </>
         }
-    } else {
     }
 }
diff --git a/src/widgets/image.rs b/src/widgets/image.rs
index 6b97d32..854e10d 100644
--- a/src/widgets/image.rs
+++ b/src/widgets/image.rs
@@ -1,15 +1,33 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
-    widget, Children,
+    widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct ImageProps {
+    pub handle: u16,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn Image(handle: u16, children: Children) {
-    *styles = Some(Style {
-        render_command: StyleProp::Value(RenderCommand::Image { handle }),
-        ..styles.clone().unwrap_or_default()
+pub fn Image(props: ImageProps) {
+    props.styles = Some(Style {
+        render_command: StyleProp::Value(RenderCommand::Image { handle: props.handle }),
+        ..props.styles.clone().unwrap_or_default()
     });
 
     rsx! {
diff --git a/src/widgets/inspector.rs b/src/widgets/inspector.rs
index 8b461e5..bee6cd1 100644
--- a/src/widgets/inspector.rs
+++ b/src/widgets/inspector.rs
@@ -3,7 +3,7 @@ use kayak_core::{Bound, Color, EventType, OnEvent, VecTracker};
 use kayak_render_macros::{constructor, use_state};
 
 use crate::core::derivative::*;
-use crate::core::{rsx, widget, MutableBound};
+use crate::core::{rsx, widget, WidgetProps, MutableBound};
 
 use crate::widgets::{Background, Button, Text};
 
@@ -13,8 +13,14 @@ pub enum InspectData {
     Data(Vec<String>),
 }
 
+#[derive(WidgetProps, Default, Debug, PartialEq, Clone)]
+pub struct InspectorProps {
+    #[props(Styles)]
+    pub styles: Option<Style>,
+}
+
 #[widget]
-pub fn Inspector() {
+pub fn Inspector(props: InspectorProps) {
     let (inspect_data, set_inspect_data, _) = use_state!(Vec::<String>::new());
 
     let background_styles = Some(Style {
@@ -25,7 +31,7 @@ pub fn Inspector() {
         top: StyleProp::Value(Units::Stretch(0.0)),
         bottom: StyleProp::Value(Units::Stretch(0.0)),
         width: StyleProp::Value(Units::Pixels(200.0)),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
     let last_clicked = context.get_last_clicked_widget();
diff --git a/src/widgets/nine_patch.rs b/src/widgets/nine_patch.rs
index c462e16..31a7c1c 100644
--- a/src/widgets/nine_patch.rs
+++ b/src/widgets/nine_patch.rs
@@ -1,16 +1,35 @@
 use crate::core::{
-    layout_cache::Space,
     render_command::RenderCommand,
-    rsx,
+    layout_cache::Space,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
-    widget, Children,
+    widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct NinePatchProps {
+    pub handle: u16,
+    pub border: Space,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn NinePatch(handle: u16, border: Space, children: Children) {
-    *styles = Some(Style {
-        render_command: StyleProp::Value(RenderCommand::NinePatch { handle, border }),
-        ..styles.clone().unwrap_or_default()
+pub fn NinePatch(props: NinePatchProps) {
+    props.styles = Some(Style {
+        render_command: StyleProp::Value(RenderCommand::NinePatch { handle: props.handle, border: props.border }),
+        ..props.styles.clone().unwrap_or_default()
     });
 
     rsx! {
diff --git a/src/widgets/text.rs b/src/widgets/text.rs
index a7d9fa3..1a18142 100644
--- a/src/widgets/text.rs
+++ b/src/widgets/text.rs
@@ -3,18 +3,32 @@ use kayak_font::{CoordinateSystem, KayakFont};
 
 use crate::core::{
     render_command::RenderCommand,
+    derivative::Derivative,
+    OnEvent, rsx, WidgetProps,
     styles::{Style, StyleProp},
-    widget,
+    widget, Children, Fragment,
 };
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct TextProps {
+    pub content: String,
+    pub font: Option<String>,
+    pub line_height: Option<f32>,
+    pub size: f32,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn Text(
-    size: f32,
-    line_height: Option<f32>,
-    content: String,
-    styles: Option<Style>,
-    font: Option<String>,
-) {
+pub fn Text(props: TextProps) {
+    let TextProps{content, font, line_height, size, ..} = props.clone();
     let font_name = font;
     let font: Binding<Option<KayakFont>> =
         context.get_asset(font_name.clone().unwrap_or("Roboto".into()));
@@ -53,10 +67,10 @@ pub fn Text(
         font: font_name.clone().unwrap_or("Roboto".into()),
     };
 
-    *styles = Some(Style {
+    props.styles = Some(Style {
         render_command: StyleProp::Value(render_command),
         width: StyleProp::Value(Units::Pixels(layout_size.0)),
         height: StyleProp::Value(Units::Pixels(layout_size.1)),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 }
diff --git a/src/widgets/text_box.rs b/src/widgets/text_box.rs
index a6f2cfb..a5a2d8d 100644
--- a/src/widgets/text_box.rs
+++ b/src/widgets/text_box.rs
@@ -1,6 +1,7 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    Children, rsx, WidgetProps,
     styles::{Style, StyleProp, Units},
     widget, Bound, Color, EventType, MutableBound, OnEvent,
 };
@@ -8,6 +9,25 @@ use std::sync::{Arc, RwLock};
 
 use crate::widgets::{Background, Clip, Text};
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct TextBoxProps {
+    pub value: String,
+    pub on_change: Option<OnChange>,
+    pub placeholder: Option<String>,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "Some(true)"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[derive(Debug, Clone, PartialEq)]
 pub struct ChangeEvent {
     pub value: String,
@@ -38,9 +58,10 @@ impl std::fmt::Debug for OnChange {
 pub struct Focus(pub bool);
 
 #[widget(focusable)]
-pub fn TextBox(value: String, on_change: Option<OnChange>, placeholder: Option<String>) {
-    let current_styles = styles.clone().unwrap_or_default();
-    *styles = Some(Style {
+pub fn TextBox(props: TextBoxProps) {
+    let TextBoxProps {on_change, placeholder, value, ..} = props.clone();
+    let current_styles = props.styles.clone().unwrap_or_default();
+    props.styles = Some(Style {
         render_command: StyleProp::Value(RenderCommand::Layout),
         height: StyleProp::Value(Units::Pixels(26.0)),
         top: if matches!(current_styles.top, StyleProp::Value { .. }) {
@@ -62,7 +83,7 @@ pub fn TextBox(value: String, on_change: Option<OnChange>, placeholder: Option<S
         height: StyleProp::Value(Units::Pixels(26.0)),
         padding_left: StyleProp::Value(Units::Pixels(5.0)),
         padding_right: StyleProp::Value(Units::Pixels(5.0)),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     };
 
     let has_focus = context.create_state(Focus(false)).unwrap();
@@ -71,7 +92,7 @@ pub fn TextBox(value: String, on_change: Option<OnChange>, placeholder: Option<S
     let cloned_on_change = on_change.clone();
     let cloned_has_focus = has_focus.clone();
 
-    self.on_event = Some(OnEvent::new(move |_, event| match event.event_type {
+    props.on_event = Some(OnEvent::new(move |_, event| match event.event_type {
         EventType::CharInput { c } => {
             if !cloned_has_focus.get().0 {
                 return;
@@ -103,7 +124,7 @@ pub fn TextBox(value: String, on_change: Option<OnChange>, placeholder: Option<S
         }
     } else {
         Style {
-            color: styles.clone().unwrap_or_default().color,
+            color: props.styles.clone().unwrap_or_default().color,
             ..Style::default()
         }
     };
diff --git a/src/widgets/tooltip.rs b/src/widgets/tooltip.rs
index 9c11089..0fea0ac 100644
--- a/src/widgets/tooltip.rs
+++ b/src/widgets/tooltip.rs
@@ -1,6 +1,7 @@
 use crate::core::{
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    rsx, WidgetProps,
     styles::{PositionType, Style, StyleProp, Units},
     widget, Bound, Children, Color, EventType, MutableBound, OnEvent,
 };
@@ -20,6 +21,37 @@ pub struct TooltipData {
     pub visible: bool,
 }
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct TooltipProviderProps {
+    pub position: (f32, f32),
+    pub size: (f32, f32),
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+}
+
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct TooltipConsumerProps {
+    pub anchor: Option<(f32, f32)>,
+    pub size: Option<(f32, f32)>,
+    pub text: String,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+}
+
 /// A provider for managing a tooltip context.
 ///
 /// This widget creates a single tooltip that can be controlled by any descendant [TooltipConsumer].
@@ -58,7 +90,8 @@ pub struct TooltipData {
 /// }
 /// ```
 #[widget]
-pub fn TooltipProvider(children: Children, position: (f32, f32), size: (f32, f32)) {
+pub fn TooltipProvider(props: TooltipProviderProps) {
+    let TooltipProviderProps{position, size, ..} = props;
     const WIDTH: f32 = 150.0;
     const HEIGHT: f32 = 18.0;
     const PADDING: (f32, f32) = (10.0, 5.0);
@@ -73,15 +106,15 @@ pub fn TooltipProvider(children: Children, position: (f32, f32), size: (f32, f32
     } = tooltip.get();
     let tooltip_size = tooltip_size.unwrap_or((WIDTH, HEIGHT));
 
-    *styles = Some(Style {
+    props.styles = Some(Style {
         left: StyleProp::Value(Units::Pixels(position.0)),
         top: StyleProp::Value(Units::Pixels(position.1)),
         width: StyleProp::Value(Units::Pixels(size.0)),
         height: StyleProp::Value(Units::Pixels(size.1)),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
-    let base_styles = styles.clone().unwrap();
+    let base_styles = props.styles.clone().unwrap();
     let mut tooltip_styles = Style {
         position_type: StyleProp::Value(PositionType::SelfDirected),
         background_color: if matches!(base_styles.background_color, StyleProp::Default) {
@@ -166,17 +199,13 @@ pub fn TooltipProvider(children: Children, position: (f32, f32), size: (f32, f32
 /// }
 /// ```
 #[widget]
-pub fn TooltipConsumer(
-    children: Children,
-    text: String,
-    anchor: Option<(f32, f32)>,
-    size: Option<(f32, f32)>,
-) {
-    *styles = Some(Style {
+pub fn TooltipConsumer(props: TooltipConsumerProps) {
+    let TooltipConsumerProps {anchor, size, text, ..} = props.clone();
+    props.styles = Some(Style {
         render_command: StyleProp::Value(RenderCommand::Clip),
         width: StyleProp::Value(Units::Auto),
         height: StyleProp::Value(Units::Auto),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
     let data = context
@@ -184,7 +213,7 @@ pub fn TooltipConsumer(
         .expect("TooltipConsumer requires TooltipProvider as an ancestor");
 
     let text = Arc::new(text);
-    self.on_event = Some(OnEvent::new(move |ctx, event| match event.event_type {
+    props.on_event = Some(OnEvent::new(move |ctx, event| match event.event_type {
         EventType::MouseIn(..) => {
             let mut state = data.get();
             state.visible = true;
diff --git a/src/widgets/window.rs b/src/widgets/window.rs
index 9e1088c..84ca262 100644
--- a/src/widgets/window.rs
+++ b/src/widgets/window.rs
@@ -1,22 +1,38 @@
 use crate::core::{
     color::Color,
     render_command::RenderCommand,
-    rsx,
+    derivative::Derivative,
+    rsx, WidgetProps,
     styles::{PositionType, Style, StyleProp, Units},
     use_state, widget, Children, EventType, OnEvent,
 };
 
 use crate::widgets::{Background, Clip, Element, Text};
 
+#[derive(WidgetProps, Derivative)]
+#[derivative(Default, Debug, PartialEq, Clone)]
+pub struct WindowProps {
+    pub draggable: bool,
+    pub position: (f32, f32),
+    pub size: (f32, f32),
+    pub title: String,
+    #[props(Styles)]
+    pub styles: Option<Style>,
+    #[props(Children)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub children: Children,
+    #[props(OnEvent)]
+    #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
+    pub on_event: Option<OnEvent>,
+    #[props(Focusable)]
+    #[derivative(Default(value = "None"), PartialEq = "ignore")]
+    pub focusable: Option<bool>,
+}
+
 #[widget]
-pub fn Window(
-    children: Children,
-    styles: Option<Style>,
-    position: (f32, f32),
-    size: (f32, f32),
-    title: String,
-    draggable: bool,
-) {
+pub fn Window(props: WindowProps) {
+    let WindowProps{draggable, position, size, title, ..} = props.clone();
+
     let (is_dragging, set_is_dragging, ..) = use_state!(false);
     let (offset, set_offset, ..) = use_state!((0.0, 0.0));
     let (pos, set_pos, ..) = use_state!(position);
@@ -43,7 +59,7 @@ pub fn Window(
         None
     };
 
-    *styles = Some(Style {
+    props.styles = Some(Style {
         background_color: StyleProp::Value(Color::new(0.125, 0.125, 0.125, 1.0)),
         border: StyleProp::Value((4.0, 4.0, 4.0, 4.0)),
         border_radius: StyleProp::Value((5.0, 5.0, 5.0, 5.0)),
@@ -55,7 +71,7 @@ pub fn Window(
         height: StyleProp::Value(Units::Pixels(size.1)),
         max_width: StyleProp::Value(Units::Pixels(size.0)),
         max_height: StyleProp::Value(Units::Pixels(size.1)),
-        ..styles.clone().unwrap_or_default()
+        ..props.styles.clone().unwrap_or_default()
     });
 
     let clip_styles = Style {
-- 
GitLab