Преглед на файлове

Merge pull request #940 from Demonthos/make-widgets-cross-framework

Jonathan Kelley преди 1 година
родител
ревизия
f4b3be1b13
променени са 44 файла, в които са добавени 3232 реда и са изтрити 1185 реда
  1. 15 14
      packages/dioxus-tui/examples/widgets.rs
  2. 4 6
      packages/dioxus-tui/src/lib.rs
  3. 0 2
      packages/dioxus-tui/src/prelude/mod.rs
  4. 0 59
      packages/dioxus-tui/src/widgets/button.rs
  5. 0 82
      packages/dioxus-tui/src/widgets/checkbox.rs
  6. 0 102
      packages/dioxus-tui/src/widgets/input.rs
  7. 0 22
      packages/dioxus-tui/src/widgets/mod.rs
  8. 0 209
      packages/dioxus-tui/src/widgets/number.rs
  9. 0 193
      packages/dioxus-tui/src/widgets/password.rs
  10. 0 107
      packages/dioxus-tui/src/widgets/slider.rs
  11. 0 182
      packages/dioxus-tui/src/widgets/textbox.rs
  12. 4 4
      packages/native-core-macro/src/lib.rs
  13. 1 1
      packages/native-core/examples/custom_attr.rs
  14. 1 1
      packages/native-core/examples/font_size.rs
  15. 1 1
      packages/native-core/examples/simple.rs
  16. 1 1
      packages/native-core/examples/simple_dioxus.rs
  17. 180 0
      packages/native-core/src/custom_element.rs
  18. 2 0
      packages/native-core/src/lib.rs
  19. 28 3
      packages/native-core/src/node_ref.rs
  20. 10 8
      packages/native-core/src/node_watcher.rs
  21. 28 9
      packages/native-core/src/passes.rs
  22. 208 26
      packages/native-core/src/real_dom.rs
  23. 305 29
      packages/native-core/src/tree.rs
  24. 2 2
      packages/native-core/src/utils/persistant_iterator.rs
  25. 1 1
      packages/native-core/tests/called_minimally_on_build.rs
  26. 389 0
      packages/native-core/tests/custom_element.rs
  27. 1 2
      packages/rink/examples/counter_button.rs
  28. 1 2
      packages/rink/examples/grid.rs
  29. 101 0
      packages/rink/examples/widgets.rs
  30. 82 73
      packages/rink/src/hooks.rs
  31. 3 6
      packages/rink/src/layout.rs
  32. 98 19
      packages/rink/src/lib.rs
  33. 0 2
      packages/rink/src/prelude/mod.rs
  34. 19 15
      packages/rink/src/query.rs
  35. 5 2
      packages/rink/src/render.rs
  36. 206 0
      packages/rink/src/widgets/button.rs
  37. 249 0
      packages/rink/src/widgets/checkbox.rs
  38. 153 0
      packages/rink/src/widgets/input.rs
  39. 128 0
      packages/rink/src/widgets/mod.rs
  40. 91 0
      packages/rink/src/widgets/number.rs
  41. 12 0
      packages/rink/src/widgets/password.rs
  42. 456 0
      packages/rink/src/widgets/slider.rs
  43. 444 0
      packages/rink/src/widgets/text_like.rs
  44. 3 0
      packages/rink/src/widgets/textbox.rs

+ 15 - 14
packages/dioxus-tui/examples/widgets.rs

@@ -1,6 +1,5 @@
 use dioxus::prelude::*;
-use dioxus_html::FormData;
-use dioxus_tui::prelude::*;
+use dioxus_tui::Config;
 
 fn main() {
     dioxus_tui::launch_cfg(app, Config::new());
@@ -18,8 +17,8 @@ fn app(cx: Scope) -> Element {
             align_items: "center",
             justify_content: "center",
 
-            Input{
-                oninput: |data: FormData| if &data.value == "good"{
+            input {
+                oninput: |data| if &data.value == "good"{
                     bg_green.set(true);
                 } else{
                     bg_green.set(false);
@@ -30,8 +29,8 @@ fn app(cx: Scope) -> Element {
                 height: "10%",
                 checked: "true",
             }
-            Input{
-                oninput: |data: FormData| if &data.value == "hello world"{
+            input {
+                oninput: |data| if &data.value == "hello world"{
                     bg_green.set(true);
                 } else{
                     bg_green.set(false);
@@ -40,8 +39,8 @@ fn app(cx: Scope) -> Element {
                 height: "10%",
                 maxlength: "11",
             }
-            Input{
-                oninput: |data: FormData| {
+            input {
+                oninput: |data| {
                     if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
                         bg_green.set(true);
                     } else{
@@ -54,8 +53,8 @@ fn app(cx: Scope) -> Element {
                 min: "20",
                 max: "80",
             }
-            Input{
-                oninput: |data: FormData| {
+            input {
+                oninput: |data| {
                     if data.value == "10"{
                         bg_green.set(true);
                     } else{
@@ -67,8 +66,8 @@ fn app(cx: Scope) -> Element {
                 height: "10%",
                 maxlength: "4",
             }
-            Input{
-                oninput: |data: FormData| {
+            input {
+                oninput: |data| {
                     if data.value == "hello world"{
                         bg_green.set(true);
                     } else{
@@ -80,8 +79,10 @@ fn app(cx: Scope) -> Element {
                 height: "10%",
                 maxlength: "11",
             }
-            Input{
-                onclick: |_: FormData| bg_green.set(true),
+            input {
+                oninput: |_| {
+                    bg_green.set(true)
+                },
                 r#type: "button",
                 value: "green",
                 width: "50%",

+ 4 - 6
packages/dioxus-tui/src/lib.rs

@@ -1,6 +1,4 @@
 mod element;
-pub mod prelude;
-pub mod widgets;
 
 use std::{
     any::Any,
@@ -10,7 +8,6 @@ use std::{
 };
 
 use dioxus_core::{Component, ElementId, VirtualDom};
-use dioxus_html::EventData;
 use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
 use dioxus_native_core::prelude::*;
 
@@ -48,7 +45,7 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
             let mut dioxus_state = dioxus_state.write().unwrap();
 
             // Find any mount events
-            let mounted = dbg!(find_mount_events(&muts));
+            let mounted = find_mount_events(&muts);
 
             dioxus_state.apply_mutations(&mut rdom, muts);
 
@@ -119,13 +116,14 @@ impl Driver for DioxusRenderer {
         rdom: &Arc<RwLock<RealDom>>,
         id: NodeId,
         event: &str,
-        value: Rc<EventData>,
+        value: Rc<rink::EventData>,
         bubbles: bool,
     ) {
         let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
         if let Some(id) = id {
+            let inner_value = value.deref().clone();
             self.vdom
-                .handle_event(event, value.deref().clone().into_any(), id, bubbles);
+                .handle_event(event, inner_value.into_any(), id, bubbles);
         }
     }
 

+ 0 - 2
packages/dioxus-tui/src/prelude/mod.rs

@@ -1,2 +0,0 @@
-pub use crate::widgets::*;
-pub use rink::Config;

+ 0 - 59
packages/dioxus-tui/src/widgets/button.rs

@@ -1,59 +0,0 @@
-use std::collections::HashMap;
-
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-
-#[derive(Props)]
-pub(crate) struct ButtonProps<'a> {
-    #[props(!optional)]
-    raw_onclick: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-}
-
-#[allow(non_snake_case)]
-pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
-    let state = use_state(cx, || false);
-    let width = cx.props.width.unwrap_or("1px");
-    let height = cx.props.height.unwrap_or("1px");
-
-    let single_char = width == "1px" || height == "1px";
-    let text = if let Some(v) = cx.props.value { v } else { "" };
-    let border_style = if single_char { "none" } else { "solid" };
-    let update = || {
-        let new_state = !state.get();
-        if let Some(callback) = cx.props.raw_onclick {
-            callback.call(FormData {
-                value: text.to_string(),
-                values: HashMap::new(),
-                files: None,
-            });
-        }
-        state.set(new_state);
-    };
-    render! {
-        div{
-            width: "{width}",
-            height: "{height}",
-            border_style: "{border_style}",
-            flex_direction: "row",
-            align_items: "center",
-            justify_content: "center",
-            onclick: move |_| {
-                update();
-            },
-            onkeydown: move |evt|{
-                if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false }  {
-                    update();
-                }
-            },
-            "{text}"
-        }
-    }
-}

+ 0 - 82
packages/dioxus-tui/src/widgets/checkbox.rs

@@ -1,82 +0,0 @@
-use std::collections::HashMap;
-
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-
-#[derive(Props)]
-pub(crate) struct CheckBoxProps<'a> {
-    #[props(!optional)]
-    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-    #[props(!optional)]
-    checked: Option<&'a str>,
-}
-
-#[allow(non_snake_case)]
-pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
-    let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
-    let width = cx.props.width.unwrap_or("1px");
-    let height = cx.props.height.unwrap_or("1px");
-
-    let single_char = width == "1px" && height == "1px";
-    let text = if single_char {
-        if *state.get() {
-            "☑"
-        } else {
-            "☐"
-        }
-    } else if *state.get() {
-        "✓"
-    } else {
-        " "
-    };
-    let border_style = if width == "1px" || height == "1px" {
-        "none"
-    } else {
-        "solid"
-    };
-    let update = move || {
-        let new_state = !state.get();
-        if let Some(callback) = cx.props.raw_oninput {
-            callback.call(FormData {
-                value: if let Some(value) = &cx.props.value {
-                    if new_state {
-                        value.to_string()
-                    } else {
-                        String::new()
-                    }
-                } else {
-                    "on".to_string()
-                },
-                values: HashMap::new(),
-                files: None,
-            });
-        }
-        state.set(new_state);
-    };
-    render! {
-        div {
-            width: "{width}",
-            height: "{height}",
-            border_style: "{border_style}",
-            align_items: "center",
-            justify_content: "center",
-            onclick: move |_| {
-                update();
-            },
-            onkeydown: move |evt| {
-                if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false }  {
-                    update();
-                }
-            },
-            "{text}"
-        }
-    }
-}

+ 0 - 102
packages/dioxus-tui/src/widgets/input.rs

@@ -1,102 +0,0 @@
-use dioxus::prelude::*;
-use dioxus_core::prelude::fc_to_builder;
-use dioxus_html::FormData;
-
-use crate::widgets::button::Button;
-use crate::widgets::checkbox::CheckBox;
-use crate::widgets::number::NumbericInput;
-use crate::widgets::password::Password;
-use crate::widgets::slider::Slider;
-use crate::widgets::textbox::TextBox;
-
-#[derive(Props)]
-pub struct InputProps<'a> {
-    r#type: Option<&'static str>,
-    oninput: Option<EventHandler<'a, FormData>>,
-    onclick: Option<EventHandler<'a, FormData>>,
-    value: Option<&'a str>,
-    size: Option<&'a str>,
-    maxlength: Option<&'a str>,
-    width: Option<&'a str>,
-    height: Option<&'a str>,
-    min: Option<&'a str>,
-    max: Option<&'a str>,
-    step: Option<&'a str>,
-    checked: Option<&'a str>,
-}
-
-#[allow(non_snake_case)]
-pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> {
-    cx.render(match cx.props.r#type {
-        Some("checkbox") => {
-            rsx! {
-                CheckBox{
-                    raw_oninput: cx.props.oninput.as_ref(),
-                    value: cx.props.value,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                    checked: cx.props.checked,
-                }
-            }
-        }
-        Some("range") => {
-            rsx! {
-                Slider{
-                    raw_oninput: cx.props.oninput.as_ref(),
-                    value: cx.props.value,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                    max: cx.props.max,
-                    min: cx.props.min,
-                    step: cx.props.step,
-                }
-            }
-        }
-        Some("button") => {
-            rsx! {
-                Button{
-                    raw_onclick: cx.props.onclick.as_ref(),
-                    value: cx.props.value,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                }
-            }
-        }
-        Some("number") => {
-            rsx! {
-                NumbericInput{
-                    raw_oninput: cx.props.oninput.as_ref(),
-                    value: cx.props.value,
-                    size: cx.props.size,
-                    max_length: cx.props.maxlength,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                }
-            }
-        }
-        Some("password") => {
-            rsx! {
-                Password{
-                    raw_oninput: cx.props.oninput.as_ref(),
-                    value: cx.props.value,
-                    size: cx.props.size,
-                    max_length: cx.props.maxlength,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                }
-            }
-        }
-        _ => {
-            rsx! {
-                TextBox{
-                    raw_oninput: cx.props.oninput.as_ref(),
-                    value: cx.props.value,
-                    size: cx.props.size,
-                    max_length: cx.props.maxlength,
-                    width: cx.props.width,
-                    height: cx.props.height,
-                }
-            }
-        }
-    })
-}

+ 0 - 22
packages/dioxus-tui/src/widgets/mod.rs

@@ -1,22 +0,0 @@
-mod button;
-mod checkbox;
-mod input;
-mod number;
-mod password;
-mod slider;
-mod textbox;
-
-use dioxus_core::{RenderReturn, Scope};
-use dioxus_native_core::NodeId;
-pub use input::*;
-
-use crate::DioxusElementToNodeId;
-
-pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<NodeId> {
-    if let RenderReturn::Ready(sync) = cx.root_node() {
-        let mapping: DioxusElementToNodeId = cx.consume_context()?;
-        mapping.get_node_id(sync.root_ids.get(0)?)
-    } else {
-        None
-    }
-}

+ 0 - 209
packages/dioxus-tui/src/widgets/number.rs

@@ -1,209 +0,0 @@
-use crate::widgets::get_root_id;
-use crossterm::{cursor::MoveTo, execute};
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-use dioxus_native_core::utils::cursor::{Cursor, Pos};
-use rink::Query;
-use std::{collections::HashMap, io::stdout};
-use taffy::geometry::Point;
-
-#[derive(Props)]
-pub(crate) struct NumbericInputProps<'a> {
-    #[props(!optional)]
-    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    size: Option<&'a str>,
-    #[props(!optional)]
-    max_length: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-}
-#[allow(non_snake_case)]
-pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
-    let tui_query: Query = cx.consume_context().unwrap();
-    let tui_query_clone = tui_query.clone();
-
-    let text_ref = use_ref(cx, || {
-        if let Some(intial_text) = cx.props.value {
-            intial_text.to_string()
-        } else {
-            String::new()
-        }
-    });
-    let cursor = use_ref(cx, Cursor::default);
-    let dragging = use_state(cx, || false);
-
-    let text = text_ref.read().clone();
-    let start_highlight = cursor.read().first().idx(&*text);
-    let end_highlight = cursor.read().last().idx(&*text);
-    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
-    let (text_highlighted, text_after_second_cursor) =
-        text_after_first_cursor.split_at(end_highlight - start_highlight);
-
-    let max_len = cx
-        .props
-        .max_length
-        .as_ref()
-        .and_then(|s| s.parse().ok())
-        .unwrap_or(usize::MAX);
-
-    let width = cx
-        .props
-        .width
-        .map(|s| s.to_string())
-        // px is the same as em in tui
-        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
-        .unwrap_or_else(|| "10px".to_string());
-    let height = cx.props.height.unwrap_or("3px");
-
-    // don't draw a border unless there is enough space
-    let border = if width
-        .strip_suffix("px")
-        .and_then(|w| w.parse::<i32>().ok())
-        .filter(|w| *w < 3)
-        .is_some()
-        || height
-            .strip_suffix("px")
-            .and_then(|h| h.parse::<i32>().ok())
-            .filter(|h| *h < 3)
-            .is_some()
-    {
-        "none"
-    } else {
-        "solid"
-    };
-
-    let update = |text: String| {
-        if let Some(input_handler) = &cx.props.raw_oninput {
-            input_handler.call(FormData {
-                value: text,
-                values: HashMap::new(),
-                files: None,
-            });
-        }
-    };
-    let increase = move || {
-        let mut text = text_ref.write();
-        *text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
-        update(text.clone());
-    };
-    let decrease = move || {
-        let mut text = text_ref.write();
-        *text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
-        update(text.clone());
-    };
-
-    render! {
-        div{
-            width: "{width}",
-            height: "{height}",
-            border_style: "{border}",
-
-            onkeydown: move |k| {
-                let is_text = match k.key(){
-                    Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
-                    Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true,
-                    _  => false,
-                };
-                if is_text{
-                    let mut text = text_ref.write();
-                    cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
-                    update(text.clone());
-
-                    let node = tui_query.get(get_root_id(cx).unwrap());
-                    let Point{ x, y } = node.pos().unwrap();
-
-                    let Pos { col, row } = cursor.read().start;
-                    let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
-                    if let Ok(pos) = crossterm::cursor::position() {
-                        if pos != (x, y){
-                            execute!(stdout(), MoveTo(x, y)).unwrap();
-                        }
-                    }
-                    else{
-                        execute!(stdout(), MoveTo(x, y)).unwrap();
-                    }
-                }
-                else{
-                    match k.key() {
-                        Key::ArrowUp =>{
-                            increase();
-                        }
-                        Key::ArrowDown =>{
-                            decrease();
-                        }
-                        _ => ()
-                    }
-                }
-            },
-            onmousemove: move |evt| {
-                if *dragging.get() {
-                    let offset = evt.data.element_coordinates();
-                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
-                    if border != "none" {
-                        new.col = new.col.saturating_sub(1);
-                    }
-                    // textboxs are only one line tall
-                    new.row = 0;
-
-                    if new != cursor.read().start {
-                        cursor.write().end = Some(new);
-                    }
-                }
-            },
-            onmousedown: move |evt| {
-                let offset = evt.data.element_coordinates();
-                let mut new = Pos::new(offset.x as usize,  offset.y as usize);
-                if border != "none" {
-                    new.col = new.col.saturating_sub(1);
-                }
-                new.row = 0;
-
-                new.realize_col(text_ref.read().as_str());
-                cursor.set(Cursor::from_start(new));
-                dragging.set(true);
-                let node = tui_query_clone.get(get_root_id(cx).unwrap());
-                let Point{ x, y } = node.pos().unwrap();
-
-                let Pos { col, row } = cursor.read().start;
-                let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
-                if let Ok(pos) = crossterm::cursor::position() {
-                    if pos != (x, y){
-                        execute!(stdout(), MoveTo(x, y)).unwrap();
-                    }
-                }
-                else{
-                    execute!(stdout(), MoveTo(x, y)).unwrap();
-                }
-            },
-            onmouseup: move |_| {
-                dragging.set(false);
-            },
-            onmouseleave: move |_| {
-                dragging.set(false);
-            },
-            onmouseenter: move |_| {
-                dragging.set(false);
-            },
-            onfocusout: |_| {
-                execute!(stdout(), MoveTo(0, 1000)).unwrap();
-            },
-
-            "{text_before_first_cursor}"
-
-            span{
-                background_color: "rgba(255, 255, 255, 50%)",
-
-                "{text_highlighted}"
-            }
-
-            "{text_after_second_cursor}"
-        }
-    }
-}

+ 0 - 193
packages/dioxus-tui/src/widgets/password.rs

@@ -1,193 +0,0 @@
-use crate::widgets::get_root_id;
-use crossterm::{cursor::*, execute};
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-use dioxus_native_core::utils::cursor::{Cursor, Pos};
-use rink::Query;
-use std::{collections::HashMap, io::stdout};
-use taffy::geometry::Point;
-
-#[derive(Props)]
-pub(crate) struct PasswordProps<'a> {
-    #[props(!optional)]
-    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    size: Option<&'a str>,
-    #[props(!optional)]
-    max_length: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-}
-#[allow(non_snake_case)]
-pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
-    let tui_query: Query = cx.consume_context().unwrap();
-    let tui_query_clone = tui_query.clone();
-
-    let text_ref = use_ref(cx, || {
-        if let Some(intial_text) = cx.props.value {
-            intial_text.to_string()
-        } else {
-            String::new()
-        }
-    });
-    let cursor = use_ref(cx, Cursor::default);
-    let dragging = use_state(cx, || false);
-
-    let text = text_ref.read().clone();
-    let start_highlight = cursor.read().first().idx(&*text);
-    let end_highlight = cursor.read().last().idx(&*text);
-    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
-    let (text_highlighted, text_after_second_cursor) =
-        text_after_first_cursor.split_at(end_highlight - start_highlight);
-
-    let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
-    let text_highlighted = ".".repeat(text_highlighted.len());
-    let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
-
-    let max_len = cx
-        .props
-        .max_length
-        .as_ref()
-        .and_then(|s| s.parse().ok())
-        .unwrap_or(usize::MAX);
-
-    let width = cx
-        .props
-        .width
-        .map(|s| s.to_string())
-        // px is the same as em in tui
-        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
-        .unwrap_or_else(|| "10px".to_string());
-    let height = cx.props.height.unwrap_or("3px");
-
-    // don't draw a border unless there is enough space
-    let border = if width
-        .strip_suffix("px")
-        .and_then(|w| w.parse::<i32>().ok())
-        .filter(|w| *w < 3)
-        .is_some()
-        || height
-            .strip_suffix("px")
-            .and_then(|h| h.parse::<i32>().ok())
-            .filter(|h| *h < 3)
-            .is_some()
-    {
-        "none"
-    } else {
-        "solid"
-    };
-
-    let onkeydown = move |k: KeyboardEvent| {
-        if k.key() == Key::Enter {
-            return;
-        }
-        let mut text = text_ref.write();
-        cursor
-            .write()
-            .handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
-        if let Some(input_handler) = &cx.props.raw_oninput {
-            input_handler.call(FormData {
-                value: text.clone(),
-                values: HashMap::new(),
-                files: None,
-            });
-        }
-
-        let node = tui_query.get(get_root_id(cx).unwrap());
-        let Point { x, y } = node.pos().unwrap();
-
-        let Pos { col, row } = cursor.read().start;
-        let (x, y) = (
-            col as u16 + x as u16 + u16::from(border != "none"),
-            row as u16 + y as u16 + u16::from(border != "none"),
-        );
-        if let Ok(pos) = crossterm::cursor::position() {
-            if pos != (x, y) {
-                execute!(stdout(), MoveTo(x, y)).unwrap();
-            }
-        } else {
-            execute!(stdout(), MoveTo(x, y)).unwrap();
-        }
-    };
-
-    render! {
-        div {
-            width: "{width}",
-            height: "{height}",
-            border_style: "{border}",
-
-            onkeydown: onkeydown,
-
-            onmousemove: move |evt| {
-                if *dragging.get() {
-                    let offset = evt.data.element_coordinates();
-                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
-                    if border != "none" {
-                        new.col = new.col.saturating_sub(1);
-                    }
-                    // textboxs are only one line tall
-                    new.row = 0;
-
-                    if new != cursor.read().start {
-                        cursor.write().end = Some(new);
-                    }
-                }
-            },
-
-            onmousedown: move |evt| {
-                let offset = evt.data.element_coordinates();
-                let mut new = Pos::new(offset.x as usize, offset.y as usize);
-                if border != "none" {
-                    new.col = new.col.saturating_sub(1);
-                }
-                // textboxs are only one line tall
-                new.row = 0;
-
-                new.realize_col(text_ref.read().as_str());
-                cursor.set(Cursor::from_start(new));
-                dragging.set(true);
-                let node = tui_query_clone.get(get_root_id(cx).unwrap());
-                let Point{ x, y } = node.pos().unwrap();
-
-                let Pos { col, row } = cursor.read().start;
-                let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
-                if let Ok(pos) = crossterm::cursor::position() {
-                    if pos != (x, y){
-                        execute!(stdout(), MoveTo(x, y)).unwrap();
-                    }
-                }
-                else{
-                    execute!(stdout(), MoveTo(x, y)).unwrap();
-                }
-            },
-            onmouseup: move |_| {
-                dragging.set(false);
-            },
-            onmouseleave: move |_| {
-                dragging.set(false);
-            },
-            onmouseenter: move |_| {
-                dragging.set(false);
-            },
-            onfocusout: |_| {
-                execute!(stdout(), MoveTo(0, 1000)).unwrap();
-            },
-
-            "{text_before_first_cursor}"
-
-            span{
-                background_color: "rgba(255, 255, 255, 50%)",
-
-                "{text_highlighted}"
-            }
-
-            "{text_after_second_cursor}"
-        }
-    }
-}

+ 0 - 107
packages/dioxus-tui/src/widgets/slider.rs

@@ -1,107 +0,0 @@
-use std::collections::HashMap;
-
-use crate::widgets::get_root_id;
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-use rink::Query;
-
-#[derive(Props)]
-pub(crate) struct SliderProps<'a> {
-    #[props(!optional)]
-    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-    #[props(!optional)]
-    min: Option<&'a str>,
-    #[props(!optional)]
-    max: Option<&'a str>,
-    #[props(!optional)]
-    step: Option<&'a str>,
-}
-
-#[allow(non_snake_case)]
-pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
-    let tui_query: Query = cx.consume_context().unwrap();
-
-    let value_state = use_state(cx, || 0.0);
-    let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
-    let width = cx.props.width.unwrap_or("20px");
-    let height = cx.props.height.unwrap_or("1px");
-    let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
-    let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
-    let size = max - min;
-    let step = cx
-        .props
-        .step
-        .and_then(|v| v.parse().ok())
-        .unwrap_or(size / 10.0);
-
-    let current_value = match value {
-        Some(value) => value,
-        None => *value_state.get(),
-    }
-    .clamp(min, max);
-
-    let fst_width = 100.0 * (current_value - min) / size;
-    let snd_width = 100.0 * (max - current_value) / size;
-    assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0);
-
-    let update = |value: String| {
-        if let Some(oninput) = cx.props.raw_oninput {
-            oninput.call(FormData {
-                value,
-                values: HashMap::new(),
-                files: None,
-            });
-        }
-    };
-
-    render! {
-        div{
-            width: "{width}",
-            height: "{height}",
-            display: "flex",
-            flex_direction: "row",
-            onkeydown: move |event| {
-                match event.key() {
-                    Key::ArrowLeft => {
-                        value_state.set((current_value - step).clamp(min, max));
-                        update(value_state.current().to_string());
-                    }
-                    Key::ArrowRight => {
-                        value_state.set((current_value + step).clamp(min, max));
-                        update(value_state.current().to_string());
-                    }
-                    _ => ()
-                }
-            },
-            onmousemove: move |evt| {
-                let mouse = evt.data;
-                if !mouse.held_buttons().is_empty(){
-                    let node = tui_query.get(get_root_id(cx).unwrap());
-                    let width = node.size().unwrap().width;
-                    let offset = mouse.element_coordinates();
-                    value_state.set(min + size*(offset.x as f32) / width as f32);
-                    update(value_state.current().to_string());
-                }
-            },
-            div{
-                width: "{fst_width}%",
-                background_color: "rgba(10,10,10,0.5)",
-            }
-            div{
-                "|"
-            }
-            div{
-                width: "{snd_width}%",
-                background_color: "rgba(10,10,10,0.5)",
-            }
-        }
-    }
-}

+ 0 - 182
packages/dioxus-tui/src/widgets/textbox.rs

@@ -1,182 +0,0 @@
-use crate::widgets::get_root_id;
-use crossterm::{cursor::*, execute};
-use dioxus::prelude::*;
-use dioxus_elements::input_data::keyboard_types::Key;
-use dioxus_html as dioxus_elements;
-use dioxus_html::FormData;
-use dioxus_native_core::utils::cursor::{Cursor, Pos};
-use rink::Query;
-use std::{collections::HashMap, io::stdout};
-use taffy::geometry::Point;
-
-#[derive(Props)]
-pub(crate) struct TextBoxProps<'a> {
-    #[props(!optional)]
-    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
-    #[props(!optional)]
-    value: Option<&'a str>,
-    #[props(!optional)]
-    size: Option<&'a str>,
-    #[props(!optional)]
-    max_length: Option<&'a str>,
-    #[props(!optional)]
-    width: Option<&'a str>,
-    #[props(!optional)]
-    height: Option<&'a str>,
-}
-#[allow(non_snake_case)]
-pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
-    let tui_query: Query = cx.consume_context().unwrap();
-    let tui_query_clone = tui_query.clone();
-
-    let text_ref = use_ref(cx, || {
-        if let Some(intial_text) = cx.props.value {
-            intial_text.to_string()
-        } else {
-            String::new()
-        }
-    });
-    let cursor = use_ref(cx, Cursor::default);
-    let dragging = use_state(cx, || false);
-
-    let text = text_ref.read().clone();
-    let start_highlight = cursor.read().first().idx(&*text);
-    let end_highlight = cursor.read().last().idx(&*text);
-    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
-    let (text_highlighted, text_after_second_cursor) =
-        text_after_first_cursor.split_at(end_highlight - start_highlight);
-
-    let max_len = cx
-        .props
-        .max_length
-        .as_ref()
-        .and_then(|s| s.parse().ok())
-        .unwrap_or(usize::MAX);
-
-    let width = cx
-        .props
-        .width
-        .map(|s| s.to_string())
-        // px is the same as em in tui
-        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
-        .unwrap_or_else(|| "10px".to_string());
-    let height = cx.props.height.unwrap_or("3px");
-
-    // don't draw a border unless there is enough space
-    let border = if width
-        .strip_suffix("px")
-        .and_then(|w| w.parse::<i32>().ok())
-        .filter(|w| *w < 3)
-        .is_some()
-        || height
-            .strip_suffix("px")
-            .and_then(|h| h.parse::<i32>().ok())
-            .filter(|h| *h < 3)
-            .is_some()
-    {
-        "none"
-    } else {
-        "solid"
-    };
-
-    render! {
-        div{
-            width: "{width}",
-            height: "{height}",
-            border_style: "{border}",
-
-            onkeydown: move |k| {
-                if k.key() == Key::Enter {
-                    return;
-                }
-                let mut text = text_ref.write();
-                cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
-                if let Some(input_handler) = &cx.props.raw_oninput{
-                    input_handler.call(FormData{
-                        value: text.clone(),
-                        values: HashMap::new(),
-                        files: None
-                    });
-                }
-
-                let node = tui_query.get(get_root_id(cx).unwrap());
-                let Point{ x, y } = node.pos().unwrap();
-
-                let Pos { col, row } = cursor.read().start;
-                let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
-                if let Ok(pos) = crossterm::cursor::position() {
-                    if pos != (x, y){
-                        execute!(stdout(), MoveTo(x, y)).unwrap();
-                    }
-                }
-                else{
-                    execute!(stdout(), MoveTo(x, y)).unwrap();
-                }
-            },
-
-            onmousemove: move |evt| {
-                if *dragging.get() {
-                    let offset = evt.data.element_coordinates();
-                    let mut new = Pos::new(offset.x as usize, offset.y as usize);
-                    if border != "none" {
-                        new.col = new.col.saturating_sub(1);
-                    }
-                    // textboxs are only one line tall
-                    new.row = 0;
-
-                    if new != cursor.read().start {
-                        cursor.write().end = Some(new);
-                    }
-                }
-            },
-            onmousedown: move |evt| {
-                let offset = evt.data.element_coordinates();
-                let mut new = Pos::new(offset.x as usize, offset.y as usize);
-                if border != "none" {
-                    new.col = new.col.saturating_sub(1);
-                }
-                // textboxs are only one line tall
-                new.row = 0;
-
-                new.realize_col(text_ref.read().as_str());
-                cursor.set(Cursor::from_start(new));
-                dragging.set(true);
-                let node = tui_query_clone.get(get_root_id(cx).unwrap());
-                let Point{ x, y } = node.pos().unwrap();
-
-                let Pos { col, row } = cursor.read().start;
-                let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
-                if let Ok(pos) = crossterm::cursor::position() {
-                    if pos != (x, y){
-                        execute!(stdout(), MoveTo(x, y)).unwrap();
-                    }
-                }
-                else{
-                    execute!(stdout(), MoveTo(x, y)).unwrap();
-                }
-            },
-            onmouseup: move |_| {
-                dragging.set(false);
-            },
-            onmouseleave: move |_| {
-                dragging.set(false);
-            },
-            onmouseenter: move |_| {
-                dragging.set(false);
-            },
-            onfocusout: |_| {
-                execute!(stdout(), MoveTo(0, 1000)).unwrap();
-            },
-
-            "{text_before_first_cursor}"
-
-            span{
-                background_color: "rgba(255, 255, 255, 50%)",
-
-                "{text_highlighted}"
-            }
-
-            "{text_after_second_cursor}"
-        }
-    }
-}

+ 4 - 4
packages/native-core-macro/src/lib.rs

@@ -224,14 +224,14 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream {
     let get_parent_view = {
         if parent_dependencies.is_empty() {
             quote! {
-                let raw_parent = tree.parent_id(id).map(|_| ());
+                let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).map(|_| ());
             }
         } else {
             let temps = (0..parent_dependencies.len())
                 .map(|i| format_ident!("__temp{}", i))
                 .collect::<Vec<_>>();
             quote! {
-                let raw_parent = tree.parent_id(id).and_then(|parent_id| {
+                let raw_parent = tree.parent_id_advanced(id, Self::TRAVERSE_SHADOW_DOM).and_then(|parent_id| {
                     let raw_parent: Option<(#(*const #parent_dependencies,)*)> = (#(&#parent_view,)*).get(parent_id).ok().map(|c| {
                         let (#(#temps,)*) = c;
                         (#(#temps as *const _,)*)
@@ -261,14 +261,14 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream {
     let get_child_view = {
         if child_dependencies.is_empty() {
             quote! {
-                let raw_children: Vec<_> = tree.children_ids(id).into_iter().map(|_| ()).collect();
+                let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().map(|_| ()).collect();
             }
         } else {
             let temps = (0..child_dependencies.len())
                 .map(|i| format_ident!("__temp{}", i))
                 .collect::<Vec<_>>();
             quote! {
-                let raw_children: Vec<_> = tree.children_ids(id).into_iter().filter_map(|id| {
+                let raw_children: Vec<_> = tree.children_ids_advanced(id, Self::TRAVERSE_SHADOW_DOM).into_iter().filter_map(|id| {
                     let raw_children: Option<(#(*const #child_dependencies,)*)> = (#(&#child_view,)*).get(id).ok().map(|c| {
                         let (#(#temps,)*) = c;
                         (#(#temps as *const _,)*)

+ 1 - 1
packages/native-core/examples/custom_attr.rs

@@ -215,7 +215,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                 let _to_rerender = rdom.update_state(ctx);
 
                 // render...
-                rdom.traverse_depth_first(|node| {
+                rdom.traverse_depth_first_advanced(true, |node| {
                     let indent = " ".repeat(node.height() as usize);
                     let color = *node.get::<TextColor>().unwrap();
                     let size = *node.get::<Size>().unwrap();

+ 1 - 1
packages/native-core/examples/font_size.rs

@@ -160,7 +160,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                 let _to_rerender = rdom.update_state(ctx);
 
                 // render...
-                rdom.traverse_depth_first(|node| {
+                rdom.traverse_depth_first_advanced(true, |node| {
                     let indent = " ".repeat(node.height() as usize);
                     let font_size = *node.get::<FontSize>().unwrap();
                     let size = *node.get::<Size>().unwrap();

+ 1 - 1
packages/native-core/examples/simple.rs

@@ -206,7 +206,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                 let _to_rerender = rdom.update_state(ctx);
 
                 // render...
-                rdom.traverse_depth_first(|node| {
+                rdom.traverse_depth_first_advanced(true, |node| {
                     let indent = " ".repeat(node.height() as usize);
                     let color = *node.get::<TextColor>().unwrap();
                     let size = *node.get::<Size>().unwrap();

+ 1 - 1
packages/native-core/examples/simple_dioxus.rs

@@ -234,7 +234,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
                 let _to_rerender = rdom.update_state(ctx);
 
                 // render...
-                rdom.traverse_depth_first(|node| {
+                rdom.traverse_depth_first_advanced(true, |node| {
                     let indent = " ".repeat(node.height() as usize);
                     let color = *node.get::<TextColor>().unwrap();
                     let size = *node.get::<Size>().unwrap();

+ 180 - 0
packages/native-core/src/custom_element.rs

@@ -0,0 +1,180 @@
+//! A custom element is a controlled element that renders to a shadow DOM. This allows you to create elements that act like widgets without relying on a specific framework.
+//!
+//! Each custom element is registered with a element name and namespace with [`RealDom::register_custom_element`] or [`RealDom::register_custom_element_with_factory`]. Once registered, they will be created automatically when the element is added to the DOM.
+
+// Used in doc links
+#[allow(unused)]
+use crate::real_dom::RealDom;
+
+use std::sync::{Arc, RwLock};
+
+use rustc_hash::FxHashMap;
+use shipyard::Component;
+
+use crate::{
+    node::{FromAnyValue, NodeType},
+    node_ref::AttributeMask,
+    prelude::{NodeImmutable, NodeMut},
+    tree::TreeMut,
+    NodeId,
+};
+
+pub(crate) struct CustomElementRegistry<V: FromAnyValue + Send + Sync> {
+    builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder<V>>,
+}
+
+impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
+    fn default() -> Self {
+        Self {
+            builders: FxHashMap::default(),
+        }
+    }
+}
+
+impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
+    pub fn register<F, U>(&mut self)
+    where
+        F: CustomElementFactory<U, V>,
+        U: CustomElementUpdater<V>,
+    {
+        self.builders.insert(
+            (F::NAME, F::NAMESPACE),
+            CustomElementBuilder {
+                create: |node| Box::new(F::create(node)),
+            },
+        );
+    }
+
+    pub fn add_shadow_dom(&self, mut node: NodeMut<V>) {
+        let element_tag = if let NodeType::Element(el) = &*node.node_type() {
+            Some((el.tag.clone(), el.namespace.clone()))
+        } else {
+            None
+        };
+        if let Some((tag, ns)) = element_tag {
+            if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) {
+                let boxed_custom_element = { (builder.create)(node.reborrow()) };
+
+                let shadow_roots = boxed_custom_element.roots();
+
+                let light_id = node.id();
+                node.real_dom_mut().tree_mut().create_subtree(
+                    light_id,
+                    shadow_roots,
+                    boxed_custom_element.slot(),
+                );
+
+                let boxed_custom_element = CustomElementManager {
+                    inner: Arc::new(RwLock::new(boxed_custom_element)),
+                };
+
+                node.insert(boxed_custom_element);
+            }
+        }
+    }
+}
+
+struct CustomElementBuilder<V: FromAnyValue + Send + Sync> {
+    create: fn(NodeMut<V>) -> Box<dyn CustomElementUpdater<V>>,
+}
+
+/// A controlled element that renders to a shadow DOM.
+///
+/// Register with [`RealDom::register_custom_element`]
+///
+/// This is a simplified custom element trait for elements that can create themselves. For more granular control, implement [`CustomElementFactory`] and [`CustomElementUpdater`] instead.
+pub trait CustomElement<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
+    /// The tag of the element
+    const NAME: &'static str;
+
+    /// The namespace of the element
+    const NAMESPACE: Option<&'static str> = None;
+
+    /// Create a new element *without mounting* it.
+    /// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
+    fn create(light_root: NodeMut<V>) -> Self;
+
+    /// The root node of the custom element. These roots must be not change once the element is created.
+    fn roots(&self) -> Vec<NodeId>;
+
+    /// The slot to render children of the element into. The slot must be not change once the element is created.
+    fn slot(&self) -> Option<NodeId> {
+        None
+    }
+
+    /// Update the custom element's shadow tree with the new attributes.
+    /// Called when the attributes of the custom element are changed.
+    fn attributes_changed(&mut self, light_node: NodeMut<V>, attributes: &AttributeMask);
+}
+
+/// A factory for creating custom elements
+///
+/// Register with [`RealDom::register_custom_element_with_factory`]
+pub trait CustomElementFactory<W: CustomElementUpdater<V>, V: FromAnyValue + Send + Sync = ()>:
+    Send + Sync + 'static
+{
+    /// The tag of the element
+    const NAME: &'static str;
+
+    /// The namespace of the element
+    const NAMESPACE: Option<&'static str> = None;
+
+    /// Create a new element *without mounting* it.
+    /// The node passed in is the light DOM node. The element should not modify the light DOM node, but it can get the [`NodeMut::real_dom_mut`] from the node to create new nodes.
+    fn create(dom: NodeMut<V>) -> W;
+}
+
+impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementFactory<W, V> for W {
+    const NAME: &'static str = W::NAME;
+
+    const NAMESPACE: Option<&'static str> = W::NAMESPACE;
+
+    fn create(node: NodeMut<V>) -> Self {
+        Self::create(node)
+    }
+}
+
+/// A trait for updating custom elements
+pub trait CustomElementUpdater<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
+    /// Update the custom element's shadow tree with the new attributes.
+    /// Called when the attributes of the custom element are changed.
+    fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask);
+
+    /// The root node of the custom element. These roots must be not change once the element is created.
+    fn roots(&self) -> Vec<NodeId>;
+
+    /// The slot to render children of the element into. The slot must be not change once the element is created.
+    fn slot(&self) -> Option<NodeId> {
+        None
+    }
+}
+
+impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementUpdater<V> for W {
+    fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask) {
+        self.attributes_changed(light_root, attributes);
+    }
+
+    fn roots(&self) -> Vec<NodeId> {
+        self.roots()
+    }
+
+    fn slot(&self) -> Option<NodeId> {
+        self.slot()
+    }
+}
+
+/// A dynamic trait object wrapper for [`CustomElementUpdater`]
+#[derive(Component, Clone)]
+pub(crate) struct CustomElementManager<V: FromAnyValue = ()> {
+    inner: Arc<RwLock<Box<dyn CustomElementUpdater<V>>>>,
+}
+
+impl<V: FromAnyValue + Send + Sync> CustomElementManager<V> {
+    /// Update the custom element based on attributes changed.
+    pub fn on_attributes_changed(&self, light_root: NodeMut<V>, attributes: &AttributeMask) {
+        self.inner
+            .write()
+            .unwrap()
+            .attributes_changed(light_root, attributes);
+    }
+}

+ 2 - 0
packages/native-core/src/lib.rs

@@ -7,6 +7,7 @@ use std::hash::BuildHasherDefault;
 use node_ref::NodeMask;
 use rustc_hash::FxHasher;
 
+pub mod custom_element;
 #[cfg(feature = "dioxus")]
 pub mod dioxus;
 #[cfg(feature = "layout-attributes")]
@@ -18,6 +19,7 @@ mod passes;
 pub mod real_dom;
 pub mod tree;
 pub mod utils;
+
 pub use shipyard::EntityId as NodeId;
 
 pub mod exports {

+ 28 - 3
packages/native-core/src/node_ref.rs

@@ -60,7 +60,7 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> {
             NodeType::Element(ElementNode { attributes, .. }) => Some(
                 attributes
                     .iter()
-                    .filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name))
+                    .filter(move |(attr, _)| self.mask.attritutes.contains(&attr.name))
                     .map(|(attr, val)| OwnedAttributeView {
                         attribute: attr,
                         value: val,
@@ -107,8 +107,8 @@ pub enum AttributeMask {
 }
 
 impl AttributeMask {
-    /// Check if the given attribute is visible
-    fn contains_attribute(&self, attr: &str) -> bool {
+    /// Check if the mask contains the given attribute
+    pub fn contains(&self, attr: &str) -> bool {
         match self {
             AttributeMask::All => true,
             AttributeMask::Some(attrs) => attrs.contains(attr),
@@ -187,25 +187,50 @@ impl NodeMask {
         self.attritutes = self.attritutes.union(&attributes);
     }
 
+    /// Get the mask for the attributes
+    pub fn attributes(&self) -> &AttributeMask {
+        &self.attritutes
+    }
+
     /// Set the mask to view the tag
     pub fn set_tag(&mut self) {
         self.tag = true;
     }
 
+    /// Get the mask for the tag
+    pub fn tag(&self) -> bool {
+        self.tag
+    }
+
     /// Set the mask to view the namespace
     pub fn set_namespace(&mut self) {
         self.namespace = true;
     }
 
+    /// Get the mask for the namespace
+    pub fn namespace(&self) -> bool {
+        self.namespace
+    }
+
     /// Set the mask to view the text
     pub fn set_text(&mut self) {
         self.text = true;
     }
 
+    /// Get the mask for the text
+    pub fn text(&self) -> bool {
+        self.text
+    }
+
     /// Set the mask to view the listeners
     pub fn set_listeners(&mut self) {
         self.listeners = true;
     }
+
+    /// Get the mask for the listeners
+    pub fn listeners(&self) -> bool {
+        self.listeners
+    }
 }
 
 /// A builder for a mask that controls what attributes are visible.

+ 10 - 8
packages/native-core/src/node_watcher.rs

@@ -1,17 +1,19 @@
 //! Helpers for watching for changes in the DOM tree.
 
-use crate::{node::FromAnyValue, prelude::*};
+use crate::{node::FromAnyValue, node_ref::AttributeMask, prelude::*};
 
 /// A trait for watching for changes in the DOM tree.
 pub trait NodeWatcher<V: FromAnyValue + Send + Sync> {
     /// Called after a node is added to the tree.
-    fn on_node_added(&self, _node: NodeMut<V>) {}
+    fn on_node_added(&mut self, _node: NodeMut<V>) {}
     /// Called before a node is removed from the tree.
-    fn on_node_removed(&self, _node: NodeMut<V>) {}
+    fn on_node_removed(&mut self, _node: NodeMut<V>) {}
     /// Called after a node is moved to a new parent.
-    fn on_node_moved(&self, _node: NodeMut<V>) {}
-    // /// Called after the text content of a node is changed.
-    // fn on_text_changed(&self, _node: NodeMut<V>) {}
-    // /// Called after an attribute of an element is changed.
-    // fn on_attribute_changed(&self, _node: NodeMut<V>, attribute: &str) {}
+    fn on_node_moved(&mut self, _node: NodeMut<V>) {}
+}
+
+/// A trait for watching for changes to attributes of an element.
+pub trait AttributeWatcher<V: FromAnyValue + Send + Sync> {
+    /// Called before update_state is called on the RealDom
+    fn on_attributes_changed(&self, _node: NodeMut<V>, _attributes: &AttributeMask) {}
 }

+ 28 - 9
packages/native-core/src/passes.rs

@@ -104,6 +104,9 @@ pub trait State<V: FromAnyValue + Send + Sync = ()>: Any + Send + Sync {
     /// This is a mask of what aspects of the node are required to update this state
     const NODE_MASK: NodeMaskBuilder<'static>;
 
+    /// Does the state traverse into the shadow dom or pass over it. This should be true for layout and false for styles
+    const TRAVERSE_SHADOW_DOM: bool = false;
+
     /// Update this state in a node, returns if the state was updated
     fn update<'a>(
         &mut self,
@@ -150,6 +153,7 @@ pub trait State<V: FromAnyValue + Send + Sync = ()>: Any + Send + Sync {
             dependants: Default::default(),
             mask: node_mask,
             pass_direction: pass_direction::<V, Self>(),
+            enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM,
             workload: Self::workload_system,
             phantom: PhantomData,
         }
@@ -228,28 +232,42 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Dependant {
+    pub(crate) type_id: TypeId,
+    pub(crate) enter_shadow_dom: bool,
+}
+
 /// The states that depend on this state
 #[derive(Default, Debug, Clone, PartialEq, Eq)]
 pub struct Dependants {
     /// The states in the parent direction that should be invalidated when this state is invalidated
-    pub parent: Vec<TypeId>,
+    pub(crate) parent: Vec<Dependant>,
     /// The states in the child direction that should be invalidated when this state is invalidated
-    pub child: Vec<TypeId>,
+    pub(crate) child: Vec<Dependant>,
     /// The states in the node direction that should be invalidated when this state is invalidated
-    pub node: Vec<TypeId>,
+    pub(crate) node: Vec<TypeId>,
 }
 
 impl Dependants {
     fn mark_dirty(&self, dirty: &DirtyNodeStates, id: NodeId, tree: &impl TreeRef, height: u16) {
-        for dependant in &self.child {
-            for id in tree.children_ids(id) {
-                dirty.insert(*dependant, id, height + 1);
+        for &Dependant {
+            type_id,
+            enter_shadow_dom,
+        } in &self.child
+        {
+            for id in tree.children_ids_advanced(id, enter_shadow_dom) {
+                dirty.insert(type_id, id, height + 1);
             }
         }
 
-        for dependant in &self.parent {
-            if let Some(id) = tree.parent_id(id) {
-                dirty.insert(*dependant, id, height - 1);
+        for &Dependant {
+            type_id,
+            enter_shadow_dom,
+        } in &self.parent
+        {
+            if let Some(id) = tree.parent_id_advanced(id, enter_shadow_dom) {
+                dirty.insert(type_id, id, height - 1);
             }
         }
 
@@ -269,6 +287,7 @@ pub struct TypeErasedState<V: FromAnyValue + Send = ()> {
     pub(crate) mask: NodeMask,
     pub(crate) workload: fn(TypeId, Arc<Dependants>, PassDirection) -> WorkloadSystem,
     pub(crate) pass_direction: PassDirection,
+    pub(crate) enter_shadow_dom: bool,
     phantom: PhantomData<V>,
 }
 

+ 208 - 26
packages/native-core/src/real_dom.rs

@@ -10,12 +10,16 @@ use std::collections::VecDeque;
 use std::ops::{Deref, DerefMut};
 use std::sync::{Arc, RwLock};
 
+use crate::custom_element::{
+    CustomElement, CustomElementFactory, CustomElementManager, CustomElementRegistry,
+    CustomElementUpdater,
+};
 use crate::node::{
     ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode,
 };
 use crate::node_ref::{NodeMask, NodeMaskBuilder};
-use crate::node_watcher::NodeWatcher;
-use crate::passes::{DirtyNodeStates, PassDirection, TypeErasedState};
+use crate::node_watcher::{AttributeWatcher, NodeWatcher};
+use crate::passes::{Dependant, DirtyNodeStates, PassDirection, TypeErasedState};
 use crate::prelude::AttributeMaskBuilder;
 use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView};
 use crate::NodeId;
@@ -49,6 +53,7 @@ impl Deref for DirtyNodesResult {
 pub(crate) struct NodesDirty<V: FromAnyValue + Send + Sync> {
     passes_updated: FxHashMap<NodeId, FxHashSet<TypeId>>,
     nodes_updated: FxHashMap<NodeId, NodeMask>,
+    nodes_created: FxHashSet<NodeId>,
     pub(crate) passes: Box<[TypeErasedState<V>]>,
 }
 
@@ -92,6 +97,7 @@ impl<V: FromAnyValue + Send + Sync> NodesDirty<V> {
 }
 
 type NodeWatchers<V> = Arc<RwLock<Vec<Box<dyn NodeWatcher<V> + Send + Sync>>>>;
+type AttributeWatchers<V> = Arc<RwLock<Vec<Box<dyn AttributeWatcher<V> + Send + Sync>>>>;
 
 /// A Dom that can sync with the VirtualDom mutations intended for use in lazy renderers.
 /// The render state passes from parent to children and or accumulates state from children to parents.
@@ -108,8 +114,10 @@ pub struct RealDom<V: FromAnyValue + Send + Sync = ()> {
     nodes_listening: FxHashMap<String, FxHashSet<NodeId>>,
     pub(crate) dirty_nodes: NodesDirty<V>,
     node_watchers: NodeWatchers<V>,
+    attribute_watchers: AttributeWatchers<V>,
     workload: ScheduledWorkload,
     root_id: NodeId,
+    custom_elements: Arc<RwLock<CustomElementRegistry<V>>>,
     phantom: std::marker::PhantomData<V>,
 }
 
@@ -123,19 +131,25 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
             let (current, before) = before.split_last_mut().unwrap();
             for state in before.iter_mut().chain(after.iter_mut()) {
                 let dependants = Arc::get_mut(&mut state.dependants).unwrap();
+
+                let current_dependant = Dependant {
+                    type_id: current.this_type_id,
+                    enter_shadow_dom: current.enter_shadow_dom,
+                };
+
                 // If this node depends on the other state as a parent, then the other state should update its children of the current type when it is invalidated
                 if current
                     .parent_dependancies_ids
                     .contains(&state.this_type_id)
-                    && !dependants.child.contains(&current.this_type_id)
+                    && !dependants.child.contains(&current_dependant)
                 {
-                    dependants.child.push(current.this_type_id);
+                    dependants.child.push(current_dependant);
                 }
                 // If this node depends on the other state as a child, then the other state should update its parent of the current type when it is invalidated
                 if current.child_dependancies_ids.contains(&state.this_type_id)
-                    && !dependants.parent.contains(&current.this_type_id)
+                    && !dependants.parent.contains(&current_dependant)
                 {
-                    dependants.parent.push(current.this_type_id);
+                    dependants.parent.push(current_dependant);
                 }
                 // If this node depends on the other state as a sibling, then the other state should update its siblings of the current type when it is invalidated
                 if current.node_dependancies_ids.contains(&state.this_type_id)
@@ -146,15 +160,19 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
             }
             // If the current state depends on itself, then it should update itself when it is invalidated
             let dependants = Arc::get_mut(&mut current.dependants).unwrap();
+            let current_dependant = Dependant {
+                type_id: current.this_type_id,
+                enter_shadow_dom: current.enter_shadow_dom,
+            };
             match current.pass_direction {
                 PassDirection::ChildToParent => {
-                    if !dependants.parent.contains(&current.this_type_id) {
-                        dependants.parent.push(current.this_type_id);
+                    if !dependants.parent.contains(&current_dependant) {
+                        dependants.parent.push(current_dependant);
                     }
                 }
                 PassDirection::ParentToChild => {
-                    if !dependants.child.contains(&current.this_type_id) {
-                        dependants.child.push(current.this_type_id);
+                    if !dependants.child.contains(&current_dependant) {
+                        dependants.child.push(current_dependant);
                     }
                 }
                 _ => {}
@@ -191,10 +209,13 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
                 passes_updated,
                 nodes_updated,
                 passes: tracked_states,
+                nodes_created: [root_id].into_iter().collect(),
             },
             node_watchers: Default::default(),
+            attribute_watchers: Default::default(),
             workload,
             root_id,
+            custom_elements: Default::default(),
             phantom: std::marker::PhantomData,
         }
     }
@@ -211,8 +232,12 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
 
     /// Create a new node of the given type in the dom and return a mutable reference to it.
     pub fn create_node(&mut self, node: impl Into<NodeType<V>>) -> NodeMut<'_, V> {
-        let id = self.world.add_entity(node.into());
+        let node = node.into();
+        let is_element = matches!(node, NodeType::Element(_));
+
+        let id = self.world.add_entity(node);
         self.tree_mut().create_node(id);
+
         self.dirty_nodes
             .passes_updated
             .entry(id)
@@ -220,10 +245,17 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
             .extend(self.dirty_nodes.passes.iter().map(|x| x.this_type_id));
         self.dirty_nodes
             .mark_dirty(id, NodeMaskBuilder::ALL.build());
-        let watchers = self.node_watchers.clone();
-        for watcher in &*watchers.read().unwrap() {
-            watcher.on_node_added(NodeMut::new(id, self));
+        self.dirty_nodes.nodes_created.insert(id);
+
+        // Create a custom element if needed
+        if is_element {
+            let custom_elements = self.custom_elements.clone();
+            custom_elements
+                .read()
+                .unwrap()
+                .add_shadow_dom(NodeMut::new(id, self));
         }
+
         NodeMut::new(id, self)
     }
 
@@ -284,8 +316,48 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
         &mut self,
         ctx: SendAnyMap,
     ) -> (FxDashSet<NodeId>, FxHashMap<NodeId, NodeMask>) {
+        let nodes_created = std::mem::take(&mut self.dirty_nodes.nodes_created);
+
+        // call node watchers
+        {
+            let watchers = self.node_watchers.clone();
+
+            // ignore watchers if they are already being modified
+            if let Ok(mut watchers) = watchers.try_write() {
+                for id in &nodes_created {
+                    for watcher in &mut *watchers {
+                        watcher.on_node_added(NodeMut::new(*id, self));
+                    }
+                }
+            };
+        }
+
         let passes = std::mem::take(&mut self.dirty_nodes.passes_updated);
         let nodes_updated = std::mem::take(&mut self.dirty_nodes.nodes_updated);
+
+        for (node_id, mask) in &nodes_updated {
+            if self.contains(*node_id) {
+                // call attribute watchers but ignore watchers if they are already being modified
+                let watchers = self.attribute_watchers.clone();
+                if let Ok(mut watchers) = watchers.try_write() {
+                    for watcher in &mut *watchers {
+                        watcher.on_attributes_changed(
+                            self.get_mut(*node_id).unwrap(),
+                            mask.attributes(),
+                        );
+                    }
+                };
+
+                // call custom element watchers
+                let node = self.get_mut(*node_id).unwrap();
+                let custom_element_manager =
+                    node.get::<CustomElementManager<V>>().map(|x| x.clone());
+                if let Some(custom_element_manager) = custom_element_manager {
+                    custom_element_manager.on_attributes_changed(node, mask.attributes());
+                }
+            }
+        }
+
         let dirty_nodes =
             DirtyNodeStates::with_passes(self.dirty_nodes.passes.iter().map(|p| p.this_type_id));
         let tree = self.tree_ref();
@@ -312,27 +384,42 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
     }
 
     /// Traverses the dom in a depth first manner, calling the provided function on each node.
-    pub fn traverse_depth_first(&self, mut f: impl FnMut(NodeRef<V>)) {
+    /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree.
+    pub fn traverse_depth_first_advanced(
+        &self,
+        enter_shadow_dom: bool,
+        mut f: impl FnMut(NodeRef<V>),
+    ) {
         let mut stack = vec![self.root_id()];
         let tree = self.tree_ref();
         while let Some(id) = stack.pop() {
             if let Some(node) = self.get(id) {
                 f(node);
-                let children = tree.children_ids(id);
+                let children = tree.children_ids_advanced(id, enter_shadow_dom);
                 stack.extend(children.iter().copied().rev());
             }
         }
     }
 
+    /// Traverses the dom in a depth first manner, calling the provided function on each node.
+    pub fn traverse_depth_first(&self, f: impl FnMut(NodeRef<V>)) {
+        self.traverse_depth_first_advanced(true, f)
+    }
+
     /// Traverses the dom in a breadth first manner, calling the provided function on each node.
-    pub fn traverse_breadth_first(&self, mut f: impl FnMut(NodeRef<V>)) {
+    /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree.
+    pub fn traverse_breadth_first_advanced(
+        &self,
+        enter_shadow_doms: bool,
+        mut f: impl FnMut(NodeRef<V>),
+    ) {
         let mut queue = VecDeque::new();
         queue.push_back(self.root_id());
         let tree = self.tree_ref();
         while let Some(id) = queue.pop_front() {
             if let Some(node) = self.get(id) {
                 f(node);
-                let children = tree.children_ids(id);
+                let children = tree.children_ids_advanced(id, enter_shadow_doms);
                 for id in children {
                     queue.push_back(id);
                 }
@@ -340,12 +427,22 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
         }
     }
 
+    /// Traverses the dom in a breadth first manner, calling the provided function on each node.
+    pub fn traverse_breadth_first(&self, f: impl FnMut(NodeRef<V>)) {
+        self.traverse_breadth_first_advanced(true, f);
+    }
+
     /// Traverses the dom in a depth first manner mutably, calling the provided function on each node.
-    pub fn traverse_depth_first_mut(&mut self, mut f: impl FnMut(NodeMut<V>)) {
+    /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree.
+    pub fn traverse_depth_first_mut_advanced(
+        &mut self,
+        enter_shadow_doms: bool,
+        mut f: impl FnMut(NodeMut<V>),
+    ) {
         let mut stack = vec![self.root_id()];
         while let Some(id) = stack.pop() {
             let tree = self.tree_ref();
-            let mut children = tree.children_ids(id);
+            let mut children = tree.children_ids_advanced(id, enter_shadow_doms);
             drop(tree);
             children.reverse();
             if let Some(node) = self.get_mut(id) {
@@ -356,13 +453,23 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
         }
     }
 
+    /// Traverses the dom in a depth first manner mutably, calling the provided function on each node.
+    pub fn traverse_depth_first_mut(&mut self, f: impl FnMut(NodeMut<V>)) {
+        self.traverse_depth_first_mut_advanced(true, f)
+    }
+
     /// Traverses the dom in a breadth first manner mutably, calling the provided function on each node.
-    pub fn traverse_breadth_first_mut(&mut self, mut f: impl FnMut(NodeMut<V>)) {
+    /// If `enter_shadow_dom` is true, then the traversal will enter shadow doms in the tree.
+    pub fn traverse_breadth_first_mut_advanced(
+        &mut self,
+        enter_shadow_doms: bool,
+        mut f: impl FnMut(NodeMut<V>),
+    ) {
         let mut queue = VecDeque::new();
         queue.push_back(self.root_id());
         while let Some(id) = queue.pop_front() {
             let tree = self.tree_ref();
-            let children = tree.children_ids(id);
+            let children = tree.children_ids_advanced(id, enter_shadow_doms);
             drop(tree);
             if let Some(node) = self.get_mut(id) {
                 f(node);
@@ -373,11 +480,27 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
         }
     }
 
+    /// Traverses the dom in a breadth first manner mutably, calling the provided function on each node.
+    pub fn traverse_breadth_first_mut(&mut self, f: impl FnMut(NodeMut<V>)) {
+        self.traverse_breadth_first_mut_advanced(true, f);
+    }
+
     /// Adds a [`NodeWatcher`] to the dom. Node watchers are called whenever a node is created or removed.
     pub fn add_node_watcher(&mut self, watcher: impl NodeWatcher<V> + 'static + Send + Sync) {
         self.node_watchers.write().unwrap().push(Box::new(watcher));
     }
 
+    /// Adds an [`AttributeWatcher`] to the dom. Attribute watchers are called whenever an attribute is changed.
+    pub fn add_attribute_watcher(
+        &mut self,
+        watcher: impl AttributeWatcher<V> + 'static + Send + Sync,
+    ) {
+        self.attribute_watchers
+            .write()
+            .unwrap()
+            .push(Box::new(watcher));
+    }
+
     /// Returns a reference to the underlying world. Any changes made to the world will not update the reactive system.
     pub fn raw_world(&self) -> &World {
         &self.world
@@ -387,6 +510,20 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
     pub fn raw_world_mut(&mut self) -> &mut World {
         &mut self.world
     }
+
+    /// Registers a new custom element.
+    pub fn register_custom_element<E: CustomElement<V>>(&mut self) {
+        self.register_custom_element_with_factory::<E, E>()
+    }
+
+    /// Registers a new custom element with a custom factory.
+    pub fn register_custom_element_with_factory<F, U>(&mut self)
+    where
+        F: CustomElementFactory<U, V>,
+        U: CustomElementUpdater<V>,
+    {
+        self.custom_elements.write().unwrap().register::<F, U>()
+    }
 }
 
 /// A reference to a tracked component in a node.
@@ -458,6 +595,14 @@ pub trait NodeImmutable<V: FromAnyValue + Send + Sync = ()>: Sized {
             .then(|| ViewEntry::new(view, self.id()))
     }
 
+    /// Get the ids of the children of the current node, if enter_shadow_dom is true and the current node is a shadow slot, the ids of the nodes under the node the shadow slot is attached to will be returned
+    #[inline]
+    fn children_ids_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Vec<NodeId> {
+        self.real_dom()
+            .tree_ref()
+            .children_ids_advanced(id, enter_shadow_dom)
+    }
+
     /// Get the ids of the children of the current node
     #[inline]
     fn child_ids(&self) -> Vec<NodeId> {
@@ -476,6 +621,14 @@ pub trait NodeImmutable<V: FromAnyValue + Send + Sync = ()>: Sized {
             .collect()
     }
 
+    /// Get the id of the parent of the current node, if enter_shadow_dom is true and the current node is a shadow root, the node the shadow root is attached to will be returned
+    #[inline]
+    fn parent_id_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Option<NodeId> {
+        self.real_dom()
+            .tree_ref()
+            .parent_id_advanced(id, enter_shadow_dom)
+    }
+
     /// Get the id of the parent of the current node
     #[inline]
     fn parent_id(&self) -> Option<NodeId> {
@@ -585,6 +738,14 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeImmutable<V> for NodeMut<'a, V> {
 }
 
 impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> {
+    /// Reborrow the node mutably
+    pub fn reborrow(&mut self) -> NodeMut<'_, V> {
+        NodeMut {
+            id: self.id,
+            dom: self.dom,
+        }
+    }
+
     /// Get the real dom this node was created in mutably
     #[inline(always)]
     pub fn real_dom_mut(&mut self) -> &mut RealDom<V> {
@@ -741,6 +902,7 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> {
         }
         let id = self.id();
         self.dom.tree_mut().replace(id, new);
+        self.remove();
     }
 
     /// Add an event listener
@@ -798,7 +960,7 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> {
     /// mark that this node was removed for the incremental system
     fn mark_removed(&mut self) {
         let watchers = self.dom.node_watchers.clone();
-        for watcher in &*watchers.read().unwrap() {
+        for watcher in &mut *watchers.write().unwrap() {
             watcher.on_node_removed(NodeMut::new(self.id(), self.dom));
         }
     }
@@ -806,9 +968,12 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> {
     /// mark that this node was moved for the incremental system
     fn mark_moved(&mut self) {
         let watchers = self.dom.node_watchers.clone();
-        for watcher in &*watchers.read().unwrap() {
-            watcher.on_node_moved(NodeMut::new(self.id(), self.dom));
-        }
+        // ignore watchers if the we are inside of a watcher
+        if let Ok(mut watchers) = watchers.try_write() {
+            for watcher in &mut *watchers {
+                watcher.on_node_moved(NodeMut::new(self.id(), self.dom));
+            }
+        };
     }
 
     /// Get a mutable reference to the type of the current node
@@ -925,6 +1090,15 @@ pub struct ElementNodeMut<'a, V: FromAnyValue + Send + Sync = ()> {
     dirty_nodes: &'a mut NodesDirty<V>,
 }
 
+impl std::fmt::Debug for ElementNodeMut<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ElementNodeMut")
+            .field("id", &self.id)
+            .field("element", &*self.element)
+            .finish()
+    }
+}
+
 impl<V: FromAnyValue + Send + Sync> ElementNodeMut<'_, V> {
     /// Get the current element
     fn element(&self) -> &ElementNode<V> {
@@ -1016,6 +1190,14 @@ impl<V: FromAnyValue + Send + Sync> ElementNodeMut<'_, V> {
         self.element_mut().attributes.get_mut(name)
     }
 
+    /// Get an attribute of the element
+    pub fn get_attribute(
+        &self,
+        name: &OwnedAttributeDiscription,
+    ) -> Option<&OwnedAttributeValue<V>> {
+        self.element().attributes.get(name)
+    }
+
     /// Get the set of all events the element is listening to
     pub fn listeners(&self) -> &FxHashSet<String> {
         &self.element().listeners

+ 305 - 29
packages/native-core/src/tree.rs

@@ -4,11 +4,25 @@ use crate::NodeId;
 use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut};
 use std::fmt::Debug;
 
+/// A shadow tree reference inside of a tree. This tree is isolated from the main tree.
+#[derive(PartialEq, Eq, Clone, Debug, Component)]
+pub struct ShadowTree {
+    /// The root of the shadow tree
+    pub shadow_roots: Vec<NodeId>,
+    /// The node that children of the super tree should be inserted under.
+    pub slot: Option<NodeId>,
+}
+
 /// A node in a tree.
 #[derive(PartialEq, Eq, Clone, Debug, Component)]
 pub struct Node {
     parent: Option<NodeId>,
     children: Vec<NodeId>,
+    child_subtree: Option<ShadowTree>,
+    /// If this node is a slot in a shadow_tree, this is node whose child_subtree is that shadow_tree.
+    slot_for_light_tree: Option<NodeId>,
+    /// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree.
+    root_for_light_tree: Option<NodeId>,
     height: u16,
 }
 
@@ -19,10 +33,51 @@ pub type TreeMutView<'a> = (EntitiesViewMut<'a>, ViewMut<'a, Node>);
 
 /// A immutable view of a tree.
 pub trait TreeRef {
+    /// Get the id of the parent of the current node, if enter_shadow_dom is true and the current node is a shadow root, the node the shadow root is attached to will be returned
+    #[inline]
+    fn parent_id_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Option<NodeId> {
+        // If this node is the root of a shadow_tree, return the node the shadow_tree is attached
+        let root_for_light_tree = self.root_for_light_tree(id);
+        match (root_for_light_tree, enter_shadow_dom) {
+            (Some(id), true) => Some(id),
+            _ => {
+                let parent_id = self.parent_id(id);
+                if enter_shadow_dom {
+                    // If this node is attached via a slot, return the slot as the parent instead of the light tree parent
+                    parent_id.map(|id| {
+                        self.shadow_tree(id)
+                            .and_then(|tree| tree.slot)
+                            .unwrap_or(id)
+                    })
+                } else {
+                    parent_id
+                }
+            }
+        }
+    }
     /// The parent id of the node.
     fn parent_id(&self, id: NodeId) -> Option<NodeId>;
+    /// Get the ids of the children of the current node, if enter_shadow_dom is true and the current node is a shadow slot, the ids of the nodes under the node the shadow slot is attached to will be returned
+    #[inline]
+    fn children_ids_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Vec<NodeId> {
+        let shadow_tree = self.shadow_tree(id);
+        let slot_of_light_tree = self.slot_for_light_tree(id);
+        match (shadow_tree, slot_of_light_tree, enter_shadow_dom) {
+            // If this node is a shadow root, return the shadow roots
+            (Some(tree), _, true) => tree.shadow_roots.clone(),
+            // If this node is a slot, return the children of the node the slot is attached to
+            (None, Some(id), true) => self.children_ids(id),
+            _ => self.children_ids(id),
+        }
+    }
     /// The children ids of the node.
     fn children_ids(&self, id: NodeId) -> Vec<NodeId>;
+    /// The shadow tree tree under the node.
+    fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>;
+    /// The node that contains the shadow tree this node is a slot for
+    fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
+    /// The node that contains the shadow tree this node is a root of
+    fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId>;
     /// The height of the node.
     fn height(&self, id: NodeId) -> Option<u16>;
     /// Returns true if the node exists.
@@ -31,10 +86,8 @@ pub trait TreeRef {
 
 /// A mutable view of a tree.
 pub trait TreeMut: TreeRef {
-    /// Removes the node and all of its children.
+    /// Removes the node and its children from the tree but do not delete the entities.
     fn remove(&mut self, id: NodeId);
-    /// Removes the node and all of its children.
-    fn remove_single(&mut self, id: NodeId);
     /// Adds a new node to the tree.
     fn create_node(&mut self, id: NodeId);
     /// Adds a child to the node.
@@ -45,6 +98,10 @@ pub trait TreeMut: TreeRef {
     fn insert_before(&mut self, old_id: NodeId, new_id: NodeId);
     /// Inserts a node after another node.
     fn insert_after(&mut self, old_id: NodeId, new_id: NodeId);
+    /// Creates a new shadow tree.
+    fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>);
+    /// Remove any shadow tree.
+    fn remove_subtree(&mut self, id: NodeId);
 }
 
 impl<'a> TreeRef for TreeRefView<'a> {
@@ -65,28 +122,47 @@ impl<'a> TreeRef for TreeRefView<'a> {
     fn contains(&self, id: NodeId) -> bool {
         self.get(id).is_ok()
     }
+
+    fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
+        self.get(id).ok()?.child_subtree.as_ref()
+    }
+
+    fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
+        self.get(id).ok()?.slot_for_light_tree
+    }
+
+    fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
+        self.get(id).ok()?.root_for_light_tree
+    }
 }
 
 impl<'a> TreeMut for TreeMutView<'a> {
     fn remove(&mut self, id: NodeId) {
         fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) {
-            let children = tree.children_ids(id);
+            let (light_tree, children) = {
+                let node = (&mut tree.1).get(id).unwrap();
+                (node.slot_for_light_tree, std::mem::take(&mut node.children))
+            };
+
             for child in children {
                 recurse(tree, child);
             }
-        }
-        {
-            let mut node_data_mut = &mut self.1;
-            if let Some(parent) = node_data_mut.get(id).unwrap().parent {
-                let parent = (&mut node_data_mut).get(parent).unwrap();
-                parent.children.retain(|&child| child != id);
+
+            // If this node is a slot in a shadow_tree, remove it from the shadow_tree.
+            if let Some(light_tree) = light_tree {
+                let root_for_light_tree = (&mut tree.1).get(light_tree).unwrap();
+
+                if let Some(shadow_tree) = &mut root_for_light_tree.child_subtree {
+                    shadow_tree.slot = None;
+                }
+
+                debug_assert!(
+                    root_for_light_tree.children.is_empty(),
+                    "ShadowTree root should have no children when slot is removed."
+                );
             }
         }
 
-        recurse(self, id);
-    }
-
-    fn remove_single(&mut self, id: NodeId) {
         {
             let mut node_data_mut = &mut self.1;
             if let Some(parent) = node_data_mut.get(id).unwrap().parent {
@@ -94,6 +170,8 @@ impl<'a> TreeMut for TreeMutView<'a> {
                 parent.children.retain(|&child| child != id);
             }
         }
+
+        recurse(self, id);
     }
 
     fn create_node(&mut self, id: NodeId) {
@@ -105,19 +183,21 @@ impl<'a> TreeMut for TreeMutView<'a> {
                 parent: None,
                 children: Vec::new(),
                 height: 0,
+                child_subtree: None,
+                slot_for_light_tree: None,
+                root_for_light_tree: None,
             },
         );
     }
 
     fn add_child(&mut self, parent: NodeId, new: NodeId) {
-        let height;
         {
             let mut node_state = &mut self.1;
             (&mut node_state).get(new).unwrap().parent = Some(parent);
             let parent = (&mut node_state).get(parent).unwrap();
             parent.children.push(new);
-            height = parent.height + 1;
         }
+        let height = child_height((&self.1).get(parent).unwrap(), self);
         set_height(self, new, height);
     }
 
@@ -133,27 +213,29 @@ impl<'a> TreeMut for TreeMutView<'a> {
                         break;
                     }
                 }
-                let height = parent.height + 1;
+                let height = child_height((&self.1).get(parent_id).unwrap(), self);
                 set_height(self, new_id, height);
             }
         }
-        // remove the old node
         self.remove(old_id);
     }
 
     fn insert_before(&mut self, old_id: NodeId, new_id: NodeId) {
-        let mut node_state = &mut self.1;
-        let old_node = node_state.get(old_id).unwrap();
-        let parent_id = old_node.parent.expect("tried to insert before root");
-        (&mut node_state).get(new_id).unwrap().parent = Some(parent_id);
-        let parent = (&mut node_state).get(parent_id).unwrap();
+        let parent_id = {
+            let old_node = self.1.get(old_id).unwrap();
+            old_node.parent.expect("tried to insert before root")
+        };
+        {
+            (&mut self.1).get(new_id).unwrap().parent = Some(parent_id);
+        }
+        let parent = (&mut self.1).get(parent_id).unwrap();
         let index = parent
             .children
             .iter()
             .position(|child| *child == old_id)
             .unwrap();
         parent.children.insert(index, new_id);
-        let height = parent.height + 1;
+        let height = child_height((&self.1).get(parent_id).unwrap(), self);
         set_height(self, new_id, height);
     }
 
@@ -169,21 +251,121 @@ impl<'a> TreeMut for TreeMutView<'a> {
             .position(|child| *child == old_id)
             .unwrap();
         parent.children.insert(index + 1, new_id);
-        let height = parent.height + 1;
+        let height = child_height((&self.1).get(parent_id).unwrap(), self);
         set_height(self, new_id, height);
     }
+
+    fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec<NodeId>, slot: Option<NodeId>) {
+        let (_, node_data_mut) = self;
+
+        let light_root_height;
+        {
+            let shadow_tree = ShadowTree {
+                shadow_roots: shadow_roots.clone(),
+                slot,
+            };
+
+            let light_root = node_data_mut
+                .get(id)
+                .expect("tried to create shadow_tree with non-existent id");
+
+            light_root.child_subtree = Some(shadow_tree);
+            light_root_height = light_root.height;
+
+            if let Some(slot) = slot {
+                let slot = node_data_mut
+                    .get(slot)
+                    .expect("tried to create shadow_tree with non-existent slot");
+                slot.slot_for_light_tree = Some(id);
+            }
+        }
+
+        // Now that we have created the shadow_tree, we need to update the height of the shadow_tree roots
+        for root in shadow_roots {
+            (&mut self.1).get(root).unwrap().root_for_light_tree = Some(id);
+            set_height(self, root, light_root_height + 1);
+        }
+    }
+
+    fn remove_subtree(&mut self, id: NodeId) {
+        let (_, node_data_mut) = self;
+
+        if let Ok(node) = node_data_mut.get(id) {
+            if let Some(shadow_tree) = node.child_subtree.take() {
+                // Remove the slot's link to the shadow_tree
+                if let Some(slot) = shadow_tree.slot {
+                    let slot = node_data_mut
+                        .get(slot)
+                        .expect("tried to remove shadow_tree with non-existent slot");
+                    slot.slot_for_light_tree = None;
+                }
+
+                let node = node_data_mut.get(id).unwrap();
+
+                // Reset the height of the light root's children
+                let height = node.height;
+                for child in node.children.clone() {
+                    println!("child: {:?}", child);
+                    set_height(self, child, height + 1);
+                }
+
+                // Reset the height of the shadow roots
+                for root in &shadow_tree.shadow_roots {
+                    set_height(self, *root, 0);
+                }
+            }
+        }
+    }
+}
+
+fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 {
+    match &parent.child_subtree {
+        Some(shadow_tree) => {
+            if let Some(slot) = shadow_tree.slot {
+                tree.height(slot)
+                    .expect("Attempted to read a slot that does not exist")
+                    + 1
+            } else {
+                panic!("Attempted to read the height of a child of a node with a shadow tree, but the shadow tree does not have a slot. Every shadow tree attached to a node with children must have a slot.")
+            }
+        }
+        None => parent.height + 1,
+    }
 }
 
 /// Sets the height of a node and updates the height of all its children
 fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
-    let children = {
+    let (shadow_tree, light_tree, children) = {
         let mut node_data_mut = &mut tree.1;
         let node = (&mut node_data_mut).get(node).unwrap();
         node.height = height;
-        node.children.clone()
+
+        (
+            node.child_subtree.clone(),
+            node.slot_for_light_tree,
+            node.children.clone(),
+        )
     };
-    for child in children {
-        set_height(tree, child, height + 1);
+
+    // If the children are actually part of a shadow_tree, there height is determined by the height of the shadow_tree
+    if let Some(shadow_tree) = shadow_tree {
+        // Set the height of the shadow_tree roots
+        for &shadow_root in &shadow_tree.shadow_roots {
+            set_height(tree, shadow_root, height + 1);
+        }
+    } else {
+        // Otherwise, we just set the height of the children to be one more than the height of the parent
+        for child in children {
+            set_height(tree, child, height + 1);
+        }
+    }
+
+    // If this nodes is a slot for a shadow_tree, we need to go to the super tree and update the height of its children
+    if let Some(light_tree) = light_tree {
+        let children = (&tree.1).get(light_tree).unwrap().children.clone();
+        for child in children {
+            set_height(tree, child, height + 1);
+        }
     }
 }
 
@@ -209,6 +391,21 @@ impl<'a> TreeRef for TreeMutView<'a> {
     fn contains(&self, id: NodeId) -> bool {
         self.1.get(id).is_ok()
     }
+
+    fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> {
+        let node_data = &self.1;
+        node_data.get(id).ok()?.child_subtree.as_ref()
+    }
+
+    fn slot_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
+        let node_data = &self.1;
+        node_data.get(id).ok()?.slot_for_light_tree
+    }
+
+    fn root_for_light_tree(&self, id: NodeId) -> Option<NodeId> {
+        let node_data = &self.1;
+        node_data.get(id).ok()?.root_for_light_tree
+    }
 }
 
 #[test]
@@ -235,6 +432,85 @@ fn creation() {
     assert_eq!(tree.children_ids(parent_id), &[child_id]);
 }
 
+#[test]
+fn shadow_tree() {
+    use shipyard::World;
+    #[derive(Component)]
+    struct Num(i32);
+
+    let mut world = World::new();
+    // Create main tree
+    let parent_id = world.add_entity(Num(1i32));
+    let child_id = world.add_entity(Num(0i32));
+
+    // Create shadow tree
+    let shadow_parent_id = world.add_entity(Num(2i32));
+    let shadow_child_id = world.add_entity(Num(3i32));
+
+    let mut tree = world.borrow::<TreeMutView>().unwrap();
+
+    tree.create_node(parent_id);
+    tree.create_node(child_id);
+
+    tree.add_child(parent_id, child_id);
+
+    tree.create_node(shadow_parent_id);
+    tree.create_node(shadow_child_id);
+
+    tree.add_child(shadow_parent_id, shadow_child_id);
+
+    // Check that both trees are correct individually
+    assert_eq!(tree.height(parent_id), Some(0));
+    assert_eq!(tree.height(child_id), Some(1));
+    assert_eq!(tree.parent_id(parent_id), None);
+    assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
+    assert_eq!(tree.children_ids(parent_id), &[child_id]);
+
+    assert_eq!(tree.height(shadow_parent_id), Some(0));
+    assert_eq!(tree.height(shadow_child_id), Some(1));
+    assert_eq!(tree.parent_id(shadow_parent_id), None);
+    assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
+    assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
+
+    // Add shadow tree to main tree
+    tree.create_subtree(parent_id, vec![shadow_parent_id], Some(shadow_child_id));
+
+    assert_eq!(tree.height(parent_id), Some(0));
+    assert_eq!(tree.height(shadow_parent_id), Some(1));
+    assert_eq!(tree.height(shadow_child_id), Some(2));
+    assert_eq!(tree.height(child_id), Some(3));
+    assert_eq!(
+        tree.1
+            .get(parent_id)
+            .unwrap()
+            .child_subtree
+            .as_ref()
+            .unwrap()
+            .shadow_roots,
+        &[shadow_parent_id]
+    );
+    assert_eq!(
+        tree.1.get(shadow_child_id).unwrap().slot_for_light_tree,
+        Some(parent_id)
+    );
+
+    // Remove shadow tree from main tree
+    tree.remove_subtree(parent_id);
+
+    // Check that both trees are correct individually
+    assert_eq!(tree.height(parent_id), Some(0));
+    assert_eq!(tree.height(child_id), Some(1));
+    assert_eq!(tree.parent_id(parent_id), None);
+    assert_eq!(tree.parent_id(child_id).unwrap(), parent_id);
+    assert_eq!(tree.children_ids(parent_id), &[child_id]);
+
+    assert_eq!(tree.height(shadow_parent_id), Some(0));
+    assert_eq!(tree.height(shadow_child_id), Some(1));
+    assert_eq!(tree.parent_id(shadow_parent_id), None);
+    assert_eq!(tree.parent_id(shadow_child_id).unwrap(), shadow_parent_id);
+    assert_eq!(tree.children_ids(shadow_parent_id), &[shadow_child_id]);
+}
+
 #[test]
 fn insertion() {
     use shipyard::World;

+ 2 - 2
packages/native-core/src/utils/persistant_iterator.rs

@@ -60,7 +60,7 @@ struct PersistantElementIterUpdater<V> {
 }
 
 impl<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpdater<V> {
-    fn on_node_moved(&self, node: NodeMut<V>) {
+    fn on_node_moved(&mut self, node: NodeMut<V>) {
         // if any element is moved, update its parents in the stack
         let mut stack = self.stack.lock().unwrap();
         let moved = node.id();
@@ -78,7 +78,7 @@ impl<V: FromAnyValue + Sync + Send> NodeWatcher<V> for PersistantElementIterUpda
         }
     }
 
-    fn on_node_removed(&self, node: NodeMut<V>) {
+    fn on_node_removed(&mut self, node: NodeMut<V>) {
         // if any element is removed in the chain, remove it and its children from the stack
         let mut stack = self.stack.lock().unwrap();
         let removed = node.id();

+ 1 - 1
packages/native-core/tests/called_minimally_on_build.rs

@@ -146,7 +146,7 @@ macro_rules! test_state{
             dioxus_state.apply_mutations(&mut dom, mutations);
             dom.update_state(SendAnyMap::new());
 
-            dom.traverse_depth_first(|n| {
+            dom.traverse_depth_first_advanced(false, |n| {
                 $(
                     assert_eq!(n.get::<$state>().unwrap().0, 1);
                 )*

+ 389 - 0
packages/native-core/tests/custom_element.rs

@@ -0,0 +1,389 @@
+use dioxus::prelude::*;
+use dioxus_native_core::{custom_element::CustomElement, prelude::*};
+use dioxus_native_core_macro::partial_derive_state;
+use shipyard::Component;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
+pub struct ColorState {
+    color: usize,
+}
+
+#[partial_derive_state]
+impl State for ColorState {
+    type ParentDependencies = (Self,);
+    type ChildDependencies = ();
+    type NodeDependencies = ();
+
+    // The color state should not be effected by the shadow dom
+    const TRAVERSE_SHADOW_DOM: bool = false;
+
+    const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
+        .with_attrs(AttributeMaskBuilder::Some(&["color"]))
+        .with_element();
+
+    fn update<'a>(
+        &mut self,
+        view: NodeView,
+        _: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        _: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
+        _: &SendAnyMap,
+    ) -> bool {
+        if let Some(size) = view
+            .attributes()
+            .into_iter()
+            .flatten()
+            .find(|attr| attr.attribute.name == "color")
+        {
+            self.color = size
+                .value
+                .as_float()
+                .or_else(|| size.value.as_int().map(|i| i as f64))
+                .or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
+                .unwrap_or(0.0) as usize;
+        } else if let Some((parent,)) = parent {
+            *self = *parent;
+        }
+        true
+    }
+
+    fn create<'a>(
+        node_view: NodeView<()>,
+        node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
+        context: &SendAnyMap,
+    ) -> Self {
+        let mut myself = Self::default();
+        myself.update(node_view, node, parent, children, context);
+        myself
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)]
+pub struct LayoutState {
+    size: usize,
+}
+
+#[partial_derive_state]
+impl State for LayoutState {
+    type ParentDependencies = (Self,);
+    type ChildDependencies = ();
+    type NodeDependencies = ();
+
+    // The layout state should be effected by the shadow dom
+    const TRAVERSE_SHADOW_DOM: bool = true;
+
+    const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
+        .with_attrs(AttributeMaskBuilder::Some(&["size"]))
+        .with_element();
+
+    fn update<'a>(
+        &mut self,
+        view: NodeView,
+        _: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        _: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
+        _: &SendAnyMap,
+    ) -> bool {
+        if let Some(size) = view
+            .attributes()
+            .into_iter()
+            .flatten()
+            .find(|attr| attr.attribute.name == "size")
+        {
+            self.size = size
+                .value
+                .as_float()
+                .or_else(|| size.value.as_int().map(|i| i as f64))
+                .or_else(|| size.value.as_text().and_then(|i| i.parse().ok()))
+                .unwrap_or(0.0) as usize;
+        } else if let Some((parent,)) = parent {
+            if parent.size > 0 {
+                self.size = parent.size - 1;
+            }
+        }
+        true
+    }
+
+    fn create<'a>(
+        node_view: NodeView<()>,
+        node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
+        context: &SendAnyMap,
+    ) -> Self {
+        let mut myself = Self::default();
+        myself.update(node_view, node, parent, children, context);
+        myself
+    }
+}
+
+mod dioxus_elements {
+    macro_rules! builder_constructors {
+        (
+            $(
+                $(#[$attr:meta])*
+                $name:ident {
+                    $(
+                        $(#[$attr_method:meta])*
+                        $fil:ident: $vil:ident,
+                    )*
+                };
+            )*
+        ) => {
+            $(
+                #[allow(non_camel_case_types)]
+                $(#[$attr])*
+                pub struct $name;
+
+                #[allow(non_upper_case_globals, unused)]
+                impl $name {
+                    pub const TAG_NAME: &'static str = stringify!($name);
+                    pub const NAME_SPACE: Option<&'static str> = None;
+
+                    $(
+                        pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false);
+                    )*
+                }
+
+                impl GlobalAttributes for $name {}
+            )*
+        }
+    }
+
+    pub trait GlobalAttributes {}
+
+    pub trait SvgAttributes {}
+
+    builder_constructors! {
+        customelementslot {
+            size: attr,
+            color: attr,
+        };
+        customelementnoslot {
+            size: attr,
+            color: attr,
+        };
+        testing132 {
+            color: attr,
+        };
+    }
+}
+
+struct CustomElementWithSlot {
+    root: NodeId,
+    slot: NodeId,
+}
+
+impl CustomElement for CustomElementWithSlot {
+    const NAME: &'static str = "customelementslot";
+
+    fn create(mut node: NodeMut<()>) -> Self {
+        let dom = node.real_dom_mut();
+        let child = dom.create_node(ElementNode {
+            tag: "div".into(),
+            namespace: None,
+            attributes: Default::default(),
+            listeners: Default::default(),
+        });
+        let slot_id = child.id();
+        let mut root = dom.create_node(ElementNode {
+            tag: "div".into(),
+            namespace: None,
+            attributes: Default::default(),
+            listeners: Default::default(),
+        });
+        root.add_child(slot_id);
+
+        Self {
+            root: root.id(),
+            slot: slot_id,
+        }
+    }
+
+    fn slot(&self) -> Option<NodeId> {
+        Some(self.slot)
+    }
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.root]
+    }
+
+    fn attributes_changed(
+        &mut self,
+        node: NodeMut<()>,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        println!("attributes_changed");
+        println!("{:?}", attributes);
+        println!("{:?}: {:#?}", node.id(), &*node.node_type());
+    }
+}
+
+struct CustomElementWithNoSlot {
+    root: NodeId,
+}
+
+impl CustomElement for CustomElementWithNoSlot {
+    const NAME: &'static str = "customelementnoslot";
+
+    fn create(mut node: NodeMut<()>) -> Self {
+        let dom = node.real_dom_mut();
+        let root = dom.create_node(ElementNode {
+            tag: "div".into(),
+            namespace: None,
+            attributes: Default::default(),
+            listeners: Default::default(),
+        });
+        Self { root: root.id() }
+    }
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.root]
+    }
+
+    fn attributes_changed(
+        &mut self,
+        node: NodeMut<()>,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        println!("attributes_changed");
+        println!("{:?}", attributes);
+        println!("{:?}: {:#?}", node.id(), &*node.node_type());
+    }
+}
+
+#[test]
+fn custom_elements_work() {
+    fn app(cx: Scope) -> Element {
+        let count = use_state(cx, || 0);
+
+        use_future!(cx, |count| async move {
+            count.with_mut(|count| *count += 1);
+        });
+
+        cx.render(rsx! {
+            customelementslot {
+                size: "{count}",
+                color: "1",
+                customelementslot {
+                    testing132 {}
+                }
+            }
+        })
+    }
+
+    let rt = tokio::runtime::Builder::new_current_thread()
+        .enable_time()
+        .build()
+        .unwrap();
+
+    rt.block_on(async {
+        let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
+        rdom.register_custom_element::<CustomElementWithSlot>();
+        let mut dioxus_state = DioxusState::create(&mut rdom);
+        let mut dom = VirtualDom::new(app);
+
+        let mutations = dom.rebuild();
+        dioxus_state.apply_mutations(&mut rdom, mutations);
+
+        let ctx = SendAnyMap::new();
+        rdom.update_state(ctx);
+
+        for i in 0..10usize {
+            dom.wait_for_work().await;
+
+            let mutations = dom.render_immediate();
+            dioxus_state.apply_mutations(&mut rdom, mutations);
+
+            let ctx = SendAnyMap::new();
+            rdom.update_state(ctx);
+
+            // render...
+            rdom.traverse_depth_first_advanced(true, |node| {
+                let node_type = &*node.node_type();
+                let height = node.height() as usize;
+                let indent = " ".repeat(height);
+                let color = *node.get::<ColorState>().unwrap();
+                let size = *node.get::<LayoutState>().unwrap();
+                let id = node.id();
+                println!("{indent}{id:?} {color:?} {size:?} {node_type:?}");
+                if let NodeType::Element(el) = node_type {
+                    match el.tag.as_str() {
+                        // the color should bubble up from customelementslot
+                        "testing132" | "customelementslot" => {
+                            assert_eq!(color.color, 1);
+                        }
+                        // the color of the light dom should not effect the color of the shadow dom, so the color of divs in the shadow dom should be 0
+                        "div" => {
+                            assert_eq!(color.color, 0);
+                        }
+                        _ => {}
+                    }
+                    if el.tag != "Root" {
+                        assert_eq!(size.size, (i + 2).saturating_sub(height));
+                    }
+                }
+            });
+        }
+    });
+}
+
+#[test]
+#[should_panic]
+fn slotless_custom_element_cant_have_children() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            customelementnoslot {
+                testing132 {}
+            }
+        })
+    }
+
+    let rt = tokio::runtime::Builder::new_current_thread()
+        .enable_time()
+        .build()
+        .unwrap();
+
+    rt.block_on(async {
+        let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
+        rdom.register_custom_element::<CustomElementWithNoSlot>();
+        let mut dioxus_state = DioxusState::create(&mut rdom);
+        let mut dom = VirtualDom::new(app);
+
+        let mutations = dom.rebuild();
+        dioxus_state.apply_mutations(&mut rdom, mutations);
+
+        let ctx = SendAnyMap::new();
+        rdom.update_state(ctx);
+    });
+}
+
+#[test]
+fn slotless_custom_element() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            customelementnoslot {
+            }
+        })
+    }
+
+    let rt = tokio::runtime::Builder::new_current_thread()
+        .enable_time()
+        .build()
+        .unwrap();
+
+    rt.block_on(async {
+        let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
+        rdom.register_custom_element::<CustomElementWithNoSlot>();
+        let mut dioxus_state = DioxusState::create(&mut rdom);
+        let mut dom = VirtualDom::new(app);
+
+        let mutations = dom.rebuild();
+        dioxus_state.apply_mutations(&mut rdom, mutations);
+
+        let ctx = SendAnyMap::new();
+        rdom.update_state(ctx);
+    });
+}

+ 1 - 2
packages/rink/examples/counter_button.rs

@@ -1,11 +1,10 @@
-use dioxus_html::EventData;
 use dioxus_native_core::{
     node::TextNode,
     prelude::*,
     real_dom::{NodeImmutable, NodeTypeMut},
     NodeId,
 };
-use rink::{render, Config, Driver};
+use rink::{render, Config, Driver, EventData};
 use std::rc::Rc;
 use std::sync::{Arc, RwLock};
 

+ 1 - 2
packages/rink/examples/grid.rs

@@ -1,11 +1,10 @@
-use dioxus_html::EventData;
 use dioxus_native_core::{
     node::TextNode,
     prelude::*,
     real_dom::{NodeImmutable, NodeTypeMut},
     NodeId,
 };
-use rink::{render, Config, Driver};
+use rink::{render, Config, Driver, EventData};
 use rustc_hash::FxHashSet;
 use std::rc::Rc;
 use std::sync::{Arc, RwLock};

+ 101 - 0
packages/rink/examples/widgets.rs

@@ -0,0 +1,101 @@
+use dioxus_native_core::{
+    prelude::*,
+    real_dom::{NodeImmutable, NodeTypeMut},
+    NodeId,
+};
+use rink::{render, Config, Driver, EventData};
+use std::rc::Rc;
+use std::sync::{Arc, RwLock};
+
+#[derive(Default)]
+struct Counter {
+    count: f64,
+    button_id: NodeId,
+}
+
+impl Counter {
+    fn create(mut root: NodeMut) -> Self {
+        let mut myself = Self::default();
+
+        let root_id = root.id();
+        let rdom = root.real_dom_mut();
+
+        // create the counter
+        let count = myself.count;
+        let mut button = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "input".to_string(),
+            attributes: [
+                // supported types: button, checkbox, textbox, password, number, range
+                ("type".to_string().into(), "range".to_string().into()),
+                ("display".to_string().into(), "flex".to_string().into()),
+                (("flex-direction", "style").into(), "row".to_string().into()),
+                (
+                    ("justify-content", "style").into(),
+                    "center".to_string().into(),
+                ),
+                (("align-items", "style").into(), "center".to_string().into()),
+                (
+                    "value".to_string().into(),
+                    format!("click me {count}").into(),
+                ),
+                (("width", "style").into(), "50%".to_string().into()),
+                (("height", "style").into(), "10%".to_string().into()),
+                ("min".to_string().into(), "20".to_string().into()),
+                ("max".to_string().into(), "80".to_string().into()),
+            ]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        button.add_event_listener("input");
+        myself.button_id = button.id();
+        rdom.get_mut(root_id).unwrap().add_child(myself.button_id);
+
+        myself
+    }
+}
+
+impl Driver for Counter {
+    fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
+        // update the counter
+        let mut rdom = rdom.write().unwrap();
+        let mut node = rdom.get_mut(self.button_id).unwrap();
+        if let NodeTypeMut::Element(mut el) = node.node_type_mut() {
+            el.set_attribute(
+                ("background-color", "style"),
+                format!("rgb({}, {}, {})", 255.0 - self.count * 2.0, 0, 0,),
+            );
+        };
+    }
+
+    fn handle_event(
+        &mut self,
+        _: &Arc<RwLock<RealDom>>,
+        _: NodeId,
+        event_type: &str,
+        event: Rc<EventData>,
+        _: bool,
+    ) {
+        if event_type == "input" {
+            // when the button is clicked, increment the counter
+            if let EventData::Form(input_event) = &*event {
+                if let Ok(value) = input_event.value.parse::<f64>() {
+                    self.count = value;
+                }
+            }
+        }
+    }
+
+    fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
+        Box::pin(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await })
+    }
+}
+
+fn main() {
+    render(Config::new(), |rdom, _, _| {
+        let mut rdom = rdom.write().unwrap();
+        let root = rdom.root_id();
+        Counter::create(rdom.get_mut(root).unwrap())
+    })
+    .unwrap();
+}

+ 82 - 73
packages/rink/src/hooks.rs

@@ -13,7 +13,9 @@ use dioxus_html::geometry::{
 use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
 use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
 use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
-use dioxus_html::{event_bubbles, EventData, FocusData, KeyboardData, MouseData, WheelData};
+use dioxus_html::{event_bubbles, FocusData, KeyboardData, MouseData, WheelData};
+use std::any::Any;
+use std::collections::HashMap;
 use std::{
     cell::{RefCell, RefMut},
     rc::Rc,
@@ -24,15 +26,64 @@ use taffy::{prelude::Layout, Taffy};
 
 use crate::focus::{Focus, Focused};
 use crate::layout::TaffyLayout;
-use crate::{layout_to_screen_space, FocusState};
+use crate::{get_abs_layout, layout_to_screen_space, FocusState};
 
-pub(crate) struct Event {
+#[derive(Debug, Clone, PartialEq)]
+pub struct Event {
     pub id: NodeId,
     pub name: &'static str,
-    pub data: Rc<EventData>,
+    pub data: EventData,
     pub bubbles: bool,
 }
 
+#[derive(Debug, Clone, PartialEq)]
+pub enum EventData {
+    Mouse(MouseData),
+    Keyboard(KeyboardData),
+    Focus(FocusData),
+    Wheel(WheelData),
+    Form(FormData),
+}
+
+impl EventData {
+    pub fn into_any(self) -> Rc<dyn Any> {
+        match self {
+            EventData::Mouse(m) => Rc::new(m),
+            EventData::Keyboard(k) => Rc::new(k),
+            EventData::Focus(f) => Rc::new(f),
+            EventData::Wheel(w) => Rc::new(w),
+            EventData::Form(f) => Rc::new(f.into_html()),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct FormData {
+    pub value: String,
+
+    pub values: HashMap<String, Vec<String>>,
+
+    pub files: Option<Files>,
+}
+
+impl FormData {
+    fn into_html(self) -> dioxus_html::FormData {
+        dioxus_html::FormData {
+            value: self.value,
+            values: self.values,
+            files: None,
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Files {
+    files: FxHashMap<String, File>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct File {}
+
 type EventCore = (&'static str, EventData);
 
 const MAX_REPEAT_TIME: Duration = Duration::from_millis(100);
@@ -148,13 +199,13 @@ impl InnerInputState {
                 resolved_events.push(Event {
                     name: "focus",
                     id,
-                    data: Rc::new(EventData::Focus(FocusData {})),
+                    data: EventData::Focus(FocusData {}),
                     bubbles: event_bubbles("focus"),
                 });
                 resolved_events.push(Event {
                     name: "focusin",
                     id,
-                    data: Rc::new(EventData::Focus(FocusData {})),
+                    data: EventData::Focus(FocusData {}),
                     bubbles: event_bubbles("focusin"),
                 });
             }
@@ -162,7 +213,7 @@ impl InnerInputState {
                 resolved_events.push(Event {
                     name: "focusout",
                     id,
-                    data: Rc::new(EventData::Focus(FocusData {})),
+                    data: EventData::Focus(FocusData {}),
                     bubbles: event_bubbles("focusout"),
                 });
             }
@@ -198,7 +249,7 @@ impl InnerInputState {
 
         fn try_create_event(
             name: &'static str,
-            data: Rc<EventData>,
+            data: EventData,
             will_bubble: &mut FxHashSet<NodeId>,
             resolved_events: &mut Vec<Event>,
             node: NodeRef,
@@ -273,7 +324,7 @@ impl InnerInputState {
                 if old_pos != Some(new_pos) {
                     let mut will_bubble = FxHashSet::default();
                     for node in dom.get_listening_sorted("mousemove") {
-                        let node_layout = get_abs_layout(node, dom, layout);
+                        let node_layout = get_abs_layout(node, layout);
                         let previously_contained = old_pos
                             .filter(|pos| layout_contains_point(&node_layout, *pos))
                             .is_some();
@@ -282,10 +333,7 @@ impl InnerInputState {
                         if currently_contains && previously_contained {
                             try_create_event(
                                 "mousemove",
-                                Rc::new(EventData::Mouse(prepare_mouse_data(
-                                    mouse_data,
-                                    &node_layout,
-                                ))),
+                                EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                                 &mut will_bubble,
                                 resolved_events,
                                 node,
@@ -300,7 +348,7 @@ impl InnerInputState {
                 // mouseenter
                 let mut will_bubble = FxHashSet::default();
                 for node in dom.get_listening_sorted("mouseenter") {
-                    let node_layout = get_abs_layout(node, dom, layout);
+                    let node_layout = get_abs_layout(node, layout);
                     let previously_contained = old_pos
                         .filter(|pos| layout_contains_point(&node_layout, *pos))
                         .is_some();
@@ -309,7 +357,7 @@ impl InnerInputState {
                     if currently_contains && !previously_contained {
                         try_create_event(
                             "mouseenter",
-                            Rc::new(dioxus_html::EventData::Mouse(mouse_data.clone())),
+                            EventData::Mouse(mouse_data.clone()),
                             &mut will_bubble,
                             resolved_events,
                             node,
@@ -323,7 +371,7 @@ impl InnerInputState {
                 // mouseover
                 let mut will_bubble = FxHashSet::default();
                 for node in dom.get_listening_sorted("mouseover") {
-                    let node_layout = get_abs_layout(node, dom, layout);
+                    let node_layout = get_abs_layout(node, layout);
                     let previously_contained = old_pos
                         .filter(|pos| layout_contains_point(&node_layout, *pos))
                         .is_some();
@@ -332,10 +380,7 @@ impl InnerInputState {
                     if currently_contains && !previously_contained {
                         try_create_event(
                             "mouseover",
-                            Rc::new(EventData::Mouse(prepare_mouse_data(
-                                mouse_data,
-                                &node_layout,
-                            ))),
+                            EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                             &mut will_bubble,
                             resolved_events,
                             node,
@@ -349,16 +394,13 @@ impl InnerInputState {
             if was_pressed {
                 let mut will_bubble = FxHashSet::default();
                 for node in dom.get_listening_sorted("mousedown") {
-                    let node_layout = get_abs_layout(node, dom, layout);
+                    let node_layout = get_abs_layout(node, layout);
                     let currently_contains = layout_contains_point(&node_layout, new_pos);
 
                     if currently_contains {
                         try_create_event(
                             "mousedown",
-                            Rc::new(EventData::Mouse(prepare_mouse_data(
-                                mouse_data,
-                                &node_layout,
-                            ))),
+                            EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                             &mut will_bubble,
                             resolved_events,
                             node,
@@ -373,16 +415,13 @@ impl InnerInputState {
                 if was_released {
                     let mut will_bubble = FxHashSet::default();
                     for node in dom.get_listening_sorted("mouseup") {
-                        let node_layout = get_abs_layout(node, dom, layout);
+                        let node_layout = get_abs_layout(node, layout);
                         let currently_contains = layout_contains_point(&node_layout, new_pos);
 
                         if currently_contains {
                             try_create_event(
                                 "mouseup",
-                                Rc::new(EventData::Mouse(prepare_mouse_data(
-                                    mouse_data,
-                                    &node_layout,
-                                ))),
+                                EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                                 &mut will_bubble,
                                 resolved_events,
                                 node,
@@ -398,16 +437,13 @@ impl InnerInputState {
                 if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released {
                     let mut will_bubble = FxHashSet::default();
                     for node in dom.get_listening_sorted("click") {
-                        let node_layout = get_abs_layout(node, dom, layout);
+                        let node_layout = get_abs_layout(node, layout);
                         let currently_contains = layout_contains_point(&node_layout, new_pos);
 
                         if currently_contains {
                             try_create_event(
                                 "click",
-                                Rc::new(EventData::Mouse(prepare_mouse_data(
-                                    mouse_data,
-                                    &node_layout,
-                                ))),
+                                EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                                 &mut will_bubble,
                                 resolved_events,
                                 node,
@@ -424,16 +460,13 @@ impl InnerInputState {
                 {
                     let mut will_bubble = FxHashSet::default();
                     for node in dom.get_listening_sorted("contextmenu") {
-                        let node_layout = get_abs_layout(node, dom, layout);
+                        let node_layout = get_abs_layout(node, layout);
                         let currently_contains = layout_contains_point(&node_layout, new_pos);
 
                         if currently_contains {
                             try_create_event(
                                 "contextmenu",
-                                Rc::new(EventData::Mouse(prepare_mouse_data(
-                                    mouse_data,
-                                    &node_layout,
-                                ))),
+                                EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                                 &mut will_bubble,
                                 resolved_events,
                                 node,
@@ -450,14 +483,14 @@ impl InnerInputState {
                     if was_scrolled {
                         let mut will_bubble = FxHashSet::default();
                         for node in dom.get_listening_sorted("wheel") {
-                            let node_layout = get_abs_layout(node, dom, layout);
+                            let node_layout = get_abs_layout(node, layout);
 
                             let currently_contains = layout_contains_point(&node_layout, new_pos);
 
                             if currently_contains {
                                 try_create_event(
                                     "wheel",
-                                    Rc::new(EventData::Wheel(w.clone())),
+                                    EventData::Wheel(w.clone()),
                                     &mut will_bubble,
                                     resolved_events,
                                     node,
@@ -473,7 +506,7 @@ impl InnerInputState {
                 // mouseleave
                 let mut will_bubble = FxHashSet::default();
                 for node in dom.get_listening_sorted("mouseleave") {
-                    let node_layout = get_abs_layout(node, dom, layout);
+                    let node_layout = get_abs_layout(node, layout);
                     let previously_contained = old_pos
                         .filter(|pos| layout_contains_point(&node_layout, *pos))
                         .is_some();
@@ -482,10 +515,7 @@ impl InnerInputState {
                     if !currently_contains && previously_contained {
                         try_create_event(
                             "mouseleave",
-                            Rc::new(EventData::Mouse(prepare_mouse_data(
-                                mouse_data,
-                                &node_layout,
-                            ))),
+                            EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                             &mut will_bubble,
                             resolved_events,
                             node,
@@ -499,7 +529,7 @@ impl InnerInputState {
                 // mouseout
                 let mut will_bubble = FxHashSet::default();
                 for node in dom.get_listening_sorted("mouseout") {
-                    let node_layout = get_abs_layout(node, dom, layout);
+                    let node_layout = get_abs_layout(node, layout);
                     let previously_contained = old_pos
                         .filter(|pos| layout_contains_point(&node_layout, *pos))
                         .is_some();
@@ -508,10 +538,7 @@ impl InnerInputState {
                     if !currently_contains && previously_contained {
                         try_create_event(
                             "mouseout",
-                            Rc::new(EventData::Mouse(prepare_mouse_data(
-                                mouse_data,
-                                &node_layout,
-                            ))),
+                            EventData::Mouse(prepare_mouse_data(mouse_data, &node_layout)),
                             &mut will_bubble,
                             resolved_events,
                             node,
@@ -546,24 +573,6 @@ impl InnerInputState {
     // }
 }
 
-fn get_abs_layout(node: NodeRef, dom: &RealDom, taffy: &Taffy) -> Layout {
-    let mut node_layout = *taffy
-        .layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
-        .unwrap();
-    let mut current = node;
-
-    while let Some(parent) = current.parent_id() {
-        let parent = dom.get(parent).unwrap();
-        current = parent;
-        let parent_layout = taffy
-            .layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
-            .unwrap();
-        node_layout.location.x += parent_layout.location.x;
-        node_layout.location.y += parent_layout.location.y;
-    }
-    node_layout
-}
-
 pub struct RinkInputHandler {
     state: Rc<RefCell<InnerInputState>>,
     queued_events: Rc<RefCell<Vec<EventCore>>>,
@@ -628,12 +637,12 @@ impl RinkInputHandler {
             })
             .map(|evt| (evt.0, evt.1));
 
-        let mut hm: FxHashMap<&'static str, Vec<Rc<EventData>>> = FxHashMap::default();
+        let mut hm: FxHashMap<&'static str, Vec<EventData>> = FxHashMap::default();
         for (event, data) in events {
             if let Some(v) = hm.get_mut(event) {
-                v.push(Rc::new(data));
+                v.push(data);
             } else {
-                hm.insert(event, vec![Rc::new(data)]);
+                hm.insert(event, vec![data]);
             }
         }
         for (event, datas) in hm {

+ 3 - 6
packages/rink/src/layout.rs

@@ -25,12 +25,6 @@ impl<T> PossiblyUninitalized<T> {
             _ => panic!("uninitalized"),
         }
     }
-    pub fn ok(self) -> Option<T> {
-        match self {
-            Self::Initialized(i) => Some(i),
-            _ => None,
-        }
-    }
 }
 impl<T> Default for PossiblyUninitalized<T> {
     fn default() -> Self {
@@ -54,6 +48,9 @@ impl State for TaffyLayout {
         .with_attrs(AttributeMaskBuilder::Some(SORTED_LAYOUT_ATTRS))
         .with_text();
 
+    // The layout state should be effected by the shadow dom
+    const TRAVERSE_SHADOW_DOM: bool = true;
+
     fn update<'a>(
         &mut self,
         node_view: NodeView,

+ 98 - 19
packages/rink/src/lib.rs

@@ -6,8 +6,7 @@ use crossterm::{
     execute,
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
 };
-use dioxus_html::EventData;
-use dioxus_native_core::prelude::*;
+use dioxus_native_core::{prelude::*, tree::TreeRef};
 use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
 use focus::FocusState;
 use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
@@ -21,22 +20,22 @@ use std::{
 };
 use std::{rc::Rc, sync::RwLock};
 use style_attributes::StyleModifier;
-use taffy::Taffy;
 pub use taffy::{geometry::Point, prelude::*};
 use tokio::select;
-use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
+use tui::{backend::CrosstermBackend, Terminal};
+use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
 
 mod config;
 mod focus;
 mod hooks;
 mod layout;
-pub mod prelude;
 mod prevent_default;
 pub mod query;
 mod render;
 mod style;
 mod style_attributes;
 mod widget;
+mod widgets;
 
 pub use config::*;
 pub use hooks::*;
@@ -91,11 +90,14 @@ pub fn render<R: Driver>(
         PreventDefault::to_type_erased(),
     ]);
 
-    let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
-
     // Setup input handling
+
+    // The event channel for fully resolved events
     let (event_tx, mut event_reciever) = unbounded();
-    let event_tx_clone = event_tx.clone();
+
+    // The event channel for raw terminal events
+    let (raw_event_tx, mut raw_event_reciever) = unbounded();
+    let event_tx_clone = raw_event_tx.clone();
     if !cfg.headless {
         std::thread::spawn(move || {
             // Timeout after 10ms when waiting for events
@@ -103,7 +105,10 @@ pub fn render<R: Driver>(
             loop {
                 if crossterm::event::poll(tick_rate).unwrap() {
                     let evt = crossterm::event::read().unwrap();
-                    if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() {
+                    if raw_event_tx
+                        .unbounded_send(InputEvent::UserInput(evt))
+                        .is_err()
+                    {
                         break;
                     }
                 }
@@ -111,10 +116,21 @@ pub fn render<R: Driver>(
         });
     }
 
+    register_widgets(&mut rdom, event_tx);
+
+    let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
+
     let rdom = Arc::new(RwLock::new(rdom));
     let taffy = Arc::new(Mutex::new(Taffy::new()));
     let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
 
+    // insert the query engine into the rdom
+    let query_engine = Query::new(rdom.clone(), taffy.clone());
+    {
+        let mut rdom = rdom.write().unwrap();
+        rdom.raw_world_mut().add_unique(query_engine);
+    }
+
     {
         renderer.update(&rdom);
         let mut any_map = SendAnyMap::new();
@@ -160,7 +176,7 @@ pub fn render<R: Driver>(
 
                 if !to_rerender.is_empty() || updated {
                     updated = false;
-                    fn resize(dims: Rect, taffy: &mut Taffy, rdom: &RealDom) {
+                    fn resize(dims: tui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
                         let width = screen_to_layout_space(dims.width);
                         let height = screen_to_layout_space(dims.height);
                         let root_node = rdom
@@ -202,7 +218,7 @@ pub fn render<R: Driver>(
                     } else {
                         let rdom = rdom.read().unwrap();
                         resize(
-                            Rect {
+                            tui::layout::Rect {
                                 x: 0,
                                 y: 0,
                                 width: 1000,
@@ -214,6 +230,7 @@ pub fn render<R: Driver>(
                     }
                 }
 
+                let mut event_recieved = None;
                 {
                     let wait = renderer.poll_async();
 
@@ -223,7 +240,7 @@ pub fn render<R: Driver>(
                         _ = wait => {
 
                         },
-                        evt = event_reciever.next() => {
+                        evt = raw_event_reciever.next() => {
                             match evt.as_ref().unwrap() {
                                 InputEvent::UserInput(event) => match event {
                                     TermEvent::Key(key) => {
@@ -244,21 +261,32 @@ pub fn render<R: Driver>(
                                 register_event(evt);
                             }
                         },
+                        Some(evt) = event_reciever.next() => {
+                            event_recieved = Some(evt);
+                        }
                     }
                 }
 
                 {
+                    if let Some(evt) = event_recieved {
+                        renderer.handle_event(
+                            &rdom,
+                            evt.id,
+                            evt.name,
+                            Rc::new(evt.data),
+                            evt.bubbles,
+                        );
+                    }
                     {
-                        let evts = {
-                            handler.get_events(
-                                &taffy.lock().expect("taffy lock poisoned"),
-                                &mut rdom.write().unwrap(),
-                            )
-                        };
+                        let evts = handler.get_events(
+                            &taffy.lock().expect("taffy lock poisoned"),
+                            &mut rdom.write().unwrap(),
+                        );
                         updated |= handler.state().focus_state.clean();
 
                         for e in evts {
-                            renderer.handle_event(&rdom, e.id, e.name, e.data, e.bubbles);
+                            bubble_event_to_widgets(&mut rdom.write().unwrap(), &e);
+                            renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles);
                         }
                     }
                     // updates the dom's nodes
@@ -310,3 +338,54 @@ pub trait Driver {
     );
     fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
 }
+
+/// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening
+fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) {
+    let id = event.id;
+    let mut node = Some(id);
+
+    while let Some(node_id) = node {
+        let parent_id = {
+            let tree = rdom.tree_ref();
+            tree.parent_id_advanced(node_id, true)
+        };
+
+        {
+            // println!("@ bubbling event to node {:?}", node_id);
+            let mut node_mut = rdom.get_mut(node_id).unwrap();
+            if let Some(mut widget) = node_mut
+                .get_mut::<RinkWidgetTraitObject>()
+                .map(|w| w.clone())
+            {
+                widget.handle_event(event, node_mut)
+            }
+        }
+
+        if !event.bubbles {
+            // println!("event does not bubble");
+            break;
+        }
+        node = parent_id;
+    }
+}
+
+pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout {
+    let mut node_layout = *taffy
+        .layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
+        .unwrap();
+    let mut current = node;
+
+    let dom = node.real_dom();
+    let tree = dom.tree_ref();
+
+    while let Some(parent) = tree.parent_id_advanced(current.id(), true) {
+        let parent = dom.get(parent).unwrap();
+        current = parent;
+        let parent_layout = taffy
+            .layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
+            .unwrap();
+        node_layout.location.x += parent_layout.location.x;
+        node_layout.location.y += parent_layout.location.y;
+    }
+    node_layout
+}

+ 0 - 2
packages/rink/src/prelude/mod.rs

@@ -1,2 +0,0 @@
-#[cfg(feature = "dioxus-bindings")]
-pub use crate::widgets::*;

+ 19 - 15
packages/rink/src/query.rs

@@ -1,13 +1,14 @@
 use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard};
 
 use dioxus_native_core::prelude::*;
+use shipyard::Unique;
 use taffy::{
     geometry::Point,
     prelude::{Layout, Size},
     Taffy,
 };
 
-use crate::{layout::TaffyLayout, layout_to_screen_space};
+use crate::{get_abs_layout, layout_to_screen_space};
 
 /// Allows querying the layout of nodes after rendering. It will only provide a correct value after a node is rendered.
 /// Provided as a root context for all tui applictions.
@@ -40,7 +41,7 @@ use crate::{layout::TaffyLayout, layout_to_screen_space};
 ///     })
 /// }
 /// ```
-#[derive(Clone)]
+#[derive(Clone, Unique)]
 pub struct Query {
     pub(crate) rdom: Arc<RwLock<RealDom>>,
     pub(crate) stretch: Arc<Mutex<Taffy>>,
@@ -69,7 +70,7 @@ pub struct ElementRef<'a> {
 }
 
 impl<'a> ElementRef<'a> {
-    fn new(
+    pub(crate) fn new(
         inner: RwLockReadGuard<'a, RealDom>,
         stretch: MutexGuard<'a, Taffy>,
         id: NodeId,
@@ -89,17 +90,20 @@ impl<'a> ElementRef<'a> {
     }
 
     pub fn layout(&self) -> Option<Layout> {
-        let layout = self
-            .stretch
-            .layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
-            .ok();
-        layout.map(|layout| Layout {
-            order: layout.order,
-            size: layout.size.map(layout_to_screen_space),
-            location: Point {
-                x: layout_to_screen_space(layout.location.x),
-                y: layout_to_screen_space(layout.location.y),
-            },
-        })
+        get_layout(self.inner.get(self.id).unwrap(), &self.stretch)
     }
 }
+
+pub(crate) fn get_layout(node: NodeRef, stretch: &Taffy) -> Option<Layout> {
+    let layout = get_abs_layout(node, stretch);
+    let pos = layout.location;
+
+    Some(Layout {
+        order: layout.order,
+        size: layout.size.map(layout_to_screen_space),
+        location: Point {
+            x: layout_to_screen_space(pos.x).round(),
+            y: layout_to_screen_space(pos.y).round(),
+        },
+    })
+}

+ 5 - 2
packages/rink/src/render.rs

@@ -1,4 +1,4 @@
-use dioxus_native_core::prelude::*;
+use dioxus_native_core::{prelude::*, tree::TreeRef};
 use std::io::Stdout;
 use taffy::{
     geometry::Point,
@@ -83,7 +83,10 @@ pub(crate) fn render_vnode(
                 frame.render_widget(WidgetWithContext::new(node, cfg), area);
             }
 
-            for c in node.children() {
+            let node_id = node.id();
+            let rdom = node.real_dom();
+            for child_id in rdom.tree_ref().children_ids_advanced(node_id, true) {
+                let c = rdom.get(child_id).unwrap();
                 render_vnode(frame, layout, c, cfg, location);
             }
         }

+ 206 - 0
packages/rink/src/widgets/button.rs

@@ -0,0 +1,206 @@
+use std::collections::HashMap;
+
+use dioxus_html::input_data::keyboard_types::Key;
+use dioxus_native_core::{
+    custom_element::CustomElement,
+    node::OwnedAttributeDiscription,
+    node_ref::AttributeMask,
+    prelude::NodeType,
+    real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
+    NodeId,
+};
+use shipyard::UniqueView;
+
+use crate::FormData;
+
+use super::{RinkWidget, WidgetContext};
+
+#[derive(Debug, Default)]
+pub(crate) struct Button {
+    text_id: NodeId,
+    value: String,
+}
+
+impl Button {
+    fn width(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "width".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn height(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "height".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
+        let width = Self::width(el);
+        let height = Self::height(el);
+        let single_char = width == "1px" || height == "1px";
+        let border_style = if single_char { "none" } else { "solid" };
+        el.set_attribute(
+            OwnedAttributeDiscription {
+                name: "border-style".to_string(),
+                namespace: Some("style".to_string()),
+            },
+            border_style.to_string(),
+        );
+    }
+
+    fn update_value_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.value = value;
+        }
+    }
+
+    fn write_value(&self, rdom: &mut RealDom) {
+        if let Some(mut text) = rdom.get_mut(self.text_id) {
+            let node_type = text.node_type_mut();
+            let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
+            *text.text_mut() = self.value.clone();
+        }
+    }
+
+    fn switch(&mut self, ctx: &mut WidgetContext, node: NodeMut) {
+        let data = FormData {
+            value: self.value.to_string(),
+            values: HashMap::new(),
+            files: None,
+        };
+        ctx.send(crate::Event {
+            id: node.id(),
+            name: "input",
+            data: crate::EventData::Form(data),
+            bubbles: true,
+        });
+    }
+}
+
+impl CustomElement for Button {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.text_id]
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        let node_type = root.node_type();
+        let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
+
+        let value = el
+            .attributes
+            .get(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string());
+
+        drop(node_type);
+
+        let rdom = root.real_dom_mut();
+        let text = rdom.create_node(value.clone().unwrap_or_default());
+        let text_id = text.id();
+
+        root.add_event_listener("keydown");
+        root.add_event_listener("click");
+
+        Self {
+            text_id,
+            value: value.unwrap_or_default(),
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        mut root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        match attributes {
+            AttributeMask::All => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    self.update_value_attr(&el);
+                    self.update_size_attr(&mut el);
+                }
+                self.write_value(root.real_dom_mut());
+            }
+            AttributeMask::Some(attrs) => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    if attrs.contains("width") || attrs.contains("height") {
+                        self.update_size_attr(&mut el);
+                    }
+                    if attrs.contains("value") {
+                        self.update_value_attr(&el);
+                    }
+                }
+                if attrs.contains("value") {
+                    self.write_value(root.real_dom_mut());
+                }
+            }
+        }
+    }
+}
+
+impl RinkWidget for Button {
+    fn handle_event(
+        &mut self,
+        event: &crate::Event,
+        mut node: dioxus_native_core::real_dom::NodeMut,
+    ) {
+        let mut ctx: WidgetContext = {
+            node.real_dom_mut()
+                .raw_world_mut()
+                .borrow::<UniqueView<WidgetContext>>()
+                .expect("expected widget context")
+                .clone()
+        };
+
+        match event.name {
+            "click" => self.switch(&mut ctx, node),
+            "keydown" => {
+                if let crate::EventData::Keyboard(data) = &event.data {
+                    if !data.is_auto_repeating()
+                        && match data.key() {
+                            Key::Character(c) if c == " " => true,
+                            Key::Enter => true,
+                            _ => false,
+                        }
+                    {
+                        self.switch(&mut ctx, node);
+                    }
+                }
+            }
+            _ => {}
+        }
+    }
+}

+ 249 - 0
packages/rink/src/widgets/checkbox.rs

@@ -0,0 +1,249 @@
+use std::collections::HashMap;
+
+use dioxus_html::input_data::keyboard_types::Key;
+use dioxus_native_core::{
+    custom_element::CustomElement,
+    node::OwnedAttributeDiscription,
+    node_ref::AttributeMask,
+    prelude::NodeType,
+    real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut},
+    NodeId,
+};
+use shipyard::UniqueView;
+
+use crate::FormData;
+
+use super::{RinkWidget, WidgetContext};
+
+#[derive(Debug, Default)]
+pub(crate) struct CheckBox {
+    div_id: NodeId,
+    text_id: NodeId,
+    value: String,
+    checked: bool,
+}
+
+impl CheckBox {
+    fn width(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "width".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn height(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "height".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
+        let width = Self::width(el);
+        let height = Self::height(el);
+        let single_char = width == "1px" || height == "1px";
+        let border_style = if single_char { "none" } else { "solid" };
+        el.set_attribute(
+            OwnedAttributeDiscription {
+                name: "border-style".to_string(),
+                namespace: Some("style".to_string()),
+            },
+            border_style.to_string(),
+        );
+    }
+
+    fn update_value_attr(&mut self, el: &ElementNodeMut) {
+        self.value = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+            .unwrap_or_else(|| "on".to_string());
+    }
+
+    fn update_checked_attr(&mut self, el: &ElementNodeMut) {
+        self.checked = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "checked".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+            .unwrap_or_else(|| "false".to_string())
+            == "true";
+    }
+
+    fn write_value(&self, mut root: NodeMut) {
+        let single_char = {
+            let node_type = root.node_type_mut();
+            let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") };
+            Self::width(&el) == "1px" || Self::height(&el) == "1px"
+        };
+        let rdom = root.real_dom_mut();
+
+        if let Some(mut text) = rdom.get_mut(self.text_id) {
+            let node_type = text.node_type_mut();
+            let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
+            let value = if single_char {
+                if self.checked {
+                    "☑"
+                } else {
+                    "☐"
+                }
+            } else if self.checked {
+                "✓"
+            } else {
+                " "
+            };
+            *text.text_mut() = value.to_string();
+        }
+    }
+
+    fn switch(&mut self, mut node: NodeMut) {
+        let new_state = !self.checked;
+
+        let data = FormData {
+            value: new_state
+                .then(|| self.value.to_string())
+                .unwrap_or_default(),
+            values: HashMap::new(),
+            files: None,
+        };
+        {
+            let ctx: UniqueView<WidgetContext> = node
+                .real_dom_mut()
+                .raw_world_mut()
+                .borrow()
+                .expect("expected widget context");
+            ctx.send(crate::Event {
+                id: self.div_id,
+                name: "input",
+                data: crate::EventData::Form(data),
+                bubbles: true,
+            });
+        }
+
+        self.checked = new_state;
+
+        self.write_value(node);
+    }
+}
+
+impl CustomElement for CheckBox {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.text_id]
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        let node_type = root.node_type();
+        let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
+
+        let value = el
+            .attributes
+            .get(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string());
+
+        drop(node_type);
+
+        let rdom = root.real_dom_mut();
+        let text = rdom.create_node(String::new());
+        let text_id = text.id();
+
+        root.add_event_listener("click");
+        root.add_event_listener("keydown");
+        let div_id = root.id();
+
+        let myself = Self {
+            div_id,
+            text_id,
+            value: value.unwrap_or_default(),
+            checked: false,
+        };
+        myself.write_value(root);
+
+        myself
+    }
+
+    fn attributes_changed(
+        &mut self,
+        mut root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        match attributes {
+            AttributeMask::All => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    self.update_value_attr(&el);
+                    self.update_size_attr(&mut el);
+                    self.update_checked_attr(&el);
+                }
+                self.write_value(root);
+            }
+            AttributeMask::Some(attrs) => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    if attrs.contains("width") || attrs.contains("height") {
+                        self.update_size_attr(&mut el);
+                    }
+                    if attrs.contains("value") {
+                        self.update_value_attr(&el);
+                    }
+                    if attrs.contains("checked") {
+                        self.update_checked_attr(&el);
+                    }
+                }
+                if attrs.contains("checked") {
+                    self.write_value(root);
+                }
+            }
+        }
+    }
+}
+
+impl RinkWidget for CheckBox {
+    fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
+        match event.name {
+            "click" => self.switch(node),
+            "keydown" => {
+                if let crate::EventData::Keyboard(data) = &event.data {
+                    if !data.is_auto_repeating()
+                        && match data.key() {
+                            Key::Character(c) if c == " " => true,
+                            Key::Enter => true,
+                            _ => false,
+                        }
+                    {
+                        self.switch(node);
+                    }
+                }
+            }
+            _ => {}
+        }
+    }
+}

+ 153 - 0
packages/rink/src/widgets/input.rs

@@ -0,0 +1,153 @@
+use dioxus_native_core::{
+    custom_element::CustomElement, node::OwnedAttributeDiscription, prelude::NodeType,
+    real_dom::NodeImmutable,
+};
+
+use super::{
+    checkbox::CheckBox, number::Number, password::Password, slider::Slider, textbox::TextBox,
+    RinkWidget,
+};
+use crate::widgets::button::Button;
+
+pub(crate) enum Input {
+    Button(Button),
+    CheckBox(CheckBox),
+    TextBox(TextBox),
+    Password(Password),
+    Number(Number),
+    Slider(Slider),
+}
+
+impl CustomElement for Input {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
+        match self {
+            Input::Button(button) => button.roots(),
+            Input::CheckBox(checkbox) => checkbox.roots(),
+            Input::TextBox(textbox) => textbox.roots(),
+            Input::Password(password) => password.roots(),
+            Input::Number(number) => number.roots(),
+            Input::Slider(slider) => slider.roots(),
+        }
+    }
+
+    fn slot(&self) -> Option<dioxus_native_core::NodeId> {
+        match self {
+            Input::Button(button) => button.slot(),
+            Input::CheckBox(checkbox) => checkbox.slot(),
+            Input::TextBox(textbox) => textbox.slot(),
+            Input::Password(password) => password.slot(),
+            Input::Number(number) => number.slot(),
+            Input::Slider(slider) => slider.slot(),
+        }
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        {
+            // currently widgets are not allowed to have children
+            let children = root.child_ids();
+            let rdom = root.real_dom_mut();
+            for child in children {
+                if let Some(mut child) = rdom.get_mut(child) {
+                    child.remove();
+                }
+            }
+        }
+
+        let node_type = root.node_type();
+        let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
+        let input_type = el
+            .attributes
+            .get(&OwnedAttributeDiscription {
+                name: "type".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text());
+        match input_type
+            .map(|type_| type_.trim().to_lowercase())
+            .as_deref()
+        {
+            Some("button") => {
+                drop(node_type);
+                Input::Button(Button::create(root))
+            }
+            Some("checkbox") => {
+                drop(node_type);
+                Input::CheckBox(CheckBox::create(root))
+            }
+            Some("textbox") => {
+                drop(node_type);
+                Input::TextBox(TextBox::create(root))
+            }
+            Some("password") => {
+                drop(node_type);
+                Input::Password(Password::create(root))
+            }
+            Some("number") => {
+                drop(node_type);
+                Input::Number(Number::create(root))
+            }
+            Some("range") => {
+                drop(node_type);
+                Input::Slider(Slider::create(root))
+            }
+            _ => {
+                drop(node_type);
+                Input::TextBox(TextBox::create(root))
+            }
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        match self {
+            Input::Button(button) => {
+                button.attributes_changed(root, attributes);
+            }
+            Input::CheckBox(checkbox) => {
+                checkbox.attributes_changed(root, attributes);
+            }
+            Input::TextBox(textbox) => {
+                textbox.attributes_changed(root, attributes);
+            }
+            Input::Password(password) => {
+                password.attributes_changed(root, attributes);
+            }
+            Input::Number(number) => {
+                number.attributes_changed(root, attributes);
+            }
+            Input::Slider(slider) => {
+                slider.attributes_changed(root, attributes);
+            }
+        }
+    }
+}
+
+impl RinkWidget for Input {
+    fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
+        match self {
+            Input::Button(button) => {
+                button.handle_event(event, node);
+            }
+            Input::CheckBox(checkbox) => {
+                checkbox.handle_event(event, node);
+            }
+            Input::TextBox(textbox) => {
+                textbox.handle_event(event, node);
+            }
+            Input::Password(password) => {
+                password.handle_event(event, node);
+            }
+            Input::Number(number) => {
+                number.handle_event(event, node);
+            }
+            Input::Slider(slider) => {
+                slider.handle_event(event, node);
+            }
+        }
+    }
+}

+ 128 - 0
packages/rink/src/widgets/mod.rs

@@ -0,0 +1,128 @@
+mod button;
+mod checkbox;
+mod input;
+mod number;
+mod password;
+mod slider;
+mod text_like;
+mod textbox;
+
+use std::sync::{Arc, RwLock};
+
+use dioxus_native_core::{
+    custom_element::{CustomElement, CustomElementUpdater},
+    real_dom::{NodeMut, RealDom},
+};
+use futures_channel::mpsc::UnboundedSender;
+use shipyard::{Component, Unique};
+
+use crate::Event;
+
+pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender<Event>) {
+    // inject the widget context
+    rdom.raw_world().add_unique(WidgetContext { sender });
+
+    rdom.register_custom_element::<RinkWidgetWrapper<input::Input>>();
+}
+
+trait RinkWidget: Sync + Send + CustomElement + 'static {
+    fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
+}
+
+pub trait RinkWidgetResponder: CustomElementUpdater {
+    fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
+}
+
+impl<W: RinkWidget> RinkWidgetResponder for W {
+    fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
+        RinkWidget::handle_event(self, event, node)
+    }
+}
+
+struct RinkWidgetWrapper<W: RinkWidget> {
+    inner: RinkWidgetTraitObject,
+    _marker: std::marker::PhantomData<W>,
+}
+
+impl<W: RinkWidget> CustomElement for RinkWidgetWrapper<W> {
+    const NAME: &'static str = W::NAME;
+
+    const NAMESPACE: Option<&'static str> = W::NAMESPACE;
+
+    fn create(mut node: NodeMut) -> Self {
+        let myself = RinkWidgetTraitObject {
+            widget: Arc::new(RwLock::new(W::create(node.reborrow()))),
+        };
+
+        // Insert the widget as an arbitrary data node so that it can be recognized when bubbling events
+        node.insert(myself.clone());
+
+        RinkWidgetWrapper {
+            inner: myself,
+            _marker: std::marker::PhantomData,
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        let mut widget = self.inner.widget.write().unwrap();
+        widget.attributes_changed(root, attributes);
+    }
+
+    fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
+        let widget = self.inner.widget.read().unwrap();
+        widget.roots()
+    }
+
+    fn slot(&self) -> Option<dioxus_native_core::NodeId> {
+        let widget = self.inner.widget.read().unwrap();
+        widget.slot()
+    }
+}
+
+#[derive(Clone, Component)]
+pub(crate) struct RinkWidgetTraitObject {
+    widget: Arc<RwLock<dyn RinkWidgetResponder + Send + Sync>>,
+}
+
+impl CustomElementUpdater for RinkWidgetTraitObject {
+    fn attributes_changed(
+        &mut self,
+        light_root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        let mut widget = self.widget.write().unwrap();
+        widget.attributes_changed(light_root, attributes);
+    }
+
+    fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
+        let widget = self.widget.read().unwrap();
+        widget.roots()
+    }
+
+    fn slot(&self) -> Option<dioxus_native_core::NodeId> {
+        let widget = self.widget.read().unwrap();
+        widget.slot()
+    }
+}
+
+impl RinkWidgetResponder for RinkWidgetTraitObject {
+    fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
+        let mut widget = self.widget.write().unwrap();
+        widget.handle_event(event, node);
+    }
+}
+
+#[derive(Unique, Clone)]
+pub(crate) struct WidgetContext {
+    sender: UnboundedSender<Event>,
+}
+
+impl WidgetContext {
+    pub(crate) fn send(&self, event: Event) {
+        self.sender.unbounded_send(event).unwrap();
+    }
+}

+ 91 - 0
packages/rink/src/widgets/number.rs

@@ -0,0 +1,91 @@
+use dioxus_html::input_data::keyboard_types::Key;
+use dioxus_native_core::{
+    custom_element::CustomElement,
+    real_dom::{NodeImmutable, RealDom},
+    NodeId,
+};
+
+use crate::EventData;
+
+use super::{text_like::TextLike, RinkWidget};
+
+#[derive(Debug, Default)]
+pub(crate) struct Number {
+    text: TextLike,
+}
+
+impl Number {
+    fn increase(&mut self, rdom: &mut RealDom, id: NodeId) {
+        let num = self.text.text().parse::<f64>().unwrap_or(0.0);
+        self.text.set_text((num + 1.0).to_string(), rdom, id);
+    }
+
+    fn decrease(&mut self, rdom: &mut RealDom, id: NodeId) {
+        let num = self.text.text().parse::<f64>().unwrap_or(0.0);
+        self.text.set_text((num - 1.0).to_string(), rdom, id);
+    }
+}
+
+impl CustomElement for Number {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<NodeId> {
+        self.text.roots()
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        Number {
+            text: TextLike::create(root.reborrow()),
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        self.text.attributes_changed(root, attributes)
+    }
+}
+
+impl RinkWidget for Number {
+    fn handle_event(
+        &mut self,
+        event: &crate::Event,
+        mut node: dioxus_native_core::real_dom::NodeMut,
+    ) {
+        if event.name == "keydown" {
+            if let EventData::Keyboard(data) = &event.data {
+                let key = data.key();
+                let is_num_like = match key.clone() {
+                    Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
+                    Key::Character(c)
+                        if c == "." || c == "-" || c.chars().all(|c| c.is_numeric()) =>
+                    {
+                        true
+                    }
+                    _ => false,
+                };
+
+                if is_num_like {
+                    self.text.handle_event(event, node)
+                } else {
+                    let id = node.id();
+                    let rdom = node.real_dom_mut();
+                    match key {
+                        Key::ArrowUp => {
+                            self.increase(rdom, id);
+                        }
+                        Key::ArrowDown => {
+                            self.decrease(rdom, id);
+                        }
+                        _ => (),
+                    }
+                }
+                return;
+            }
+        }
+
+        self.text.handle_event(event, node)
+    }
+}

+ 12 - 0
packages/rink/src/widgets/password.rs

@@ -0,0 +1,12 @@
+use super::text_like::{TextLike, TextLikeController};
+
+pub(crate) type Password = TextLike<PasswordController>;
+
+#[derive(Debug, Default)]
+pub(crate) struct PasswordController;
+
+impl TextLikeController for PasswordController {
+    fn display_text(&self, text: &str) -> String {
+        text.chars().map(|_| '.').collect()
+    }
+}

+ 456 - 0
packages/rink/src/widgets/slider.rs

@@ -0,0 +1,456 @@
+use std::collections::HashMap;
+
+use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData};
+use dioxus_native_core::{
+    custom_element::CustomElement,
+    node::{OwnedAttributeDiscription, OwnedAttributeValue},
+    node_ref::AttributeMask,
+    prelude::{ElementNode, NodeType},
+    real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
+    NodeId,
+};
+use shipyard::UniqueView;
+
+use super::{RinkWidget, WidgetContext};
+use crate::{query::get_layout, Event, EventData, FormData, Query};
+
+#[derive(Debug)]
+pub(crate) struct Slider {
+    div_wrapper: NodeId,
+    pre_cursor_div: NodeId,
+    post_cursor_div: NodeId,
+    min: f64,
+    max: f64,
+    step: Option<f64>,
+    value: f64,
+    border: bool,
+}
+
+impl Default for Slider {
+    fn default() -> Self {
+        Self {
+            div_wrapper: Default::default(),
+            pre_cursor_div: Default::default(),
+            post_cursor_div: Default::default(),
+            min: 0.0,
+            max: 100.0,
+            step: None,
+            value: 0.0,
+            border: false,
+        }
+    }
+}
+
+impl Slider {
+    fn size(&self) -> f64 {
+        self.max - self.min
+    }
+
+    fn step(&self) -> f64 {
+        self.step.unwrap_or(self.size() / 10.0)
+    }
+
+    fn width(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "width".to_string(),
+                namespace: Some("style".to_string()),
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn height(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "height".to_string(),
+                namespace: Some("style".to_string()),
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn update_min_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "min".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.min = value.parse().ok().unwrap_or(0.0);
+        }
+    }
+
+    fn update_max_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "max".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.max = value.parse().ok().unwrap_or(100.0);
+        }
+    }
+
+    fn update_step_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "step".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.step = value.parse().ok();
+        }
+    }
+
+    fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
+        let width = Self::width(el);
+        let height = Self::height(el);
+        let single_char = width
+            .strip_prefix("px")
+            .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
+            .is_some()
+            || height
+                .strip_prefix("px")
+                .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
+                .is_some();
+        self.border = !single_char;
+        let border_style = if self.border { "solid" } else { "none" };
+        el.set_attribute(
+            OwnedAttributeDiscription {
+                name: "border-style".to_string(),
+                namespace: Some("style".to_string()),
+            },
+            border_style.to_string(),
+        );
+    }
+
+    fn update_value(&mut self, new: f64) {
+        self.value = new.clamp(self.min, self.max);
+    }
+
+    fn update_value_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.update_value(value.parse().ok().unwrap_or(0.0));
+        }
+    }
+
+    fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
+        let value_percent = (self.value - self.min) / self.size() * 100.0;
+
+        if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) {
+            let node_type = div.node_type_mut();
+            let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
+            element.set_attribute(
+                OwnedAttributeDiscription {
+                    name: "width".to_string(),
+                    namespace: Some("style".to_string()),
+                },
+                format!("{}%", value_percent),
+            );
+        }
+
+        if let Some(mut div) = rdom.get_mut(self.post_cursor_div) {
+            let node_type = div.node_type_mut();
+            let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
+            element.set_attribute(
+                OwnedAttributeDiscription {
+                    name: "width".to_string(),
+                    namespace: Some("style".to_string()),
+                },
+                format!("{}%", 100.0 - value_percent),
+            );
+        }
+
+        // send the event
+        let world = rdom.raw_world_mut();
+
+        {
+            let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
+
+            let data = FormData {
+                value: self.value.to_string(),
+                values: HashMap::new(),
+                files: None,
+            };
+            ctx.send(Event {
+                id,
+                name: "input",
+                data: EventData::Form(data),
+                bubbles: true,
+            });
+        }
+    }
+
+    fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) {
+        let key = data.key();
+
+        let step = self.step();
+        match key {
+            Key::ArrowDown | Key::ArrowLeft => {
+                self.update_value(self.value - step);
+            }
+            Key::ArrowUp | Key::ArrowRight => {
+                self.update_value(self.value + step);
+            }
+            _ => {
+                return;
+            }
+        }
+
+        let id = root.id();
+
+        let rdom = root.real_dom_mut();
+        self.write_value(rdom, id);
+    }
+
+    fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) {
+        if !data.held_buttons().is_empty() {
+            let id = root.id();
+            let rdom = root.real_dom_mut();
+            let world = rdom.raw_world_mut();
+            let taffy = {
+                let query: UniqueView<Query> = world.borrow().unwrap();
+                query.stretch.clone()
+            };
+
+            let taffy = taffy.lock().unwrap();
+
+            let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
+
+            let width = layout.size.width as f64;
+            let offset = data.element_coordinates();
+            self.update_value(self.min + self.size() * offset.x / width);
+
+            self.write_value(rdom, id);
+        }
+    }
+}
+
+impl CustomElement for Slider {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.div_wrapper]
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        let node_type = root.node_type();
+        let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
+
+        let value = el.attributes.get(&OwnedAttributeDiscription {
+            name: "value".to_string(),
+            namespace: None,
+        });
+        let value = value
+            .and_then(|value| match value {
+                OwnedAttributeValue::Text(text) => text.as_str().parse().ok(),
+                OwnedAttributeValue::Float(float) => Some(*float),
+                OwnedAttributeValue::Int(int) => Some(*int as f64),
+                _ => None,
+            })
+            .unwrap_or(0.0);
+
+        drop(node_type);
+
+        let rdom = root.real_dom_mut();
+
+        let pre_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "div".to_string(),
+            attributes: [(
+                OwnedAttributeDiscription {
+                    name: "background-color".to_string(),
+                    namespace: Some("style".to_string()),
+                },
+                "rgba(10,10,10,0.5)".to_string().into(),
+            )]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        let pre_cursor_div_id = pre_cursor_div.id();
+
+        let cursor_text = rdom.create_node("|".to_string());
+        let cursor_text_id = cursor_text.id();
+        let mut cursor_span = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "div".to_string(),
+            attributes: [].into_iter().collect(),
+            ..Default::default()
+        }));
+        cursor_span.add_child(cursor_text_id);
+        let cursor_span_id = cursor_span.id();
+
+        let post_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "span".to_string(),
+            attributes: [
+                (
+                    OwnedAttributeDiscription {
+                        name: "width".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "100%".to_string().into(),
+                ),
+                (
+                    OwnedAttributeDiscription {
+                        name: "background-color".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "rgba(10,10,10,0.5)".to_string().into(),
+                ),
+            ]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        let post_cursor_div_id = post_cursor_div.id();
+
+        let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "div".to_string(),
+            attributes: [
+                (
+                    OwnedAttributeDiscription {
+                        name: "display".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "flex".to_string().into(),
+                ),
+                (
+                    OwnedAttributeDiscription {
+                        name: "flex-direction".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "row".to_string().into(),
+                ),
+                (
+                    OwnedAttributeDiscription {
+                        name: "width".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "100%".to_string().into(),
+                ),
+                (
+                    OwnedAttributeDiscription {
+                        name: "height".to_string(),
+                        namespace: Some("style".to_string()),
+                    },
+                    "100%".to_string().into(),
+                ),
+            ]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        let div_wrapper_id = div_wrapper.id();
+        div_wrapper.add_child(pre_cursor_div_id);
+        div_wrapper.add_child(cursor_span_id);
+        div_wrapper.add_child(post_cursor_div_id);
+
+        root.add_event_listener("mousemove");
+        root.add_event_listener("mousedown");
+        root.add_event_listener("keydown");
+
+        Self {
+            pre_cursor_div: pre_cursor_div_id,
+            post_cursor_div: post_cursor_div_id,
+            div_wrapper: div_wrapper_id,
+            value,
+            ..Default::default()
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        mut root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        match attributes {
+            AttributeMask::All => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    self.update_value_attr(&el);
+                    self.update_size_attr(&mut el);
+                    self.update_max_attr(&el);
+                    self.update_min_attr(&el);
+                    self.update_step_attr(&el);
+                }
+                let id = root.id();
+                self.write_value(root.real_dom_mut(), id);
+            }
+            AttributeMask::Some(attrs) => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    if attrs.contains("width") || attrs.contains("height") {
+                        self.update_size_attr(&mut el);
+                    }
+                    if attrs.contains("max") {
+                        self.update_max_attr(&el);
+                    }
+                    if attrs.contains("min") {
+                        self.update_min_attr(&el);
+                    }
+                    if attrs.contains("step") {
+                        self.update_step_attr(&el);
+                    }
+                    if attrs.contains("value") {
+                        self.update_value_attr(&el);
+                    }
+                }
+                if attrs.contains("value") {
+                    let id = root.id();
+                    self.write_value(root.real_dom_mut(), id);
+                }
+            }
+        }
+    }
+}
+
+impl RinkWidget for Slider {
+    fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
+        match event.name {
+            "keydown" => {
+                if let EventData::Keyboard(data) = &event.data {
+                    self.handle_keydown(node, data);
+                }
+            }
+
+            "mousemove" => {
+                if let EventData::Mouse(data) = &event.data {
+                    self.handle_mousemove(node, data);
+                }
+            }
+
+            "mousedown" => {
+                if let EventData::Mouse(data) = &event.data {
+                    self.handle_mousemove(node, data);
+                }
+            }
+
+            _ => {}
+        }
+    }
+}

+ 444 - 0
packages/rink/src/widgets/text_like.rs

@@ -0,0 +1,444 @@
+use std::{collections::HashMap, io::stdout};
+
+use crossterm::{cursor::MoveTo, execute};
+use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData};
+use dioxus_native_core::{
+    custom_element::CustomElement,
+    node::OwnedAttributeDiscription,
+    node_ref::AttributeMask,
+    prelude::{ElementNode, NodeType},
+    real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
+    utils::cursor::{Cursor, Pos},
+    NodeId,
+};
+use shipyard::UniqueView;
+use taffy::geometry::Point;
+
+use crate::{query::get_layout, Event, EventData, FormData, Query};
+
+use super::{RinkWidget, WidgetContext};
+
+pub(crate) trait TextLikeController {
+    fn display_text(&self, text: &str) -> String {
+        text.to_string()
+    }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct EmptyController;
+
+impl TextLikeController for EmptyController {}
+
+#[derive(Debug, Default)]
+pub(crate) struct TextLike<C: TextLikeController = EmptyController> {
+    text: String,
+    div_wrapper: NodeId,
+    pre_cursor_text: NodeId,
+    highlighted_text: NodeId,
+    post_cursor_text: NodeId,
+    cursor: Cursor,
+    dragging: bool,
+    border: bool,
+    max_len: Option<usize>,
+    controller: C,
+}
+
+impl<C: TextLikeController> TextLike<C> {
+    fn width(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "width".to_string(),
+                namespace: Some("style".to_string()),
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn height(el: &ElementNodeMut) -> String {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "height".to_string(),
+                namespace: Some("style".to_string()),
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            value
+        } else {
+            "1px".to_string()
+        }
+    }
+
+    fn update_max_width_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "maxlength".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            if let Ok(max_len) = value.parse::<usize>() {
+                self.max_len = Some(max_len);
+            }
+        }
+    }
+
+    fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
+        let width = Self::width(el);
+        let height = Self::height(el);
+        let single_char = width
+            .strip_prefix("px")
+            .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
+            .is_some()
+            || height
+                .strip_prefix("px")
+                .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
+                .is_some();
+        self.border = !single_char;
+        let border_style = if self.border { "solid" } else { "none" };
+        el.set_attribute(
+            OwnedAttributeDiscription {
+                name: "border-style".to_string(),
+                namespace: Some("style".to_string()),
+            },
+            border_style.to_string(),
+        );
+    }
+
+    fn update_value_attr(&mut self, el: &ElementNodeMut) {
+        if let Some(value) = el
+            .get_attribute(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string())
+        {
+            self.text = value;
+        }
+    }
+
+    pub(crate) fn set_text(&mut self, text: String, rdom: &mut RealDom, id: NodeId) {
+        self.text = text;
+        self.write_value(rdom, id);
+    }
+
+    pub(crate) fn text(&self) -> &str {
+        self.text.as_str()
+    }
+
+    fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
+        let start_highlight = self.cursor.first().idx(self.text.as_str());
+        let end_highlight = self.cursor.last().idx(self.text.as_str());
+        let (text_before_first_cursor, text_after_first_cursor) =
+            self.text.split_at(start_highlight);
+        let (text_highlighted, text_after_second_cursor) =
+            text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+        if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) {
+            let node_type = text.node_type_mut();
+            let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
+            *text.text_mut() = self.controller.display_text(text_before_first_cursor);
+        }
+
+        if let Some(mut text) = rdom.get_mut(self.highlighted_text) {
+            let node_type = text.node_type_mut();
+            let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
+            *text.text_mut() = self.controller.display_text(text_highlighted);
+        }
+
+        if let Some(mut text) = rdom.get_mut(self.post_cursor_text) {
+            let node_type = text.node_type_mut();
+            let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
+            *text.text_mut() = self.controller.display_text(text_after_second_cursor);
+        }
+
+        // send the event
+        {
+            let world = rdom.raw_world_mut();
+            let data: FormData = FormData {
+                value: self.text.clone(),
+                values: HashMap::new(),
+                files: None,
+            };
+            let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
+
+            ctx.send(Event {
+                id,
+                name: "input",
+                data: EventData::Form(data),
+                bubbles: true,
+            });
+        }
+    }
+
+    fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) {
+        let key = data.key();
+        let modifiers = data.modifiers();
+        let code = data.code();
+
+        if key == Key::Enter {
+            return;
+        }
+        self.cursor.handle_input(
+            &code,
+            &key,
+            &modifiers,
+            &mut self.text,
+            self.max_len.unwrap_or(1000),
+        );
+
+        let id = root.id();
+
+        let rdom = root.real_dom_mut();
+        self.write_value(rdom, id);
+        let world = rdom.raw_world_mut();
+
+        // move cursor to new position
+        let taffy = {
+            let query: UniqueView<Query> = world.borrow().unwrap();
+            query.stretch.clone()
+        };
+
+        let taffy = taffy.lock().unwrap();
+
+        let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
+        let Point { x, y } = layout.location;
+
+        let Pos { col, row } = self.cursor.start;
+        let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
+        if let Ok(pos) = crossterm::cursor::position() {
+            if pos != (x, y) {
+                execute!(stdout(), MoveTo(x, y)).unwrap();
+            }
+        } else {
+            execute!(stdout(), MoveTo(x, y)).unwrap();
+        }
+    }
+
+    fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) {
+        if self.dragging {
+            let id = root.id();
+            let offset = data.element_coordinates();
+            let mut new = Pos::new(offset.x as usize, offset.y as usize);
+
+            // textboxs are only one line tall
+            new.row = 0;
+
+            if new != self.cursor.start {
+                self.cursor.end = Some(new);
+            }
+            let rdom = root.real_dom_mut();
+            self.write_value(rdom, id);
+        }
+    }
+
+    fn handle_mousedown(&mut self, mut root: NodeMut, data: &MouseData) {
+        let offset = data.element_coordinates();
+        let mut new = Pos::new(offset.x as usize, offset.y as usize);
+
+        // textboxs are only one line tall
+        new.row = 0;
+
+        new.realize_col(self.text.as_str());
+        self.cursor = Cursor::from_start(new);
+        self.dragging = true;
+
+        let id = root.id();
+
+        // move cursor to new position
+        let rdom = root.real_dom_mut();
+        let world = rdom.raw_world_mut();
+        let taffy = {
+            let query: UniqueView<Query> = world.borrow().unwrap();
+            query.stretch.clone()
+        };
+
+        let taffy = taffy.lock().unwrap();
+
+        let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
+        let Point { x, y } = layout.location;
+
+        let Pos { col, row } = self.cursor.start;
+        let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
+        if let Ok(pos) = crossterm::cursor::position() {
+            if pos != (x, y) {
+                execute!(stdout(), MoveTo(x, y)).unwrap();
+            }
+        } else {
+            execute!(stdout(), MoveTo(x, y)).unwrap();
+        }
+
+        self.write_value(rdom, id)
+    }
+}
+
+impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for TextLike<C> {
+    const NAME: &'static str = "input";
+
+    fn roots(&self) -> Vec<NodeId> {
+        vec![self.div_wrapper]
+    }
+
+    fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
+        let node_type = root.node_type();
+        let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
+
+        let value = el
+            .attributes
+            .get(&OwnedAttributeDiscription {
+                name: "value".to_string(),
+                namespace: None,
+            })
+            .and_then(|value| value.as_text())
+            .map(|value| value.to_string());
+
+        drop(node_type);
+
+        let rdom = root.real_dom_mut();
+
+        let pre_text = rdom.create_node(String::new());
+        let pre_text_id = pre_text.id();
+        let highlighted_text = rdom.create_node(String::new());
+        let highlighted_text_id = highlighted_text.id();
+        let mut highlighted_text_span = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "span".to_string(),
+            attributes: [(
+                OwnedAttributeDiscription {
+                    name: "background-color".to_string(),
+                    namespace: Some("style".to_string()),
+                },
+                "rgba(255, 255, 255, 50%)".to_string().into(),
+            )]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        highlighted_text_span.add_child(highlighted_text_id);
+        let highlighted_text_span_id = highlighted_text_span.id();
+        let post_text = rdom.create_node(value.clone().unwrap_or_default());
+        let post_text_id = post_text.id();
+        let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
+            tag: "div".to_string(),
+            attributes: [(
+                OwnedAttributeDiscription {
+                    name: "display".to_string(),
+                    namespace: Some("style".to_string()),
+                },
+                "flex".to_string().into(),
+            )]
+            .into_iter()
+            .collect(),
+            ..Default::default()
+        }));
+        let div_wrapper_id = div_wrapper.id();
+        div_wrapper.add_child(pre_text_id);
+        div_wrapper.add_child(highlighted_text_span_id);
+        div_wrapper.add_child(post_text_id);
+
+        div_wrapper.add_event_listener("mousemove");
+        div_wrapper.add_event_listener("mousedown");
+        div_wrapper.add_event_listener("mouseup");
+        div_wrapper.add_event_listener("mouseleave");
+        div_wrapper.add_event_listener("mouseenter");
+        root.add_event_listener("keydown");
+        root.add_event_listener("focusout");
+
+        Self {
+            pre_cursor_text: pre_text_id,
+            highlighted_text: highlighted_text_id,
+            post_cursor_text: post_text_id,
+            div_wrapper: div_wrapper_id,
+            cursor: Cursor::default(),
+            text: value.unwrap_or_default(),
+            ..Default::default()
+        }
+    }
+
+    fn attributes_changed(
+        &mut self,
+        mut root: dioxus_native_core::real_dom::NodeMut,
+        attributes: &dioxus_native_core::node_ref::AttributeMask,
+    ) {
+        match attributes {
+            AttributeMask::All => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    self.update_value_attr(&el);
+                    self.update_size_attr(&mut el);
+                    self.update_max_width_attr(&el);
+                }
+                let id = root.id();
+                self.write_value(root.real_dom_mut(), id);
+            }
+            AttributeMask::Some(attrs) => {
+                {
+                    let node_type = root.node_type_mut();
+                    let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") };
+                    if attrs.contains("width") || attrs.contains("height") {
+                        self.update_size_attr(&mut el);
+                    }
+                    if attrs.contains("maxlength") {
+                        self.update_max_width_attr(&el);
+                    }
+                    if attrs.contains("value") {
+                        self.update_value_attr(&el);
+                    }
+                }
+                if attrs.contains("value") {
+                    let id = root.id();
+                    self.write_value(root.real_dom_mut(), id);
+                }
+            }
+        }
+    }
+}
+
+impl<C: TextLikeController + Send + Sync + Default + 'static> RinkWidget for TextLike<C> {
+    fn handle_event(&mut self, event: &crate::Event, node: NodeMut) {
+        match event.name {
+            "keydown" => {
+                if let EventData::Keyboard(data) = &event.data {
+                    self.handle_keydown(node, data);
+                }
+            }
+
+            "mousemove" => {
+                if let EventData::Mouse(data) = &event.data {
+                    self.handle_mousemove(node, data);
+                }
+            }
+
+            "mousedown" => {
+                if let EventData::Mouse(data) = &event.data {
+                    self.handle_mousedown(node, data);
+                }
+            }
+
+            "mouseup" => {
+                self.dragging = false;
+            }
+
+            "mouseleave" => {
+                self.dragging = false;
+            }
+
+            "mouseenter" => {
+                self.dragging = false;
+            }
+
+            "focusout" => {
+                execute!(stdout(), MoveTo(0, 1000)).unwrap();
+            }
+
+            _ => {}
+        }
+    }
+}

+ 3 - 0
packages/rink/src/widgets/textbox.rs

@@ -0,0 +1,3 @@
+use super::text_like::TextLike;
+
+pub(crate) type TextBox = TextLike;