From c4c35cb0460ae47feced0dbeb07d0ad477265473 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 13:26:29 -0500 Subject: [PATCH 01/35] make TUI widgets cross framework --- packages/core/src/scopes.rs | 2 + packages/dioxus-tui/examples/widgets.rs | 29 +- packages/dioxus-tui/src/lib.rs | 9 +- packages/dioxus-tui/src/prelude/mod.rs | 2 - packages/dioxus-tui/src/widgets/button.rs | 59 --- packages/dioxus-tui/src/widgets/checkbox.rs | 82 ---- packages/dioxus-tui/src/widgets/input.rs | 102 ---- packages/dioxus-tui/src/widgets/mod.rs | 22 - packages/dioxus-tui/src/widgets/number.rs | 209 -------- packages/dioxus-tui/src/widgets/password.rs | 193 -------- packages/dioxus-tui/src/widgets/slider.rs | 107 ---- packages/dioxus-tui/src/widgets/textbox.rs | 182 ------- packages/native-core/src/node_ref.rs | 27 +- packages/native-core/src/node_watcher.rs | 18 +- packages/native-core/src/real_dom.rs | 108 +++- packages/native-core/src/utils/mod.rs | 1 + .../src/utils/persistant_iterator.rs | 4 +- .../native-core/src/utils/widget_watcher.rs | 136 +++++ packages/rink/examples/counter.rs | 3 +- packages/rink/examples/grid.rs | 3 +- packages/rink/examples/widgets.rs | 104 ++++ packages/rink/src/hooks.rs | 115 +++-- packages/rink/src/lib.rs | 77 ++- packages/rink/src/prelude/mod.rs | 2 - packages/rink/src/query.rs | 53 +- packages/rink/src/widgets/button.rs | 204 ++++++++ packages/rink/src/widgets/checkbox.rs | 250 ++++++++++ packages/rink/src/widgets/input.rs | 132 +++++ packages/rink/src/widgets/mod.rs | 95 ++++ packages/rink/src/widgets/number.rs | 464 ++++++++++++++++++ packages/rink/src/widgets/password.rs | 432 ++++++++++++++++ packages/rink/src/widgets/slider.rs | 458 +++++++++++++++++ packages/rink/src/widgets/textbox.rs | 432 ++++++++++++++++ 33 files changed, 3025 insertions(+), 1091 deletions(-) delete mode 100644 packages/dioxus-tui/src/prelude/mod.rs delete mode 100644 packages/dioxus-tui/src/widgets/button.rs delete mode 100644 packages/dioxus-tui/src/widgets/checkbox.rs delete mode 100644 packages/dioxus-tui/src/widgets/input.rs delete mode 100644 packages/dioxus-tui/src/widgets/mod.rs delete mode 100644 packages/dioxus-tui/src/widgets/number.rs delete mode 100644 packages/dioxus-tui/src/widgets/password.rs delete mode 100644 packages/dioxus-tui/src/widgets/slider.rs delete mode 100644 packages/dioxus-tui/src/widgets/textbox.rs create mode 100644 packages/native-core/src/utils/widget_watcher.rs create mode 100644 packages/rink/examples/widgets.rs delete mode 100644 packages/rink/src/prelude/mod.rs create mode 100644 packages/rink/src/widgets/button.rs create mode 100644 packages/rink/src/widgets/checkbox.rs create mode 100644 packages/rink/src/widgets/input.rs create mode 100644 packages/rink/src/widgets/mod.rs create mode 100644 packages/rink/src/widgets/number.rs create mode 100644 packages/rink/src/widgets/password.rs create mode 100644 packages/rink/src/widgets/slider.rs create mode 100644 packages/rink/src/widgets/textbox.rs diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5601d57fe..5d1c661c0 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -591,6 +591,8 @@ impl<'src> ScopeState { propagates: event.propagates, data, }); + } else { + println!("Failed to downcast event data") } })) }; diff --git a/packages/dioxus-tui/examples/widgets.rs b/packages/dioxus-tui/examples/widgets.rs index 04690522c..b5df35388 100644 --- a/packages/dioxus-tui/examples/widgets.rs +++ b/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::().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%", diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index 296fd4697..4f067b024 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -1,6 +1,3 @@ -pub mod prelude; -pub mod widgets; - use std::{ ops::Deref, rc::Rc, @@ -8,7 +5,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::*; @@ -83,13 +79,14 @@ impl Driver for DioxusRenderer { rdom: &Arc>, id: NodeId, event: &str, - value: Rc, + value: Rc, 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); } } diff --git a/packages/dioxus-tui/src/prelude/mod.rs b/packages/dioxus-tui/src/prelude/mod.rs deleted file mode 100644 index cb4fdee65..000000000 --- a/packages/dioxus-tui/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use crate::widgets::*; -pub use rink::Config; diff --git a/packages/dioxus-tui/src/widgets/button.rs b/packages/dioxus-tui/src/widgets/button.rs deleted file mode 100644 index a6578905e..000000000 --- a/packages/dioxus-tui/src/widgets/button.rs +++ /dev/null @@ -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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/checkbox.rs b/packages/dioxus-tui/src/widgets/checkbox.rs deleted file mode 100644 index 0b5ebf671..000000000 --- a/packages/dioxus-tui/src/widgets/checkbox.rs +++ /dev/null @@ -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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/input.rs b/packages/dioxus-tui/src/widgets/input.rs deleted file mode 100644 index 2294b0f10..000000000 --- a/packages/dioxus-tui/src/widgets/input.rs +++ /dev/null @@ -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>, - onclick: Option>, - 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, - } - } - } - }) -} diff --git a/packages/dioxus-tui/src/widgets/mod.rs b/packages/dioxus-tui/src/widgets/mod.rs deleted file mode 100644 index 18e3b12b4..000000000 --- a/packages/dioxus-tui/src/widgets/mod.rs +++ /dev/null @@ -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(cx: Scope) -> Option { - 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 - } -} diff --git a/packages/dioxus-tui/src/widgets/number.rs b/packages/dioxus-tui/src/widgets/number.rs deleted file mode 100644 index f82732981..000000000 --- a/packages/dioxus-tui/src/widgets/number.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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::().unwrap_or(0.0) + 1.0).to_string(); - update(text.clone()); - }; - let decrease = move || { - let mut text = text_ref.write(); - *text = (text.parse::().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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/password.rs b/packages/dioxus-tui/src/widgets/password.rs deleted file mode 100644 index 07491c140..000000000 --- a/packages/dioxus-tui/src/widgets/password.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/slider.rs b/packages/dioxus-tui/src/widgets/slider.rs deleted file mode 100644 index 9e8d63cb9..000000000 --- a/packages/dioxus-tui/src/widgets/slider.rs +++ /dev/null @@ -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 = 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)", - } - } - } -} diff --git a/packages/dioxus-tui/src/widgets/textbox.rs b/packages/dioxus-tui/src/widgets/textbox.rs deleted file mode 100644 index fcfaf0a90..000000000 --- a/packages/dioxus-tui/src/widgets/textbox.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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}" - } - } -} diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index 22ab5d8f1..a1a309622 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -126,7 +126,7 @@ impl AttributeMask { pub fn union(&self, other: &Self) -> Self { match (self, other) { (AttributeMask::Some(s), AttributeMask::Some(o)) => { - AttributeMask::Some(s.intersection(o).cloned().collect()) + AttributeMask::Some(s.union(o).cloned().collect()) } _ => AttributeMask::All, } @@ -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. diff --git a/packages/native-core/src/node_watcher.rs b/packages/native-core/src/node_watcher.rs index 27a449ef9..d437b158c 100644 --- a/packages/native-core/src/node_watcher.rs +++ b/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 { /// Called after a node is added to the tree. - fn on_node_added(&self, _node: NodeMut) {} + fn on_node_added(&mut self, _node: NodeMut) {} /// Called before a node is removed from the tree. - fn on_node_removed(&self, _node: NodeMut) {} + fn on_node_removed(&mut self, _node: NodeMut) {} /// Called after a node is moved to a new parent. - fn on_node_moved(&self, _node: NodeMut) {} - // /// Called after the text content of a node is changed. - // fn on_text_changed(&self, _node: NodeMut) {} - // /// Called after an attribute of an element is changed. - // fn on_attribute_changed(&self, _node: NodeMut, attribute: &str) {} + fn on_node_moved(&mut self, _node: NodeMut) {} +} + +/// A trait for watching for changes to attributes of an element. +pub trait AttributeWatcher { + /// Called before update_state is called on the RealDom + fn on_attributes_changed(&self, _node: NodeMut, _attributes: &AttributeMask) {} } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 1deec7d64..7d318ddc1 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -14,7 +14,7 @@ use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; use crate::node_ref::{NodeMask, NodeMaskBuilder}; -use crate::node_watcher::NodeWatcher; +use crate::node_watcher::{AttributeWatcher, NodeWatcher}; use crate::passes::{DirtyNodeStates, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; @@ -49,6 +49,7 @@ impl Deref for DirtyNodesResult { pub(crate) struct NodesDirty { passes_updated: FxHashMap>, nodes_updated: FxHashMap, + nodes_created: FxHashSet, pub(crate) passes: Box<[TypeErasedState]>, } @@ -90,6 +91,7 @@ impl NodesDirty { } type NodeWatchers = Arc + Send + Sync>>>>; +type AttributeWatchers = Arc + 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. @@ -106,6 +108,7 @@ pub struct RealDom { nodes_listening: FxHashMap>, pub(crate) dirty_nodes: NodesDirty, node_watchers: NodeWatchers, + attribute_watchers: AttributeWatchers, workload: ScheduledWorkload, root_id: NodeId, phantom: std::marker::PhantomData, @@ -159,8 +162,10 @@ impl RealDom { 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, phantom: std::marker::PhantomData, @@ -181,15 +186,16 @@ impl RealDom { pub fn create_node(&mut self, node: impl Into>) -> NodeMut<'_, V> { let id = self.world.add_entity(node.into()); self.tree_mut().create_node(id); + self.dirty_nodes .passes_updated .entry(id) .or_default() .extend(self.dirty_nodes.passes.iter().map(|x| x.this_type_id)); - let watchers = self.node_watchers.clone(); - for watcher in &*watchers.read().unwrap() { - watcher.on_node_added(NodeMut::new(id, self)); - } + self.dirty_nodes + .mark_dirty(id, NodeMaskBuilder::ALL.build()); + self.dirty_nodes.nodes_created.insert(id); + NodeMut::new(id, self) } @@ -216,16 +222,19 @@ impl RealDom { self.root_id } + /// Check if a node exists in the dom. + pub fn contains(&self, id: NodeId) -> bool { + self.tree_ref().contains(id) + } + /// Get a reference to a node. pub fn get(&self, id: NodeId) -> Option> { - self.tree_ref() - .contains(id) - .then_some(NodeRef { id, dom: self }) + self.contains(id).then_some(NodeRef { id, dom: self }) } /// Get a mutable reference to a node. pub fn get_mut(&mut self, id: NodeId) -> Option> { - let contains = self.tree_ref().contains(id); + let contains = self.contains(id); contains.then(|| NodeMut::new(id, self)) } @@ -247,8 +256,41 @@ impl RealDom { &mut self, ctx: SendAnyMap, ) -> (FxDashSet, FxHashMap) { + 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); + + // call attribute watchers + for (node_id, mask) in &nodes_updated { + if self.contains(*node_id) { + // 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(), + ); + } + }; + } + } + let dirty_nodes = DirtyNodeStates::with_passes(self.dirty_nodes.passes.iter().map(|p| p.this_type_id)); let tree = self.tree_ref(); @@ -341,6 +383,17 @@ impl RealDom { 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 + '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 @@ -399,7 +452,7 @@ impl<'a, V: Component + Send + Sync> DerefMut for ViewEntr } /// A immutable view of a node -pub trait NodeImmutable: Sized { +pub trait NodeImmutable: Sized { /// Get the real dom this node was created in fn real_dom(&self) -> &RealDom; @@ -573,7 +626,9 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { .or_default() .insert(TypeId::of::()); let view_mut: ViewMut = self.dom.borrow_raw().ok()?; - Some(ViewEntryMut::new(view_mut, self.id)) + view_mut + .contains(self.id) + .then_some(ViewEntryMut::new(view_mut, self.id)) } /// Insert a custom component into this node @@ -684,7 +739,8 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { for child in children_ids_vec { self.dom.get_mut(child).unwrap().remove(); } - self.dom.tree_mut().remove_single(id); + self.dom.tree_mut().remove(id); + self.real_dom_mut().raw_world_mut().delete_entity(id); } /// Replace this node with a different node @@ -758,7 +814,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)); } } @@ -766,9 +822,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 @@ -885,6 +944,15 @@ pub struct ElementNodeMut<'a, V: FromAnyValue + Send + Sync = ()> { dirty_nodes: &'a mut NodesDirty, } +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 ElementNodeMut<'_, V> { /// Get the current element fn element(&self) -> &ElementNode { @@ -976,6 +1044,14 @@ impl ElementNodeMut<'_, V> { self.element_mut().attributes.get_mut(name) } + /// Get an attribute of the element + pub fn get_attribute( + &self, + name: &OwnedAttributeDiscription, + ) -> Option<&OwnedAttributeValue> { + self.element().attributes.get(name) + } + /// Get the set of all events the element is listening to pub fn listeners(&self) -> &FxHashSet { &self.element().listeners diff --git a/packages/native-core/src/utils/mod.rs b/packages/native-core/src/utils/mod.rs index a78b5310b..95ccc35be 100644 --- a/packages/native-core/src/utils/mod.rs +++ b/packages/native-core/src/utils/mod.rs @@ -5,3 +5,4 @@ mod persistant_iterator; pub use persistant_iterator::*; pub mod cursor; +pub mod widget_watcher; diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 96c2cffb7..5d4902b87 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -60,7 +60,7 @@ struct PersistantElementIterUpdater { } impl NodeWatcher for PersistantElementIterUpdater { - fn on_node_moved(&self, node: NodeMut) { + fn on_node_moved(&mut self, node: NodeMut) { // 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 NodeWatcher for PersistantElementIterUpda } } - fn on_node_removed(&self, node: NodeMut) { + fn on_node_removed(&mut self, node: NodeMut) { // 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(); diff --git a/packages/native-core/src/utils/widget_watcher.rs b/packages/native-core/src/utils/widget_watcher.rs new file mode 100644 index 000000000..c6836697b --- /dev/null +++ b/packages/native-core/src/utils/widget_watcher.rs @@ -0,0 +1,136 @@ +//! Widget utilities for defining, registering and updating widgets + +use std::sync::{Arc, RwLock}; + +use rustc_hash::FxHashMap; +use shipyard::Component; + +use crate::{ + node::{FromAnyValue, NodeType}, + node_ref::AttributeMask, + node_watcher::{AttributeWatcher, NodeWatcher}, + prelude::{NodeImmutable, NodeMut, RealDom}, + NodeId, +}; + +/// A watcher that handlers registering and updating widgets +#[derive(Default, Clone)] +pub struct WidgetWatcher { + inner: Arc>>, +} + +impl NodeWatcher for WidgetWatcher { + fn on_node_added(&mut self, node: NodeMut) { + let mut inner = self.inner.write().unwrap(); + inner.on_node_added(node); + } + + fn on_node_removed(&mut self, node: NodeMut) { + let mut inner = self.inner.write().unwrap(); + inner.on_node_removed(node); + } +} + +impl WidgetWatcher { + /// Register a widget + pub fn register_widget + 'static, O: WidgetUpdater>(&mut self) { + let mut inner = self.inner.write().unwrap(); + inner.builders.insert( + W::NAME, + WidgetBuilder { + create: |mut node| Box::new(W::create(&mut node)), + }, + ); + } + + /// Attach the widget watcher to the RealDom + pub fn attach(&self, dom: &mut RealDom) { + dom.add_node_watcher(self.clone()); + dom.add_attribute_watcher(self.clone()); + } +} + +impl AttributeWatcher for WidgetWatcher { + fn on_attributes_changed(&self, node: NodeMut, attributes: &AttributeMask) { + let mut inner = self.inner.write().unwrap(); + if let Some(widget) = inner.widgets.get_mut(&node.id()) { + widget.dyn_widget.attributes_changed(node, attributes); + } + } +} + +#[derive(Default)] +struct WidgetWatcherInner { + builders: FxHashMap<&'static str, WidgetBuilder>, + widgets: FxHashMap>, +} + +impl NodeWatcher for WidgetWatcherInner { + fn on_node_added(&mut self, node: NodeMut) { + let node_type = node.node_type(); + if let NodeType::Element(el) = &*node_type { + if let Some(builder) = self.builders.get(el.tag.as_str()) { + drop(node_type); + let id = node.id(); + let widget = (builder.create)(node); + self.widgets.insert(id, BoxedWidget { dyn_widget: widget }); + } + } + } + + fn on_node_removed(&mut self, node: NodeMut) { + self.widgets.remove(&node.id()); + } +} + +#[derive(Component)] +struct BoxedWidget { + dyn_widget: Box>, +} + +struct WidgetBuilder { + create: fn(NodeMut) -> Box>, +} + +/// A controlled element (a.k.a. widget) +pub trait Widget: Send + Sync + 'static { + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget. + fn create(root: &mut NodeMut) -> Self; + + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); +} + +/// A factory for creating widgets +pub trait WidgetFactory, V: FromAnyValue + Send + Sync = ()>: + Send + Sync + 'static +{ + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget. + fn create(root: &mut NodeMut) -> W; +} + +impl, V: FromAnyValue + Send + Sync> WidgetFactory for W { + const NAME: &'static str = W::NAME; + + fn create(root: &mut NodeMut) -> Self { + W::create(root) + } +} + +/// A trait for updating widgets +pub trait WidgetUpdater: Send + Sync + 'static { + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); +} + +impl, V: FromAnyValue + Send + Sync> WidgetUpdater for W { + fn attributes_changed(&mut self, root: NodeMut, attributes: &AttributeMask) { + self.attributes_changed(root, attributes); + } +} diff --git a/packages/rink/examples/counter.rs b/packages/rink/examples/counter.rs index 4d2428d8a..7bf52a53e 100644 --- a/packages/rink/examples/counter.rs +++ b/packages/rink/examples/counter.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}; diff --git a/packages/rink/examples/grid.rs b/packages/rink/examples/grid.rs index 7abc4669f..ab4449066 100644 --- a/packages/rink/examples/grid.rs +++ b/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}; diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs new file mode 100644 index 000000000..3e709313f --- /dev/null +++ b/packages/rink/examples/widgets.rs @@ -0,0 +1,104 @@ +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>) { + // 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>, + _: NodeId, + event_type: &str, + event: Rc, + _: bool, + ) { + match event_type { + "oninput" => { + // when the button is clicked, increment the counter + if let EventData::Form(input_event) = &*event { + if let Ok(value) = input_event.value.parse::() { + self.count = value; + } + } + } + _ => {} + } + } + + fn poll_async(&mut self) -> std::pin::Pin + '_>> { + 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(); +} diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index fe29b4120..341ab02ac 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -12,7 +12,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, @@ -25,13 +27,62 @@ use crate::focus::{Focus, Focused}; use crate::layout::TaffyLayout; use crate::{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, + 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 { + 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, + + pub files: Option, +} + +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, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct File {} + type EventCore = (&'static str, EventData); const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); @@ -147,13 +198,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"), }); } @@ -161,7 +212,7 @@ impl InnerInputState { resolved_events.push(Event { name: "focusout", id, - data: Rc::new(EventData::Focus(FocusData {})), + data: EventData::Focus(FocusData {}), bubbles: event_bubbles("focusout"), }); } @@ -197,7 +248,7 @@ impl InnerInputState { fn try_create_event( name: &'static str, - data: Rc, + data: EventData, will_bubble: &mut FxHashSet, resolved_events: &mut Vec, node: NodeRef, @@ -281,10 +332,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, @@ -308,7 +356,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, @@ -331,10 +379,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, @@ -354,10 +399,7 @@ impl InnerInputState { 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, @@ -378,10 +420,7 @@ impl InnerInputState { 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, @@ -403,10 +442,7 @@ impl InnerInputState { 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, @@ -429,10 +465,7 @@ impl InnerInputState { 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, @@ -456,7 +489,7 @@ impl InnerInputState { if currently_contains { try_create_event( "wheel", - Rc::new(EventData::Wheel(w.clone())), + EventData::Wheel(w.clone()), &mut will_bubble, resolved_events, node, @@ -481,10 +514,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, @@ -507,10 +537,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, @@ -627,12 +654,12 @@ impl RinkInputHandler { }) .map(|evt| (evt.0, evt.1)); - let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); + let mut hm: FxHashMap<&'static str, Vec> = 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 { diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 9fceb6b48..2aab01664 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -6,7 +6,6 @@ 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::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap}; use focus::FocusState; @@ -25,18 +24,19 @@ use taffy::Taffy; pub use taffy::{geometry::Point, prelude::*}; use tokio::select; use tui::{backend::CrosstermBackend, layout::Rect, 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,18 +91,24 @@ pub fn render( 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 || { let tick_rate = Duration::from_millis(1000); 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; } } @@ -110,10 +116,21 @@ pub fn render( }); } + 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(); @@ -213,6 +230,7 @@ pub fn render( } } + let mut event_recieved = None; { let wait = renderer.poll_async(); @@ -222,7 +240,7 @@ pub fn render( _ = wait => { }, - evt = event_reciever.next() => { + evt = raw_event_reciever.next() => { match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -243,21 +261,32 @@ pub fn render( 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 @@ -309,3 +338,23 @@ pub trait Driver { ); fn poll_async(&mut self) -> Pin + '_>>; } + +/// 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(rdom.get_mut(id).unwrap()); + + while let Some(mut node_mut) = node { + let parent_id = node_mut.parent_id(); + if let Some(mut widget) = node_mut + .get_mut::() + .map(|w| w.clone()) + { + widget.handle_event(event, &mut node_mut) + } + if !event.bubbles { + break; + } + node = parent_id.map(|id| rdom.get_mut(id).unwrap()); + } +} diff --git a/packages/rink/src/prelude/mod.rs b/packages/rink/src/prelude/mod.rs deleted file mode 100644 index f7bbb3696..000000000 --- a/packages/rink/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "dioxus-bindings")] -pub use crate::widgets::*; diff --git a/packages/rink/src/query.rs b/packages/rink/src/query.rs index 1cf113310..c204e1953 100644 --- a/packages/rink/src/query.rs +++ b/packages/rink/src/query.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard}; use dioxus_native_core::prelude::*; +use shipyard::Unique; use taffy::{ geometry::Point, prelude::{Layout, Size}, @@ -40,7 +41,7 @@ use crate::{layout::TaffyLayout, layout_to_screen_space}; /// }) /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Unique)] pub struct Query { pub(crate) rdom: Arc>, pub(crate) stretch: Arc>, @@ -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,25 +90,33 @@ impl<'a> ElementRef<'a> { } pub fn layout(&self) -> Option { - let layout = self - .stretch - .layout( - self.inner - .get(self.id) - .unwrap() - .get::() - .unwrap() - .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 { + let layout = stretch + .layout(node.get::().unwrap().node.ok()?) + .ok()?; + + let mut current_node_id = node.parent_id(); + let mut pos = layout.location; + let rdom = node.real_dom(); + while let Some(node) = current_node_id.and_then(|id| rdom.get(id)) { + let current_layout = stretch + .layout(node.get::().unwrap().node.ok()?) + .ok()?; + pos.x += current_layout.location.x; + pos.y += current_layout.location.y; + current_node_id = node.parent_id(); + } + + 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(), + }, + }) +} diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs new file mode 100644 index 000000000..2eaddfe59 --- /dev/null +++ b/packages/rink/src/widgets/button.rs @@ -0,0 +1,204 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeTypeMut, RealDom}, + utils::widget_watcher::Widget, + NodeId, +}; +use shipyard::UniqueViewMut; + +use crate::FormData; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Button { + div_id: NodeId, + 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) { + let data = FormData { + value: self.value.to_string(), + values: HashMap::new(), + files: None, + }; + ctx.send(crate::Event { + id: self.div_id, + name: "input", + data: crate::EventData::Form(data), + bubbles: true, + }); + } +} + +impl Widget for Button { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + let div_id = root.id(); + root.add_child(text_id); + + Self { + div_id, + 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, + node: &mut dioxus_native_core::real_dom::NodeMut, + ) { + let mut ctx: UniqueViewMut = node + .real_dom_mut() + .raw_world_mut() + .borrow() + .expect("expected widget context"); + + match event.name { + "click" => self.switch(&mut ctx), + "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); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs new file mode 100644 index 000000000..943db5213 --- /dev/null +++ b/packages/rink/src/widgets/checkbox.rs @@ -0,0 +1,250 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut}, + utils::widget_watcher::Widget, + 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, root: &mut 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, node: &mut 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 = 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 Widget for CheckBox { + const NAME: &'static str = "input"; + + fn create(root: &mut 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(); + root.add_child(text_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(&mut 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(&mut root); + } + } + } + } +} + +impl RinkWidget for CheckBox { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs new file mode 100644 index 000000000..2253ae096 --- /dev/null +++ b/packages/rink/src/widgets/input.rs @@ -0,0 +1,132 @@ +use dioxus_native_core::{ + node::OwnedAttributeDiscription, prelude::NodeType, real_dom::NodeImmutable, + utils::widget_watcher::Widget, +}; + +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 Widget for Input { + const NAME: &'static str = "input"; + + fn create(root: &mut 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 { + 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: &mut 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); + } + } + } +} diff --git a/packages/rink/src/widgets/mod.rs b/packages/rink/src/widgets/mod.rs new file mode 100644 index 000000000..283ebbcd9 --- /dev/null +++ b/packages/rink/src/widgets/mod.rs @@ -0,0 +1,95 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod textbox; + +use std::sync::{Arc, RwLock}; + +use dioxus_native_core::{ + real_dom::RealDom, + utils::widget_watcher::{Widget, WidgetFactory, WidgetUpdater, WidgetWatcher}, +}; +use futures_channel::mpsc::UnboundedSender; +use shipyard::{Component, Unique}; + +use crate::Event; + +pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender) { + // inject the widget context + rdom.raw_world().add_unique(WidgetContext { sender }); + + // create the widget watcher + let mut widget_watcher = WidgetWatcher::default(); + + widget_watcher + .register_widget::, RinkWidgetTraitObject>(); + + widget_watcher.attach(rdom); +} + +trait RinkWidget: Sync + Send + Widget + 'static { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut); +} + +pub trait RinkWidgetResponder: WidgetUpdater { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut); +} + +impl RinkWidgetResponder for W { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + RinkWidget::handle_event(self, event, node) + } +} + +struct RinkWidgetTraitObjectFactory { + _marker: std::marker::PhantomData, +} + +impl WidgetFactory for RinkWidgetTraitObjectFactory { + const NAME: &'static str = W::NAME; + + fn create(node: &mut dioxus_native_core::real_dom::NodeMut) -> RinkWidgetTraitObject { + let myself = RinkWidgetTraitObject { + widget: Arc::new(RwLock::new(W::create(node))), + }; + node.insert(myself.clone()); + myself + } +} + +#[derive(Clone, Component)] +pub(crate) struct RinkWidgetTraitObject { + widget: Arc>, +} + +impl WidgetUpdater for RinkWidgetTraitObject { + fn attributes_changed( + &mut self, + root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + let mut widget = self.widget.write().unwrap(); + widget.attributes_changed(root, attributes); + } +} + +impl RinkWidgetResponder for RinkWidgetTraitObject { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + let mut widget = self.widget.write().unwrap(); + widget.handle_event(event, node); + } +} + +#[derive(Unique)] +pub(crate) struct WidgetContext { + sender: UnboundedSender, +} + +impl WidgetContext { + pub(crate) fn send(&self, event: Event) { + self.sender.unbounded_send(event).unwrap(); + } +} diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs new file mode 100644 index 000000000..2925e2966 --- /dev/null +++ b/packages/rink/src/widgets/number.rs @@ -0,0 +1,464 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Number { + 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, +} + +impl Number { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = text_before_first_cursor.to_string(); + } + + 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() = text_highlighted.to_string(); + } + + 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() = text_after_second_cursor.to_string(); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn increase(&mut self) { + let num = self.text.parse::().unwrap_or(0.0); + self.text = (num + 1.0).to_string(); + } + + fn decrease(&mut self) { + let num = self.text.parse::().unwrap_or(0.0); + self.text = (num - 1.0).to_string(); + } + + fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + let key = data.key(); + let is_text = 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_text { + 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 = 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(); + } + } else { + match key { + Key::ArrowUp => { + self.increase(); + } + Key::ArrowDown => { + self.decrease(); + } + _ => (), + } + + let id = root.id(); + let rdom = root.real_dom_mut(); + self.write_value(rdom, id); + } + } + + fn handle_mousemove(&mut self, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for Number { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for Number { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/password.rs b/packages/rink/src/widgets/password.rs new file mode 100644 index 000000000..483b1e175 --- /dev/null +++ b/packages/rink/src/widgets/password.rs @@ -0,0 +1,432 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Password { + 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, +} + +impl Password { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = ".".repeat(text_before_first_cursor.len()); + } + + 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() = ".".repeat(text_highlighted.len()); + } + + 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() = ".".repeat(text_after_second_cursor.len()); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, root: &mut 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 = 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, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for Password { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for Password { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs new file mode 100644 index 000000000..e1fa4f980 --- /dev/null +++ b/packages/rink/src/widgets/slider.rs @@ -0,0 +1,458 @@ +use std::collections::HashMap; + +use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_native_core::{ + node::{OwnedAttributeDiscription, OwnedAttributeValue}, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::widget_watcher::Widget, + 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, + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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 = 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, root: &mut 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, root: &mut 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 = 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 Widget for Slider { + const NAME: &'static str = "input"; + + fn create(root: &mut 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); + + div_wrapper.add_event_listener("mousemove"); + div_wrapper.add_event_listener("mousedown"); + root.add_event_listener("keydown"); + + root.add_child(div_wrapper_id); + + 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: &mut 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); + } + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/textbox.rs b/packages/rink/src/widgets/textbox.rs new file mode 100644 index 000000000..16f73fd78 --- /dev/null +++ b/packages/rink/src/widgets/textbox.rs @@ -0,0 +1,432 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct TextBox { + 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, +} + +impl TextBox { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = text_before_first_cursor.to_string(); + } + + 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() = text_highlighted.to_string(); + } + + 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() = text_after_second_cursor.to_string(); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, root: &mut 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 = 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, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for TextBox { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for TextBox { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} From 625dc662d2112e1926cf60a058168601ab8dca5c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 12:43:51 -0500 Subject: [PATCH 02/35] make TUI widgets cross framework --- packages/dioxus-tui/examples/widgets.rs | 29 +- packages/dioxus-tui/src/lib.rs | 9 +- packages/dioxus-tui/src/prelude/mod.rs | 2 - packages/dioxus-tui/src/widgets/button.rs | 59 --- packages/dioxus-tui/src/widgets/checkbox.rs | 82 ---- packages/dioxus-tui/src/widgets/input.rs | 102 ---- packages/dioxus-tui/src/widgets/mod.rs | 22 - packages/dioxus-tui/src/widgets/number.rs | 209 -------- packages/dioxus-tui/src/widgets/password.rs | 193 -------- packages/dioxus-tui/src/widgets/slider.rs | 107 ---- packages/dioxus-tui/src/widgets/textbox.rs | 182 ------- packages/native-core/src/node_ref.rs | 27 +- packages/native-core/src/node_watcher.rs | 18 +- packages/native-core/src/real_dom.rs | 108 +++- packages/native-core/src/utils/mod.rs | 1 + .../src/utils/persistant_iterator.rs | 4 +- .../native-core/src/utils/widget_watcher.rs | 136 +++++ packages/rink/examples/counter.rs | 3 +- packages/rink/examples/grid.rs | 3 +- packages/rink/examples/widgets.rs | 104 ++++ packages/rink/src/hooks.rs | 115 +++-- packages/rink/src/lib.rs | 77 ++- packages/rink/src/prelude/mod.rs | 2 - packages/rink/src/query.rs | 53 +- packages/rink/src/widgets/button.rs | 204 ++++++++ packages/rink/src/widgets/checkbox.rs | 250 ++++++++++ packages/rink/src/widgets/input.rs | 132 +++++ packages/rink/src/widgets/mod.rs | 95 ++++ packages/rink/src/widgets/number.rs | 464 ++++++++++++++++++ packages/rink/src/widgets/password.rs | 432 ++++++++++++++++ packages/rink/src/widgets/slider.rs | 458 +++++++++++++++++ packages/rink/src/widgets/textbox.rs | 432 ++++++++++++++++ 32 files changed, 3023 insertions(+), 1091 deletions(-) delete mode 100644 packages/dioxus-tui/src/prelude/mod.rs delete mode 100644 packages/dioxus-tui/src/widgets/button.rs delete mode 100644 packages/dioxus-tui/src/widgets/checkbox.rs delete mode 100644 packages/dioxus-tui/src/widgets/input.rs delete mode 100644 packages/dioxus-tui/src/widgets/mod.rs delete mode 100644 packages/dioxus-tui/src/widgets/number.rs delete mode 100644 packages/dioxus-tui/src/widgets/password.rs delete mode 100644 packages/dioxus-tui/src/widgets/slider.rs delete mode 100644 packages/dioxus-tui/src/widgets/textbox.rs create mode 100644 packages/native-core/src/utils/widget_watcher.rs create mode 100644 packages/rink/examples/widgets.rs delete mode 100644 packages/rink/src/prelude/mod.rs create mode 100644 packages/rink/src/widgets/button.rs create mode 100644 packages/rink/src/widgets/checkbox.rs create mode 100644 packages/rink/src/widgets/input.rs create mode 100644 packages/rink/src/widgets/mod.rs create mode 100644 packages/rink/src/widgets/number.rs create mode 100644 packages/rink/src/widgets/password.rs create mode 100644 packages/rink/src/widgets/slider.rs create mode 100644 packages/rink/src/widgets/textbox.rs diff --git a/packages/dioxus-tui/examples/widgets.rs b/packages/dioxus-tui/examples/widgets.rs index 04690522c..b5df35388 100644 --- a/packages/dioxus-tui/examples/widgets.rs +++ b/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::().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%", diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index 296fd4697..4f067b024 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -1,6 +1,3 @@ -pub mod prelude; -pub mod widgets; - use std::{ ops::Deref, rc::Rc, @@ -8,7 +5,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::*; @@ -83,13 +79,14 @@ impl Driver for DioxusRenderer { rdom: &Arc>, id: NodeId, event: &str, - value: Rc, + value: Rc, 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); } } diff --git a/packages/dioxus-tui/src/prelude/mod.rs b/packages/dioxus-tui/src/prelude/mod.rs deleted file mode 100644 index cb4fdee65..000000000 --- a/packages/dioxus-tui/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use crate::widgets::*; -pub use rink::Config; diff --git a/packages/dioxus-tui/src/widgets/button.rs b/packages/dioxus-tui/src/widgets/button.rs deleted file mode 100644 index a6578905e..000000000 --- a/packages/dioxus-tui/src/widgets/button.rs +++ /dev/null @@ -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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/checkbox.rs b/packages/dioxus-tui/src/widgets/checkbox.rs deleted file mode 100644 index 0b5ebf671..000000000 --- a/packages/dioxus-tui/src/widgets/checkbox.rs +++ /dev/null @@ -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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/input.rs b/packages/dioxus-tui/src/widgets/input.rs deleted file mode 100644 index 2294b0f10..000000000 --- a/packages/dioxus-tui/src/widgets/input.rs +++ /dev/null @@ -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>, - onclick: Option>, - 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, - } - } - } - }) -} diff --git a/packages/dioxus-tui/src/widgets/mod.rs b/packages/dioxus-tui/src/widgets/mod.rs deleted file mode 100644 index 18e3b12b4..000000000 --- a/packages/dioxus-tui/src/widgets/mod.rs +++ /dev/null @@ -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(cx: Scope) -> Option { - 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 - } -} diff --git a/packages/dioxus-tui/src/widgets/number.rs b/packages/dioxus-tui/src/widgets/number.rs deleted file mode 100644 index f82732981..000000000 --- a/packages/dioxus-tui/src/widgets/number.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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::().unwrap_or(0.0) + 1.0).to_string(); - update(text.clone()); - }; - let decrease = move || { - let mut text = text_ref.write(); - *text = (text.parse::().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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/password.rs b/packages/dioxus-tui/src/widgets/password.rs deleted file mode 100644 index 07491c140..000000000 --- a/packages/dioxus-tui/src/widgets/password.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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}" - } - } -} diff --git a/packages/dioxus-tui/src/widgets/slider.rs b/packages/dioxus-tui/src/widgets/slider.rs deleted file mode 100644 index 9e8d63cb9..000000000 --- a/packages/dioxus-tui/src/widgets/slider.rs +++ /dev/null @@ -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 = 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)", - } - } - } -} diff --git a/packages/dioxus-tui/src/widgets/textbox.rs b/packages/dioxus-tui/src/widgets/textbox.rs deleted file mode 100644 index fcfaf0a90..000000000 --- a/packages/dioxus-tui/src/widgets/textbox.rs +++ /dev/null @@ -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::().ok()) - .filter(|w| *w < 3) - .is_some() - || height - .strip_suffix("px") - .and_then(|h| h.parse::().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}" - } - } -} diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index 22ab5d8f1..a1a309622 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -126,7 +126,7 @@ impl AttributeMask { pub fn union(&self, other: &Self) -> Self { match (self, other) { (AttributeMask::Some(s), AttributeMask::Some(o)) => { - AttributeMask::Some(s.intersection(o).cloned().collect()) + AttributeMask::Some(s.union(o).cloned().collect()) } _ => AttributeMask::All, } @@ -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. diff --git a/packages/native-core/src/node_watcher.rs b/packages/native-core/src/node_watcher.rs index 27a449ef9..d437b158c 100644 --- a/packages/native-core/src/node_watcher.rs +++ b/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 { /// Called after a node is added to the tree. - fn on_node_added(&self, _node: NodeMut) {} + fn on_node_added(&mut self, _node: NodeMut) {} /// Called before a node is removed from the tree. - fn on_node_removed(&self, _node: NodeMut) {} + fn on_node_removed(&mut self, _node: NodeMut) {} /// Called after a node is moved to a new parent. - fn on_node_moved(&self, _node: NodeMut) {} - // /// Called after the text content of a node is changed. - // fn on_text_changed(&self, _node: NodeMut) {} - // /// Called after an attribute of an element is changed. - // fn on_attribute_changed(&self, _node: NodeMut, attribute: &str) {} + fn on_node_moved(&mut self, _node: NodeMut) {} +} + +/// A trait for watching for changes to attributes of an element. +pub trait AttributeWatcher { + /// Called before update_state is called on the RealDom + fn on_attributes_changed(&self, _node: NodeMut, _attributes: &AttributeMask) {} } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 1deec7d64..7d318ddc1 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -14,7 +14,7 @@ use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; use crate::node_ref::{NodeMask, NodeMaskBuilder}; -use crate::node_watcher::NodeWatcher; +use crate::node_watcher::{AttributeWatcher, NodeWatcher}; use crate::passes::{DirtyNodeStates, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; @@ -49,6 +49,7 @@ impl Deref for DirtyNodesResult { pub(crate) struct NodesDirty { passes_updated: FxHashMap>, nodes_updated: FxHashMap, + nodes_created: FxHashSet, pub(crate) passes: Box<[TypeErasedState]>, } @@ -90,6 +91,7 @@ impl NodesDirty { } type NodeWatchers = Arc + Send + Sync>>>>; +type AttributeWatchers = Arc + 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. @@ -106,6 +108,7 @@ pub struct RealDom { nodes_listening: FxHashMap>, pub(crate) dirty_nodes: NodesDirty, node_watchers: NodeWatchers, + attribute_watchers: AttributeWatchers, workload: ScheduledWorkload, root_id: NodeId, phantom: std::marker::PhantomData, @@ -159,8 +162,10 @@ impl RealDom { 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, phantom: std::marker::PhantomData, @@ -181,15 +186,16 @@ impl RealDom { pub fn create_node(&mut self, node: impl Into>) -> NodeMut<'_, V> { let id = self.world.add_entity(node.into()); self.tree_mut().create_node(id); + self.dirty_nodes .passes_updated .entry(id) .or_default() .extend(self.dirty_nodes.passes.iter().map(|x| x.this_type_id)); - let watchers = self.node_watchers.clone(); - for watcher in &*watchers.read().unwrap() { - watcher.on_node_added(NodeMut::new(id, self)); - } + self.dirty_nodes + .mark_dirty(id, NodeMaskBuilder::ALL.build()); + self.dirty_nodes.nodes_created.insert(id); + NodeMut::new(id, self) } @@ -216,16 +222,19 @@ impl RealDom { self.root_id } + /// Check if a node exists in the dom. + pub fn contains(&self, id: NodeId) -> bool { + self.tree_ref().contains(id) + } + /// Get a reference to a node. pub fn get(&self, id: NodeId) -> Option> { - self.tree_ref() - .contains(id) - .then_some(NodeRef { id, dom: self }) + self.contains(id).then_some(NodeRef { id, dom: self }) } /// Get a mutable reference to a node. pub fn get_mut(&mut self, id: NodeId) -> Option> { - let contains = self.tree_ref().contains(id); + let contains = self.contains(id); contains.then(|| NodeMut::new(id, self)) } @@ -247,8 +256,41 @@ impl RealDom { &mut self, ctx: SendAnyMap, ) -> (FxDashSet, FxHashMap) { + 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); + + // call attribute watchers + for (node_id, mask) in &nodes_updated { + if self.contains(*node_id) { + // 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(), + ); + } + }; + } + } + let dirty_nodes = DirtyNodeStates::with_passes(self.dirty_nodes.passes.iter().map(|p| p.this_type_id)); let tree = self.tree_ref(); @@ -341,6 +383,17 @@ impl RealDom { 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 + '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 @@ -399,7 +452,7 @@ impl<'a, V: Component + Send + Sync> DerefMut for ViewEntr } /// A immutable view of a node -pub trait NodeImmutable: Sized { +pub trait NodeImmutable: Sized { /// Get the real dom this node was created in fn real_dom(&self) -> &RealDom; @@ -573,7 +626,9 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { .or_default() .insert(TypeId::of::()); let view_mut: ViewMut = self.dom.borrow_raw().ok()?; - Some(ViewEntryMut::new(view_mut, self.id)) + view_mut + .contains(self.id) + .then_some(ViewEntryMut::new(view_mut, self.id)) } /// Insert a custom component into this node @@ -684,7 +739,8 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { for child in children_ids_vec { self.dom.get_mut(child).unwrap().remove(); } - self.dom.tree_mut().remove_single(id); + self.dom.tree_mut().remove(id); + self.real_dom_mut().raw_world_mut().delete_entity(id); } /// Replace this node with a different node @@ -758,7 +814,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)); } } @@ -766,9 +822,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 @@ -885,6 +944,15 @@ pub struct ElementNodeMut<'a, V: FromAnyValue + Send + Sync = ()> { dirty_nodes: &'a mut NodesDirty, } +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 ElementNodeMut<'_, V> { /// Get the current element fn element(&self) -> &ElementNode { @@ -976,6 +1044,14 @@ impl ElementNodeMut<'_, V> { self.element_mut().attributes.get_mut(name) } + /// Get an attribute of the element + pub fn get_attribute( + &self, + name: &OwnedAttributeDiscription, + ) -> Option<&OwnedAttributeValue> { + self.element().attributes.get(name) + } + /// Get the set of all events the element is listening to pub fn listeners(&self) -> &FxHashSet { &self.element().listeners diff --git a/packages/native-core/src/utils/mod.rs b/packages/native-core/src/utils/mod.rs index a78b5310b..95ccc35be 100644 --- a/packages/native-core/src/utils/mod.rs +++ b/packages/native-core/src/utils/mod.rs @@ -5,3 +5,4 @@ mod persistant_iterator; pub use persistant_iterator::*; pub mod cursor; +pub mod widget_watcher; diff --git a/packages/native-core/src/utils/persistant_iterator.rs b/packages/native-core/src/utils/persistant_iterator.rs index 96c2cffb7..5d4902b87 100644 --- a/packages/native-core/src/utils/persistant_iterator.rs +++ b/packages/native-core/src/utils/persistant_iterator.rs @@ -60,7 +60,7 @@ struct PersistantElementIterUpdater { } impl NodeWatcher for PersistantElementIterUpdater { - fn on_node_moved(&self, node: NodeMut) { + fn on_node_moved(&mut self, node: NodeMut) { // 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 NodeWatcher for PersistantElementIterUpda } } - fn on_node_removed(&self, node: NodeMut) { + fn on_node_removed(&mut self, node: NodeMut) { // 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(); diff --git a/packages/native-core/src/utils/widget_watcher.rs b/packages/native-core/src/utils/widget_watcher.rs new file mode 100644 index 000000000..c6836697b --- /dev/null +++ b/packages/native-core/src/utils/widget_watcher.rs @@ -0,0 +1,136 @@ +//! Widget utilities for defining, registering and updating widgets + +use std::sync::{Arc, RwLock}; + +use rustc_hash::FxHashMap; +use shipyard::Component; + +use crate::{ + node::{FromAnyValue, NodeType}, + node_ref::AttributeMask, + node_watcher::{AttributeWatcher, NodeWatcher}, + prelude::{NodeImmutable, NodeMut, RealDom}, + NodeId, +}; + +/// A watcher that handlers registering and updating widgets +#[derive(Default, Clone)] +pub struct WidgetWatcher { + inner: Arc>>, +} + +impl NodeWatcher for WidgetWatcher { + fn on_node_added(&mut self, node: NodeMut) { + let mut inner = self.inner.write().unwrap(); + inner.on_node_added(node); + } + + fn on_node_removed(&mut self, node: NodeMut) { + let mut inner = self.inner.write().unwrap(); + inner.on_node_removed(node); + } +} + +impl WidgetWatcher { + /// Register a widget + pub fn register_widget + 'static, O: WidgetUpdater>(&mut self) { + let mut inner = self.inner.write().unwrap(); + inner.builders.insert( + W::NAME, + WidgetBuilder { + create: |mut node| Box::new(W::create(&mut node)), + }, + ); + } + + /// Attach the widget watcher to the RealDom + pub fn attach(&self, dom: &mut RealDom) { + dom.add_node_watcher(self.clone()); + dom.add_attribute_watcher(self.clone()); + } +} + +impl AttributeWatcher for WidgetWatcher { + fn on_attributes_changed(&self, node: NodeMut, attributes: &AttributeMask) { + let mut inner = self.inner.write().unwrap(); + if let Some(widget) = inner.widgets.get_mut(&node.id()) { + widget.dyn_widget.attributes_changed(node, attributes); + } + } +} + +#[derive(Default)] +struct WidgetWatcherInner { + builders: FxHashMap<&'static str, WidgetBuilder>, + widgets: FxHashMap>, +} + +impl NodeWatcher for WidgetWatcherInner { + fn on_node_added(&mut self, node: NodeMut) { + let node_type = node.node_type(); + if let NodeType::Element(el) = &*node_type { + if let Some(builder) = self.builders.get(el.tag.as_str()) { + drop(node_type); + let id = node.id(); + let widget = (builder.create)(node); + self.widgets.insert(id, BoxedWidget { dyn_widget: widget }); + } + } + } + + fn on_node_removed(&mut self, node: NodeMut) { + self.widgets.remove(&node.id()); + } +} + +#[derive(Component)] +struct BoxedWidget { + dyn_widget: Box>, +} + +struct WidgetBuilder { + create: fn(NodeMut) -> Box>, +} + +/// A controlled element (a.k.a. widget) +pub trait Widget: Send + Sync + 'static { + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget. + fn create(root: &mut NodeMut) -> Self; + + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); +} + +/// A factory for creating widgets +pub trait WidgetFactory, V: FromAnyValue + Send + Sync = ()>: + Send + Sync + 'static +{ + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget. + fn create(root: &mut NodeMut) -> W; +} + +impl, V: FromAnyValue + Send + Sync> WidgetFactory for W { + const NAME: &'static str = W::NAME; + + fn create(root: &mut NodeMut) -> Self { + W::create(root) + } +} + +/// A trait for updating widgets +pub trait WidgetUpdater: Send + Sync + 'static { + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); +} + +impl, V: FromAnyValue + Send + Sync> WidgetUpdater for W { + fn attributes_changed(&mut self, root: NodeMut, attributes: &AttributeMask) { + self.attributes_changed(root, attributes); + } +} diff --git a/packages/rink/examples/counter.rs b/packages/rink/examples/counter.rs index 4d2428d8a..7bf52a53e 100644 --- a/packages/rink/examples/counter.rs +++ b/packages/rink/examples/counter.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}; diff --git a/packages/rink/examples/grid.rs b/packages/rink/examples/grid.rs index 7abc4669f..ab4449066 100644 --- a/packages/rink/examples/grid.rs +++ b/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}; diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs new file mode 100644 index 000000000..3e709313f --- /dev/null +++ b/packages/rink/examples/widgets.rs @@ -0,0 +1,104 @@ +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>) { + // 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>, + _: NodeId, + event_type: &str, + event: Rc, + _: bool, + ) { + match event_type { + "oninput" => { + // when the button is clicked, increment the counter + if let EventData::Form(input_event) = &*event { + if let Ok(value) = input_event.value.parse::() { + self.count = value; + } + } + } + _ => {} + } + } + + fn poll_async(&mut self) -> std::pin::Pin + '_>> { + 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(); +} diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index fe29b4120..341ab02ac 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -12,7 +12,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, @@ -25,13 +27,62 @@ use crate::focus::{Focus, Focused}; use crate::layout::TaffyLayout; use crate::{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, + 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 { + 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, + + pub files: Option, +} + +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, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct File {} + type EventCore = (&'static str, EventData); const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); @@ -147,13 +198,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"), }); } @@ -161,7 +212,7 @@ impl InnerInputState { resolved_events.push(Event { name: "focusout", id, - data: Rc::new(EventData::Focus(FocusData {})), + data: EventData::Focus(FocusData {}), bubbles: event_bubbles("focusout"), }); } @@ -197,7 +248,7 @@ impl InnerInputState { fn try_create_event( name: &'static str, - data: Rc, + data: EventData, will_bubble: &mut FxHashSet, resolved_events: &mut Vec, node: NodeRef, @@ -281,10 +332,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, @@ -308,7 +356,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, @@ -331,10 +379,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, @@ -354,10 +399,7 @@ impl InnerInputState { 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, @@ -378,10 +420,7 @@ impl InnerInputState { 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, @@ -403,10 +442,7 @@ impl InnerInputState { 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, @@ -429,10 +465,7 @@ impl InnerInputState { 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, @@ -456,7 +489,7 @@ impl InnerInputState { if currently_contains { try_create_event( "wheel", - Rc::new(EventData::Wheel(w.clone())), + EventData::Wheel(w.clone()), &mut will_bubble, resolved_events, node, @@ -481,10 +514,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, @@ -507,10 +537,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, @@ -627,12 +654,12 @@ impl RinkInputHandler { }) .map(|evt| (evt.0, evt.1)); - let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); + let mut hm: FxHashMap<&'static str, Vec> = 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 { diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 9fceb6b48..2aab01664 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -6,7 +6,6 @@ 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::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap}; use focus::FocusState; @@ -25,18 +24,19 @@ use taffy::Taffy; pub use taffy::{geometry::Point, prelude::*}; use tokio::select; use tui::{backend::CrosstermBackend, layout::Rect, 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,18 +91,24 @@ pub fn render( 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 || { let tick_rate = Duration::from_millis(1000); 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; } } @@ -110,10 +116,21 @@ pub fn render( }); } + 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(); @@ -213,6 +230,7 @@ pub fn render( } } + let mut event_recieved = None; { let wait = renderer.poll_async(); @@ -222,7 +240,7 @@ pub fn render( _ = wait => { }, - evt = event_reciever.next() => { + evt = raw_event_reciever.next() => { match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -243,21 +261,32 @@ pub fn render( 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 @@ -309,3 +338,23 @@ pub trait Driver { ); fn poll_async(&mut self) -> Pin + '_>>; } + +/// 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(rdom.get_mut(id).unwrap()); + + while let Some(mut node_mut) = node { + let parent_id = node_mut.parent_id(); + if let Some(mut widget) = node_mut + .get_mut::() + .map(|w| w.clone()) + { + widget.handle_event(event, &mut node_mut) + } + if !event.bubbles { + break; + } + node = parent_id.map(|id| rdom.get_mut(id).unwrap()); + } +} diff --git a/packages/rink/src/prelude/mod.rs b/packages/rink/src/prelude/mod.rs deleted file mode 100644 index f7bbb3696..000000000 --- a/packages/rink/src/prelude/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "dioxus-bindings")] -pub use crate::widgets::*; diff --git a/packages/rink/src/query.rs b/packages/rink/src/query.rs index 1cf113310..c204e1953 100644 --- a/packages/rink/src/query.rs +++ b/packages/rink/src/query.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard}; use dioxus_native_core::prelude::*; +use shipyard::Unique; use taffy::{ geometry::Point, prelude::{Layout, Size}, @@ -40,7 +41,7 @@ use crate::{layout::TaffyLayout, layout_to_screen_space}; /// }) /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Unique)] pub struct Query { pub(crate) rdom: Arc>, pub(crate) stretch: Arc>, @@ -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,25 +90,33 @@ impl<'a> ElementRef<'a> { } pub fn layout(&self) -> Option { - let layout = self - .stretch - .layout( - self.inner - .get(self.id) - .unwrap() - .get::() - .unwrap() - .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 { + let layout = stretch + .layout(node.get::().unwrap().node.ok()?) + .ok()?; + + let mut current_node_id = node.parent_id(); + let mut pos = layout.location; + let rdom = node.real_dom(); + while let Some(node) = current_node_id.and_then(|id| rdom.get(id)) { + let current_layout = stretch + .layout(node.get::().unwrap().node.ok()?) + .ok()?; + pos.x += current_layout.location.x; + pos.y += current_layout.location.y; + current_node_id = node.parent_id(); + } + + 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(), + }, + }) +} diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs new file mode 100644 index 000000000..2eaddfe59 --- /dev/null +++ b/packages/rink/src/widgets/button.rs @@ -0,0 +1,204 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeTypeMut, RealDom}, + utils::widget_watcher::Widget, + NodeId, +}; +use shipyard::UniqueViewMut; + +use crate::FormData; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Button { + div_id: NodeId, + 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) { + let data = FormData { + value: self.value.to_string(), + values: HashMap::new(), + files: None, + }; + ctx.send(crate::Event { + id: self.div_id, + name: "input", + data: crate::EventData::Form(data), + bubbles: true, + }); + } +} + +impl Widget for Button { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + let div_id = root.id(); + root.add_child(text_id); + + Self { + div_id, + 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, + node: &mut dioxus_native_core::real_dom::NodeMut, + ) { + let mut ctx: UniqueViewMut = node + .real_dom_mut() + .raw_world_mut() + .borrow() + .expect("expected widget context"); + + match event.name { + "click" => self.switch(&mut ctx), + "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); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs new file mode 100644 index 000000000..943db5213 --- /dev/null +++ b/packages/rink/src/widgets/checkbox.rs @@ -0,0 +1,250 @@ +use std::collections::HashMap; + +use dioxus_html::input_data::keyboard_types::Key; +use dioxus_native_core::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::NodeType, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut}, + utils::widget_watcher::Widget, + 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, root: &mut 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, node: &mut 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 = 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 Widget for CheckBox { + const NAME: &'static str = "input"; + + fn create(root: &mut 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(); + root.add_child(text_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(&mut 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(&mut root); + } + } + } + } +} + +impl RinkWidget for CheckBox { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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); + } + } + } + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs new file mode 100644 index 000000000..2253ae096 --- /dev/null +++ b/packages/rink/src/widgets/input.rs @@ -0,0 +1,132 @@ +use dioxus_native_core::{ + node::OwnedAttributeDiscription, prelude::NodeType, real_dom::NodeImmutable, + utils::widget_watcher::Widget, +}; + +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 Widget for Input { + const NAME: &'static str = "input"; + + fn create(root: &mut 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 { + 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: &mut 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); + } + } + } +} diff --git a/packages/rink/src/widgets/mod.rs b/packages/rink/src/widgets/mod.rs new file mode 100644 index 000000000..283ebbcd9 --- /dev/null +++ b/packages/rink/src/widgets/mod.rs @@ -0,0 +1,95 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod textbox; + +use std::sync::{Arc, RwLock}; + +use dioxus_native_core::{ + real_dom::RealDom, + utils::widget_watcher::{Widget, WidgetFactory, WidgetUpdater, WidgetWatcher}, +}; +use futures_channel::mpsc::UnboundedSender; +use shipyard::{Component, Unique}; + +use crate::Event; + +pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender) { + // inject the widget context + rdom.raw_world().add_unique(WidgetContext { sender }); + + // create the widget watcher + let mut widget_watcher = WidgetWatcher::default(); + + widget_watcher + .register_widget::, RinkWidgetTraitObject>(); + + widget_watcher.attach(rdom); +} + +trait RinkWidget: Sync + Send + Widget + 'static { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut); +} + +pub trait RinkWidgetResponder: WidgetUpdater { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut); +} + +impl RinkWidgetResponder for W { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + RinkWidget::handle_event(self, event, node) + } +} + +struct RinkWidgetTraitObjectFactory { + _marker: std::marker::PhantomData, +} + +impl WidgetFactory for RinkWidgetTraitObjectFactory { + const NAME: &'static str = W::NAME; + + fn create(node: &mut dioxus_native_core::real_dom::NodeMut) -> RinkWidgetTraitObject { + let myself = RinkWidgetTraitObject { + widget: Arc::new(RwLock::new(W::create(node))), + }; + node.insert(myself.clone()); + myself + } +} + +#[derive(Clone, Component)] +pub(crate) struct RinkWidgetTraitObject { + widget: Arc>, +} + +impl WidgetUpdater for RinkWidgetTraitObject { + fn attributes_changed( + &mut self, + root: dioxus_native_core::real_dom::NodeMut, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + let mut widget = self.widget.write().unwrap(); + widget.attributes_changed(root, attributes); + } +} + +impl RinkWidgetResponder for RinkWidgetTraitObject { + fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + let mut widget = self.widget.write().unwrap(); + widget.handle_event(event, node); + } +} + +#[derive(Unique)] +pub(crate) struct WidgetContext { + sender: UnboundedSender, +} + +impl WidgetContext { + pub(crate) fn send(&self, event: Event) { + self.sender.unbounded_send(event).unwrap(); + } +} diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs new file mode 100644 index 000000000..2925e2966 --- /dev/null +++ b/packages/rink/src/widgets/number.rs @@ -0,0 +1,464 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Number { + 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, +} + +impl Number { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = text_before_first_cursor.to_string(); + } + + 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() = text_highlighted.to_string(); + } + + 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() = text_after_second_cursor.to_string(); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn increase(&mut self) { + let num = self.text.parse::().unwrap_or(0.0); + self.text = (num + 1.0).to_string(); + } + + fn decrease(&mut self) { + let num = self.text.parse::().unwrap_or(0.0); + self.text = (num - 1.0).to_string(); + } + + fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + let key = data.key(); + let is_text = 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_text { + 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 = 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(); + } + } else { + match key { + Key::ArrowUp => { + self.increase(); + } + Key::ArrowDown => { + self.decrease(); + } + _ => (), + } + + let id = root.id(); + let rdom = root.real_dom_mut(); + self.write_value(rdom, id); + } + } + + fn handle_mousemove(&mut self, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for Number { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for Number { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/password.rs b/packages/rink/src/widgets/password.rs new file mode 100644 index 000000000..483b1e175 --- /dev/null +++ b/packages/rink/src/widgets/password.rs @@ -0,0 +1,432 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct Password { + 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, +} + +impl Password { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = ".".repeat(text_before_first_cursor.len()); + } + + 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() = ".".repeat(text_highlighted.len()); + } + + 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() = ".".repeat(text_after_second_cursor.len()); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, root: &mut 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 = 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, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for Password { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for Password { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs new file mode 100644 index 000000000..e1fa4f980 --- /dev/null +++ b/packages/rink/src/widgets/slider.rs @@ -0,0 +1,458 @@ +use std::collections::HashMap; + +use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_native_core::{ + node::{OwnedAttributeDiscription, OwnedAttributeValue}, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::widget_watcher::Widget, + 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, + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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 = 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, root: &mut 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, root: &mut 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 = 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 Widget for Slider { + const NAME: &'static str = "input"; + + fn create(root: &mut 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); + + div_wrapper.add_event_listener("mousemove"); + div_wrapper.add_event_listener("mousedown"); + root.add_event_listener("keydown"); + + root.add_child(div_wrapper_id); + + 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: &mut 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); + } + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/textbox.rs b/packages/rink/src/widgets/textbox.rs new file mode 100644 index 000000000..16f73fd78 --- /dev/null +++ b/packages/rink/src/widgets/textbox.rs @@ -0,0 +1,432 @@ +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::{ + node::OwnedAttributeDiscription, + node_ref::AttributeMask, + prelude::{ElementNode, NodeType}, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, + utils::{ + cursor::{Cursor, Pos}, + widget_watcher::Widget, + }, + NodeId, +}; +use shipyard::UniqueView; +use taffy::geometry::Point; + +use crate::{query::get_layout, Event, EventData, FormData, Query}; + +use super::{RinkWidget, WidgetContext}; + +#[derive(Debug, Default)] +pub(crate) struct TextBox { + 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, +} + +impl TextBox { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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; + } + } + + 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() = text_before_first_cursor.to_string(); + } + + 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() = text_highlighted.to_string(); + } + + 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() = text_after_second_cursor.to_string(); + } + + // send the event + { + let world = rdom.raw_world_mut(); + let data = FormData { + value: self.text.clone(), + values: HashMap::new(), + files: None, + }; + let ctx: UniqueView = world.borrow().expect("expected widget context"); + + ctx.send(Event { + id, + name: "input", + data: EventData::Form(data), + bubbles: true, + }); + } + } + + fn handle_keydown(&mut self, root: &mut 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 = 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, root: &mut 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); + if self.border { + new.col = new.col.saturating_sub(1); + } + // 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, root: &mut NodeMut, data: &MouseData) { + let offset = data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if self.border { + new.col = new.col.saturating_sub(1); + } + + // 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 = 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 Widget for TextBox { + const NAME: &'static str = "input"; + + fn create(root: &mut 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"); + + root.add_child(div_wrapper_id); + + 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 RinkWidget for TextBox { + fn handle_event( + &mut self, + event: &crate::Event, + node: &mut 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_mousedown(node, data); + } + } + + "mouseup" => { + self.dragging = false; + } + + "mouseleave" => { + self.dragging = false; + } + + "mouseenter" => { + self.dragging = false; + } + + "focusout" => { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + } + + _ => {} + } + } +} From b2c87f5c47a1cdf5269bd2b2d2518cd263a52161 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 6 Apr 2023 16:59:25 -0500 Subject: [PATCH 03/35] create scafolding for the shadow dom --- packages/native-core/src/custom_element.rs | 134 +++++++++++++++++ packages/native-core/src/dioxus.rs | 1 + packages/native-core/src/lib.rs | 3 + packages/native-core/src/node.rs | 6 + packages/native-core/src/passes.rs | 3 + packages/native-core/src/real_dom.rs | 12 ++ packages/native-core/src/shadow_dom.rs | 26 ++++ packages/native-core/src/utils/mod.rs | 1 - .../native-core/src/utils/widget_watcher.rs | 136 ------------------ packages/native-core/tests/passes.rs | 1 + 10 files changed, 186 insertions(+), 137 deletions(-) create mode 100644 packages/native-core/src/custom_element.rs create mode 100644 packages/native-core/src/shadow_dom.rs delete mode 100644 packages/native-core/src/utils/widget_watcher.rs diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs new file mode 100644 index 000000000..8f6142076 --- /dev/null +++ b/packages/native-core/src/custom_element.rs @@ -0,0 +1,134 @@ +use std::{ + cell::RefCell, + sync::{Arc, RwLock}, +}; + +use rustc_hash::FxHashMap; +use shipyard::Component; + +use crate::{ + node::{FromAnyValue, NodeType}, + node_ref::AttributeMask, + prelude::{NodeImmutable, NodeMut, RealDom}, + real_dom::NodeTypeMut, + shadow_dom::ShadowDom, + NodeId, +}; + +#[derive(Default)] +pub(crate) struct CustomElementRegistry { + builders: FxHashMap<&'static str, CustomElementBuilder>, +} + +impl CustomElementRegistry { + pub fn register>(&mut self) { + self.builders.insert( + W::NAME, + CustomElementBuilder { + create: |dom, light_root_id| Box::new(W::create(dom, light_root_id)), + }, + ); + } + + pub fn add_shadow_dom(&self, mut node: NodeMut) { + let element_tag = if let NodeType::Element(el) = &*node.node_type() { + Some(el.tag.clone()) + } else { + None + }; + if let Some(element_tag) = element_tag { + if let Some(builder) = self.builders.get(element_tag.as_str()) { + let boxed_widget = { + let light_root_id = node.id(); + let dom = node.real_dom_mut(); + (builder.create)(dom, light_root_id) + }; + + let boxed_widget = CustomElementManager { + inner: Arc::new(RwLock::new(boxed_widget)), + }; + + let NodeTypeMut::Element(mut el) = node.node_type_mut()else{ + panic!("The type of the light element should not change when creating a shadow DOM") + }; + + *el.shadow_root_mut() = Some(ShadowDom::new(boxed_widget).into()); + } + } + } +} + +struct CustomElementBuilder { + create: fn(&mut RealDom, NodeId) -> Box>, +} + +/// A controlled element that renders to a shadow DOM +pub trait CustomElement: Send + Sync + 'static { + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget without mounting it. + fn create(dom: &mut RealDom, light_root_id: NodeId) -> Self; + + /// The root node of the widget. + fn root(&self) -> NodeId; + + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); +} + +/// A factory for creating widgets +trait ElementFactory, V: FromAnyValue + Send + Sync = ()>: + Send + Sync + 'static +{ + /// The tag the widget is registered under. + const NAME: &'static str; + + /// Create a new widget. + fn create(dom: &mut RealDom, light_root_id: NodeId) -> W; +} + +impl, V: FromAnyValue + Send + Sync> ElementFactory for W { + const NAME: &'static str = W::NAME; + + fn create(dom: &mut RealDom, light_root_id: NodeId) -> Self { + Self::create(dom, light_root_id) + } +} + +/// A trait for updating widgets +trait CustomElementUpdater: Send + Sync + 'static { + /// Called when the attributes of the widget are changed. + fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); + + /// The root node of the widget. + fn root(&self) -> NodeId; +} + +impl, V: FromAnyValue + Send + Sync> CustomElementUpdater for W { + fn attributes_changed(&mut self, root: &mut RealDom, attributes: &AttributeMask) { + self.attributes_changed(root, attributes); + } + + fn root(&self) -> NodeId { + self.root() + } +} + +pub struct CustomElementManager { + inner: Arc>>>, +} + +impl CustomElementManager { + pub fn root(&self) -> NodeId { + self.inner.read().unwrap().root() + } +} + +impl Clone for CustomElementManager { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} diff --git a/packages/native-core/src/dioxus.rs b/packages/native-core/src/dioxus.rs index 3e9ff9ee0..9d50b1224 100644 --- a/packages/native-core/src/dioxus.rs +++ b/packages/native-core/src/dioxus.rs @@ -262,6 +262,7 @@ fn create_template_node( }) .collect(), listeners: FxHashSet::default(), + shadow: None, }); let node_id = rdom.create_node(node).id(); for child in *children { diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index ca8e8e5ee..6dfeb81cd 100644 --- a/packages/native-core/src/lib.rs +++ b/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; pub mod layout_attributes; @@ -15,8 +16,10 @@ pub mod node_ref; pub mod node_watcher; mod passes; pub mod real_dom; +mod shadow_dom; pub mod tree; pub mod utils; + pub use shipyard::EntityId as NodeId; pub mod exports { diff --git a/packages/native-core/src/node.rs b/packages/native-core/src/node.rs index 42ff8229f..2cbfa3fb3 100644 --- a/packages/native-core/src/node.rs +++ b/packages/native-core/src/node.rs @@ -7,6 +7,8 @@ use std::{ fmt::{Debug, Display}, }; +use crate::shadow_dom::ShadowDom; + /// A element node in the RealDom #[derive(Debug, Clone, Default)] pub struct ElementNode { @@ -18,6 +20,9 @@ pub struct ElementNode { pub attributes: FxHashMap>, /// The events the element is listening for pub listeners: FxHashSet, + /// The shadow dom of the element + // This is a rare property, so it is stored in an `Box` to save space + pub shadow: Option>>, } impl ElementNode { @@ -28,6 +33,7 @@ impl ElementNode { namespace: namespace.into(), attributes: Default::default(), listeners: Default::default(), + shadow: Default::default(), } } } diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 2a84e6df4..8696b8a89 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -104,6 +104,9 @@ pub trait State: 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? + const TRAVERSE_SHADOW_DOM: bool = false; + /// Update this state in a node, returns if the state was updated fn update<'a>( &mut self, diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 7d318ddc1..9e6146319 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -17,6 +17,7 @@ use crate::node_ref::{NodeMask, NodeMaskBuilder}; use crate::node_watcher::{AttributeWatcher, NodeWatcher}; use crate::passes::{DirtyNodeStates, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; +use crate::shadow_dom::ShadowDom; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; use crate::NodeId; use crate::{FxDashSet, SendAnyMap}; @@ -139,6 +140,7 @@ impl RealDom { namespace: Some("Root".to_string()), attributes: FxHashMap::default(), listeners: FxHashSet::default(), + shadow: None, }); let root_id = world.add_entity(root_node); { @@ -1056,6 +1058,16 @@ impl ElementNodeMut<'_, V> { pub fn listeners(&self) -> &FxHashSet { &self.element().listeners } + + /// Get the shadow root of the element + pub fn shadow_root(&self) -> Option<&ShadowDom> { + self.element().shadow.as_deref() + } + + /// Get the shadow root of the element + pub fn shadow_root_mut(&mut self) -> &mut Option>> { + &mut self.element_mut().shadow + } } // Create a workload from all of the passes. This orders the passes so that each pass will only run at most once. diff --git a/packages/native-core/src/shadow_dom.rs b/packages/native-core/src/shadow_dom.rs new file mode 100644 index 000000000..262d6fbb6 --- /dev/null +++ b/packages/native-core/src/shadow_dom.rs @@ -0,0 +1,26 @@ +use std::fmt::Debug; + +use crate::{custom_element::CustomElementManager, node::FromAnyValue, prelude::NodeRef, NodeId}; + +#[derive(Clone)] +pub struct ShadowDom { + shadow_root: NodeId, + updater: CustomElementManager, +} + +impl ShadowDom { + pub fn new(updater: CustomElementManager) -> Self { + Self { + shadow_root: updater.root(), + updater, + } + } +} + +impl Debug for ShadowDom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ShadowDom") + .field("shadow_root", &self.shadow_root) + .finish() + } +} diff --git a/packages/native-core/src/utils/mod.rs b/packages/native-core/src/utils/mod.rs index 95ccc35be..a78b5310b 100644 --- a/packages/native-core/src/utils/mod.rs +++ b/packages/native-core/src/utils/mod.rs @@ -5,4 +5,3 @@ mod persistant_iterator; pub use persistant_iterator::*; pub mod cursor; -pub mod widget_watcher; diff --git a/packages/native-core/src/utils/widget_watcher.rs b/packages/native-core/src/utils/widget_watcher.rs deleted file mode 100644 index c6836697b..000000000 --- a/packages/native-core/src/utils/widget_watcher.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Widget utilities for defining, registering and updating widgets - -use std::sync::{Arc, RwLock}; - -use rustc_hash::FxHashMap; -use shipyard::Component; - -use crate::{ - node::{FromAnyValue, NodeType}, - node_ref::AttributeMask, - node_watcher::{AttributeWatcher, NodeWatcher}, - prelude::{NodeImmutable, NodeMut, RealDom}, - NodeId, -}; - -/// A watcher that handlers registering and updating widgets -#[derive(Default, Clone)] -pub struct WidgetWatcher { - inner: Arc>>, -} - -impl NodeWatcher for WidgetWatcher { - fn on_node_added(&mut self, node: NodeMut) { - let mut inner = self.inner.write().unwrap(); - inner.on_node_added(node); - } - - fn on_node_removed(&mut self, node: NodeMut) { - let mut inner = self.inner.write().unwrap(); - inner.on_node_removed(node); - } -} - -impl WidgetWatcher { - /// Register a widget - pub fn register_widget + 'static, O: WidgetUpdater>(&mut self) { - let mut inner = self.inner.write().unwrap(); - inner.builders.insert( - W::NAME, - WidgetBuilder { - create: |mut node| Box::new(W::create(&mut node)), - }, - ); - } - - /// Attach the widget watcher to the RealDom - pub fn attach(&self, dom: &mut RealDom) { - dom.add_node_watcher(self.clone()); - dom.add_attribute_watcher(self.clone()); - } -} - -impl AttributeWatcher for WidgetWatcher { - fn on_attributes_changed(&self, node: NodeMut, attributes: &AttributeMask) { - let mut inner = self.inner.write().unwrap(); - if let Some(widget) = inner.widgets.get_mut(&node.id()) { - widget.dyn_widget.attributes_changed(node, attributes); - } - } -} - -#[derive(Default)] -struct WidgetWatcherInner { - builders: FxHashMap<&'static str, WidgetBuilder>, - widgets: FxHashMap>, -} - -impl NodeWatcher for WidgetWatcherInner { - fn on_node_added(&mut self, node: NodeMut) { - let node_type = node.node_type(); - if let NodeType::Element(el) = &*node_type { - if let Some(builder) = self.builders.get(el.tag.as_str()) { - drop(node_type); - let id = node.id(); - let widget = (builder.create)(node); - self.widgets.insert(id, BoxedWidget { dyn_widget: widget }); - } - } - } - - fn on_node_removed(&mut self, node: NodeMut) { - self.widgets.remove(&node.id()); - } -} - -#[derive(Component)] -struct BoxedWidget { - dyn_widget: Box>, -} - -struct WidgetBuilder { - create: fn(NodeMut) -> Box>, -} - -/// A controlled element (a.k.a. widget) -pub trait Widget: Send + Sync + 'static { - /// The tag the widget is registered under. - const NAME: &'static str; - - /// Create a new widget. - fn create(root: &mut NodeMut) -> Self; - - /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); -} - -/// A factory for creating widgets -pub trait WidgetFactory, V: FromAnyValue + Send + Sync = ()>: - Send + Sync + 'static -{ - /// The tag the widget is registered under. - const NAME: &'static str; - - /// Create a new widget. - fn create(root: &mut NodeMut) -> W; -} - -impl, V: FromAnyValue + Send + Sync> WidgetFactory for W { - const NAME: &'static str = W::NAME; - - fn create(root: &mut NodeMut) -> Self { - W::create(root) - } -} - -/// A trait for updating widgets -pub trait WidgetUpdater: Send + Sync + 'static { - /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, _root: NodeMut, _attributes: &AttributeMask); -} - -impl, V: FromAnyValue + Send + Sync> WidgetUpdater for W { - fn attributes_changed(&mut self, root: NodeMut, attributes: &AttributeMask) { - self.attributes_changed(root, attributes); - } -} diff --git a/packages/native-core/tests/passes.rs b/packages/native-core/tests/passes.rs index 368333fb5..9a0fbe51d 100644 --- a/packages/native-core/tests/passes.rs +++ b/packages/native-core/tests/passes.rs @@ -10,6 +10,7 @@ fn create_blank_element() -> NodeType { namespace: None, attributes: FxHashMap::default(), listeners: FxHashSet::default(), + shadow: None, }) } From 874956bc503cfdba7c0425fb75e360ebce5674f9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 7 Apr 2023 11:11:46 -0500 Subject: [PATCH 04/35] intial implementation of shadow trees --- packages/native-core/src/custom_element.rs | 17 +- packages/native-core/src/passes.rs | 2 +- packages/native-core/src/real_dom.rs | 4 + packages/native-core/src/tree.rs | 213 ++++++++++++++++++--- 4 files changed, 199 insertions(+), 37 deletions(-) diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index 8f6142076..9b3a19c04 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -1,10 +1,6 @@ -use std::{ - cell::RefCell, - sync::{Arc, RwLock}, -}; +use std::sync::{Arc, RwLock}; use rustc_hash::FxHashMap; -use shipyard::Component; use crate::{ node::{FromAnyValue, NodeType}, @@ -15,11 +11,18 @@ use crate::{ NodeId, }; -#[derive(Default)] pub(crate) struct CustomElementRegistry { builders: FxHashMap<&'static str, CustomElementBuilder>, } +impl Default for CustomElementRegistry { + fn default() -> Self { + Self { + builders: FxHashMap::default(), + } + } +} + impl CustomElementRegistry { pub fn register>(&mut self) { self.builders.insert( @@ -99,7 +102,7 @@ impl, V: FromAnyValue + Send + Sync> ElementFactory fo /// A trait for updating widgets trait CustomElementUpdater: Send + Sync + 'static { /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); + fn attributes_changed(&mut self, dom: &mut RealDom, attributes: &AttributeMask); /// The root node of the widget. fn root(&self) -> NodeId; diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 8696b8a89..16d0f71fc 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -104,7 +104,7 @@ pub trait State: 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? + /// 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 diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 9e6146319..72d8bce9c 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -10,6 +10,7 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, RwLock}; +use crate::custom_element::CustomElementRegistry; use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; @@ -112,6 +113,7 @@ pub struct RealDom { attribute_watchers: AttributeWatchers, workload: ScheduledWorkload, root_id: NodeId, + custom_elements: CustomElementRegistry, phantom: std::marker::PhantomData, } @@ -170,6 +172,7 @@ impl RealDom { attribute_watchers: Default::default(), workload, root_id, + custom_elements: Default::default(), phantom: std::marker::PhantomData, } } @@ -759,6 +762,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 diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index df25deac3..b87b11ab5 100644 --- a/packages/native-core/src/tree.rs +++ b/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 subtree of a tree. +#[derive(PartialEq, Eq, Clone, Debug, Component)] +pub struct Subtree { + /// The root of the subtree + shadow_roots: Vec, + /// The node that children of the super tree should be inserted under. + slot: Option, + /// The node in the super tree that the subtree is attached to. + super_tree_root: NodeId, +} + /// A node in a tree. #[derive(PartialEq, Eq, Clone, Debug, Component)] pub struct Node { parent: Option, children: Vec, + child_subtree: Option, + /// If this node is a slot in a subtree, this is node whose child_subtree is that subtree. + slot_for_supertree: Option, height: u16, } @@ -23,6 +37,8 @@ pub trait TreeRef { fn parent_id(&self, id: NodeId) -> Option; /// The children ids of the node. fn children_ids(&self, id: NodeId) -> Vec; + /// The subtree tree under the node. + fn subtree(&self, id: NodeId) -> Option<&Subtree>; /// The height of the node. fn height(&self, id: NodeId) -> Option; /// Returns true if the node exists. @@ -31,10 +47,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 +59,8 @@ 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 subtree. + fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec, slot: Option); } impl<'a> TreeRef for TreeRefView<'a> { @@ -65,16 +81,39 @@ impl<'a> TreeRef for TreeRefView<'a> { fn contains(&self, id: NodeId) -> bool { self.get(id).is_ok() } + + fn subtree(&self, id: NodeId) -> Option<&Subtree> { + self.get(id).ok()?.child_subtree.as_ref() + } } 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 (supertree, children) = { + let node = (&mut tree.1).get(id).unwrap(); + (node.slot_for_supertree, std::mem::take(&mut node.children)) + }; + for child in children { recurse(tree, child); } + + // If this node is a slot in a subtree, remove it from the subtree. + if let Some(supertree) = supertree { + let supertree_root = (&mut tree.1).get(supertree).unwrap(); + + if let Some(subtree) = &mut supertree_root.child_subtree { + subtree.slot = None; + } + + debug_assert!( + supertree_root.children.is_empty(), + "Subtree root should have no children when slot is removed." + ); + } } + { let mut node_data_mut = &mut self.1; if let Some(parent) = node_data_mut.get(id).unwrap().parent { @@ -86,16 +125,6 @@ impl<'a> TreeMut for TreeMutView<'a> { 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 { - let parent = (&mut node_data_mut).get(parent).unwrap(); - parent.children.retain(|&child| child != id); - } - } - } - fn create_node(&mut self, id: NodeId) { let (entities, node_data_mut) = self; entities.add_component( @@ -105,19 +134,20 @@ impl<'a> TreeMut for TreeMutView<'a> { parent: None, children: Vec::new(), height: 0, + child_subtree: None, + slot_for_supertree: 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 +163,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 +201,91 @@ 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, slot: Option) { + let (_, node_data_mut) = self; + + let light_root_height; + { + let subtree = Subtree { + super_tree_root: id, + shadow_roots: shadow_roots.clone(), + slot, + }; + + let light_root = node_data_mut + .get(id) + .expect("tried to create subtree with non-existent id"); + + light_root.child_subtree = Some(subtree); + light_root_height = light_root.height; + + if let Some(slot) = slot { + let slot = node_data_mut + .get(slot) + .expect("tried to create subtree with non-existent slot"); + slot.slot_for_supertree = Some(id); + } + } + + // Now that we have created the subtree, we need to update the height of the subtree roots + for root in shadow_roots { + set_height(self, root, light_root_height + 1); + } + } +} + +fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { + match &parent.child_subtree { + Some(subtree) => { + if let Some(slot) = subtree.slot { + tree.height(slot) + .expect("Attempted to read a slot that does not exist") + + 1 + } else { + panic!("Attempted to read the height of a subtree without 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 (subtree, supertree, children) = { let mut node_data_mut = &mut tree.1; let mut node = (&mut node_data_mut).get(node).unwrap(); node.height = height; - node.children.clone() + + ( + node.child_subtree.clone(), + node.slot_for_supertree, + node.children.clone(), + ) }; - for child in children { - set_height(tree, child, height + 1); + + // If the children are actually part of a subtree, there height is determined by the height of the subtree + if let Some(subtree) = subtree { + // Set the height of the subtree roots + for &shadow_root in &subtree.shadow_roots { + set_height(tree, shadow_root, height); + } + } 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 subtree, we need to go to the super tree and update the height of its children + if let Some(supertree) = supertree { + let children = (&tree.1).get(supertree).unwrap().children.clone(); + for child in children { + set_height(tree, child, height + 1); + } } } @@ -209,6 +311,11 @@ impl<'a> TreeRef for TreeMutView<'a> { fn contains(&self, id: NodeId) -> bool { self.1.get(id).is_ok() } + + fn subtree(&self, id: NodeId) -> Option<&Subtree> { + let node_data = &self.1; + node_data.get(id).unwrap().child_subtree.as_ref() + } } #[test] @@ -235,6 +342,54 @@ fn creation() { assert_eq!(tree.children_ids(parent_id), &[child_id]); } +#[test] +fn subtree_creation() { + 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::().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); + + 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)); +} + #[test] fn insertion() { use shipyard::World; From 32945998c33a593c5ad46abc40d2cc69a3af2648 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 7 Apr 2023 11:23:37 -0500 Subject: [PATCH 05/35] implement removing subtrees --- packages/native-core/src/tree.rs | 65 +++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index b87b11ab5..e44970649 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -61,6 +61,8 @@ pub trait TreeMut: TreeRef { fn insert_after(&mut self, old_id: NodeId, new_id: NodeId); /// Creates a new subtree. fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec, slot: Option); + /// Remove any subtree. + fn remove_subtree(&mut self, id: NodeId); } impl<'a> TreeRef for TreeRefView<'a> { @@ -236,6 +238,36 @@ impl<'a> TreeMut for TreeMutView<'a> { 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(subtree) = node.child_subtree.take() { + // Remove the slot's link to the subtree + if let Some(slot) = subtree.slot { + let slot = node_data_mut + .get(slot) + .expect("tried to remove subtree with non-existent slot"); + slot.slot_for_supertree = 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 &subtree.shadow_roots { + set_height(self, *root, 0); + } + } + } + } } fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { @@ -343,7 +375,7 @@ fn creation() { } #[test] -fn subtree_creation() { +fn subtree() { use shipyard::World; #[derive(Component)] struct Num(i32); @@ -369,6 +401,7 @@ fn subtree_creation() { 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); @@ -388,6 +421,36 @@ fn subtree_creation() { 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_supertree, + 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] From 60be849ff0185a7351ec46d7f17f49074913522d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 8 Apr 2023 18:53:53 -0500 Subject: [PATCH 06/35] handle passes entering or avoiding the shadow tree --- packages/native-core-macro/src/lib.rs | 2 +- packages/native-core/src/custom_element.rs | 43 ++++--- packages/native-core/src/dioxus.rs | 1 - packages/native-core/src/lib.rs | 1 - packages/native-core/src/node.rs | 6 - packages/native-core/src/passes.rs | 48 +++++++- packages/native-core/src/real_dom.rs | 42 ++++--- packages/native-core/src/shadow_dom.rs | 26 ----- packages/native-core/src/tree.rs | 130 ++++++++++++--------- packages/native-core/tests/passes.rs | 1 - 10 files changed, 167 insertions(+), 133 deletions(-) delete mode 100644 packages/native-core/src/shadow_dom.rs diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index fd25c3868..7960a0d4c 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -336,7 +336,7 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { let (#(#split_views,)*) = data; let tree = run_view.tree.clone(); let node_types = run_view.node_type.clone(); - dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context| { + dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, Self::TRAVERSE_SHADOW_DOM, |id, context| { let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err)); // get all of the states from the tree view // Safety: No node has itself as a parent or child. diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index 9b3a19c04..f829d89f0 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -1,13 +1,12 @@ use std::sync::{Arc, RwLock}; use rustc_hash::FxHashMap; +use shipyard::Component; use crate::{ node::{FromAnyValue, NodeType}, node_ref::AttributeMask, prelude::{NodeImmutable, NodeMut, RealDom}, - real_dom::NodeTypeMut, - shadow_dom::ShadowDom, NodeId, }; @@ -28,7 +27,7 @@ impl CustomElementRegistry { self.builders.insert( W::NAME, CustomElementBuilder { - create: |dom, light_root_id| Box::new(W::create(dom, light_root_id)), + create: |dom| Box::new(W::create(dom)), }, ); } @@ -42,27 +41,22 @@ impl CustomElementRegistry { if let Some(element_tag) = element_tag { if let Some(builder) = self.builders.get(element_tag.as_str()) { let boxed_widget = { - let light_root_id = node.id(); let dom = node.real_dom_mut(); - (builder.create)(dom, light_root_id) + (builder.create)(dom) }; let boxed_widget = CustomElementManager { inner: Arc::new(RwLock::new(boxed_widget)), }; - let NodeTypeMut::Element(mut el) = node.node_type_mut()else{ - panic!("The type of the light element should not change when creating a shadow DOM") - }; - - *el.shadow_root_mut() = Some(ShadowDom::new(boxed_widget).into()); + node.insert(boxed_widget); } } } } struct CustomElementBuilder { - create: fn(&mut RealDom, NodeId) -> Box>, + create: fn(&mut RealDom) -> Box>, } /// A controlled element that renders to a shadow DOM @@ -71,11 +65,16 @@ pub trait CustomElement: Send + Sync + 'stat const NAME: &'static str; /// Create a new widget without mounting it. - fn create(dom: &mut RealDom, light_root_id: NodeId) -> Self; + fn create(dom: &mut RealDom) -> Self; - /// The root node of the widget. + /// The root node of the widget. This must be static once the element is created. fn root(&self) -> NodeId; + /// The slot to render children of the element into. This must be static once the element is created. + fn slot(&self) -> Option { + None + } + /// Called when the attributes of the widget are changed. fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); } @@ -88,14 +87,14 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = const NAME: &'static str; /// Create a new widget. - fn create(dom: &mut RealDom, light_root_id: NodeId) -> W; + fn create(dom: &mut RealDom) -> W; } impl, V: FromAnyValue + Send + Sync> ElementFactory for W { const NAME: &'static str = W::NAME; - fn create(dom: &mut RealDom, light_root_id: NodeId) -> Self { - Self::create(dom, light_root_id) + fn create(dom: &mut RealDom) -> Self { + Self::create(dom) } } @@ -118,6 +117,7 @@ impl, V: FromAnyValue + Send + Sync> CustomElementUpdater } } +#[derive(Component, Clone)] pub struct CustomElementManager { inner: Arc>>>, } @@ -126,12 +126,11 @@ impl CustomElementManager { pub fn root(&self) -> NodeId { self.inner.read().unwrap().root() } -} -impl Clone for CustomElementManager { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } + pub fn on_attributes_changed(&self, dom: &mut RealDom, attributes: &AttributeMask) { + self.inner + .write() + .unwrap() + .attributes_changed(dom, attributes); } } diff --git a/packages/native-core/src/dioxus.rs b/packages/native-core/src/dioxus.rs index 9d50b1224..3e9ff9ee0 100644 --- a/packages/native-core/src/dioxus.rs +++ b/packages/native-core/src/dioxus.rs @@ -262,7 +262,6 @@ fn create_template_node( }) .collect(), listeners: FxHashSet::default(), - shadow: None, }); let node_id = rdom.create_node(node).id(); for child in *children { diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index 6dfeb81cd..fcbbdd9fb 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -16,7 +16,6 @@ pub mod node_ref; pub mod node_watcher; mod passes; pub mod real_dom; -mod shadow_dom; pub mod tree; pub mod utils; diff --git a/packages/native-core/src/node.rs b/packages/native-core/src/node.rs index 2cbfa3fb3..42ff8229f 100644 --- a/packages/native-core/src/node.rs +++ b/packages/native-core/src/node.rs @@ -7,8 +7,6 @@ use std::{ fmt::{Debug, Display}, }; -use crate::shadow_dom::ShadowDom; - /// A element node in the RealDom #[derive(Debug, Clone, Default)] pub struct ElementNode { @@ -20,9 +18,6 @@ pub struct ElementNode { pub attributes: FxHashMap>, /// The events the element is listening for pub listeners: FxHashSet, - /// The shadow dom of the element - // This is a rare property, so it is stored in an `Box` to save space - pub shadow: Option>>, } impl ElementNode { @@ -33,7 +28,6 @@ impl ElementNode { namespace: namespace.into(), attributes: Default::default(), listeners: Default::default(), - shadow: Default::default(), } } } diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 16d0f71fc..f8aa94fa5 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -194,6 +194,7 @@ pub fn run_pass( dependants: FxHashSet, pass_direction: PassDirection, view: RunPassView, + enter_shadow_dom: bool, mut update_node: impl FnMut(NodeId, &SendAnyMap) -> bool, ) { let RunPassView { @@ -209,9 +210,32 @@ pub fn run_pass( while let Some((height, id)) = dirty.pop_front(type_id) { if (update_node)(id, ctx) { nodes_updated.insert(id); - for id in tree.children_ids(id) { - for dependant in &dependants { - dirty.insert(*dependant, id, height + 1); + let shadow_tree = tree.shadow_tree(id); + match (enter_shadow_dom, shadow_tree) { + (true, Some(shadow_tree)) => { + // If this pass uses the shadow dom, update the shadow dom children instead of the normal children + for id in &shadow_tree.shadow_roots { + for dependant in &dependants { + dirty.insert(*dependant, *id, height + 1); + } + } + } + _ => { + for id in tree.children_ids(id) { + for dependant in &dependants { + dirty.insert(*dependant, id, height + 1); + } + } + } + } + // If this pass uses the shadow dom, update the light dom's children if this node is a slot + if enter_shadow_dom { + if let Some(slot_for_light_tree) = tree.slot_for_light_tree(id) { + for id in tree.children_ids(slot_for_light_tree) { + for dependant in &dependants { + dirty.insert(*dependant, id, height + 1); + } + } } } } @@ -221,9 +245,21 @@ pub fn run_pass( while let Some((height, id)) = dirty.pop_back(type_id) { if (update_node)(id, ctx) { nodes_updated.insert(id); - if let Some(id) = tree.parent_id(id) { - for dependant in &dependants { - dirty.insert(*dependant, id, height - 1); + + // If this pass uses the shadow dom, update the light dom root if this node is a root + let light_tree_root = tree.light_tree_root(id); + match (enter_shadow_dom, light_tree_root) { + (true, Some(light_tree_root)) => { + for dependant in &dependants { + dirty.insert(*dependant, light_tree_root, height + 1); + } + } + _ => { + if let Some(id) = tree.parent_id(id) { + for dependant in &dependants { + dirty.insert(*dependant, id, height - 1); + } + } } } } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 72d8bce9c..9b8ae1983 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -10,7 +10,7 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, RwLock}; -use crate::custom_element::CustomElementRegistry; +use crate::custom_element::{CustomElementManager, CustomElementRegistry}; use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; @@ -18,7 +18,6 @@ use crate::node_ref::{NodeMask, NodeMaskBuilder}; use crate::node_watcher::{AttributeWatcher, NodeWatcher}; use crate::passes::{DirtyNodeStates, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; -use crate::shadow_dom::ShadowDom; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; use crate::NodeId; use crate::{FxDashSet, SendAnyMap}; @@ -113,7 +112,7 @@ pub struct RealDom { attribute_watchers: AttributeWatchers, workload: ScheduledWorkload, root_id: NodeId, - custom_elements: CustomElementRegistry, + custom_elements: Arc>>, phantom: std::marker::PhantomData, } @@ -142,7 +141,6 @@ impl RealDom { namespace: Some("Root".to_string()), attributes: FxHashMap::default(), listeners: FxHashSet::default(), - shadow: None, }); let root_id = world.add_entity(root_node); { @@ -189,7 +187,10 @@ impl RealDom { /// 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>) -> 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 @@ -201,6 +202,15 @@ impl RealDom { .mark_dirty(id, NodeMaskBuilder::ALL.build()); 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 + .write() + .unwrap() + .add_shadow_dom(NodeMut::new(id, self)); + } + NodeMut::new(id, self) } @@ -280,10 +290,9 @@ impl RealDom { let passes = std::mem::take(&mut self.dirty_nodes.passes_updated); let nodes_updated = std::mem::take(&mut self.dirty_nodes.nodes_updated); - // call attribute watchers for (node_id, mask) in &nodes_updated { if self.contains(*node_id) { - // ignore watchers if they are already being modified + // 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 { @@ -293,6 +302,15 @@ impl RealDom { ); } }; + + // call custom element watchers + let custom_element_manager = { + let node = self.get_mut(*node_id).unwrap(); + node.get::>().map(|x| x.clone()) + }; + if let Some(custom_element_manager) = custom_element_manager { + custom_element_manager.on_attributes_changed(self, mask.attributes()); + } } } @@ -1062,16 +1080,6 @@ impl ElementNodeMut<'_, V> { pub fn listeners(&self) -> &FxHashSet { &self.element().listeners } - - /// Get the shadow root of the element - pub fn shadow_root(&self) -> Option<&ShadowDom> { - self.element().shadow.as_deref() - } - - /// Get the shadow root of the element - pub fn shadow_root_mut(&mut self) -> &mut Option>> { - &mut self.element_mut().shadow - } } // Create a workload from all of the passes. This orders the passes so that each pass will only run at most once. diff --git a/packages/native-core/src/shadow_dom.rs b/packages/native-core/src/shadow_dom.rs deleted file mode 100644 index 262d6fbb6..000000000 --- a/packages/native-core/src/shadow_dom.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::fmt::Debug; - -use crate::{custom_element::CustomElementManager, node::FromAnyValue, prelude::NodeRef, NodeId}; - -#[derive(Clone)] -pub struct ShadowDom { - shadow_root: NodeId, - updater: CustomElementManager, -} - -impl ShadowDom { - pub fn new(updater: CustomElementManager) -> Self { - Self { - shadow_root: updater.root(), - updater, - } - } -} - -impl Debug for ShadowDom { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ShadowDom") - .field("shadow_root", &self.shadow_root) - .finish() - } -} diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index e44970649..e0b32f28d 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -4,15 +4,15 @@ use crate::NodeId; use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut}; use std::fmt::Debug; -/// A subtree of a tree. +/// A shadow_tree of a tree. #[derive(PartialEq, Eq, Clone, Debug, Component)] -pub struct Subtree { - /// The root of the subtree - shadow_roots: Vec, +pub struct ShadowTree { + /// The root of the shadow_tree + pub shadow_roots: Vec, /// The node that children of the super tree should be inserted under. - slot: Option, - /// The node in the super tree that the subtree is attached to. - super_tree_root: NodeId, + pub slot: Option, + /// The node in the super tree that the shadow_tree is attached to. + pub super_tree_root: NodeId, } /// A node in a tree. @@ -20,9 +20,11 @@ pub struct Subtree { pub struct Node { parent: Option, children: Vec, - child_subtree: Option, - /// If this node is a slot in a subtree, this is node whose child_subtree is that subtree. - slot_for_supertree: Option, + child_subtree: Option, + /// 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, + /// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree. + light_tree_root: Option, height: u16, } @@ -37,8 +39,13 @@ pub trait TreeRef { fn parent_id(&self, id: NodeId) -> Option; /// The children ids of the node. fn children_ids(&self, id: NodeId) -> Vec; - /// The subtree tree under the node. - fn subtree(&self, id: NodeId) -> Option<&Subtree>; + /// The shadow tree tree under the node. + fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>; + // TODO: rethink naming + /// The node that contains the shadow tree this node is a slot for + fn slot_for_light_tree(&self, id: NodeId) -> Option; + /// The node that contains the shadow tree this node is a root of + fn light_tree_root(&self, id: NodeId) -> Option; /// The height of the node. fn height(&self, id: NodeId) -> Option; /// Returns true if the node exists. @@ -59,9 +66,9 @@ 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 subtree. + /// Creates a new shadow tree. fn create_subtree(&mut self, id: NodeId, shadow_roots: Vec, slot: Option); - /// Remove any subtree. + /// Remove any shadow tree. fn remove_subtree(&mut self, id: NodeId); } @@ -84,34 +91,42 @@ impl<'a> TreeRef for TreeRefView<'a> { self.get(id).is_ok() } - fn subtree(&self, id: NodeId) -> Option<&Subtree> { + 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 { + self.get(id).ok()?.slot_for_light_tree + } + + fn light_tree_root(&self, id: NodeId) -> Option { + self.get(id).ok()?.light_tree_root + } } impl<'a> TreeMut for TreeMutView<'a> { fn remove(&mut self, id: NodeId) { fn recurse(tree: &mut TreeMutView<'_>, id: NodeId) { - let (supertree, children) = { + let (light_tree, children) = { let node = (&mut tree.1).get(id).unwrap(); - (node.slot_for_supertree, std::mem::take(&mut node.children)) + (node.slot_for_light_tree, std::mem::take(&mut node.children)) }; for child in children { recurse(tree, child); } - // If this node is a slot in a subtree, remove it from the subtree. - if let Some(supertree) = supertree { - let supertree_root = (&mut tree.1).get(supertree).unwrap(); + // If this node is a slot in a shadow_tree, remove it from the shadow_tree. + if let Some(light_tree) = light_tree { + let light_tree_root = (&mut tree.1).get(light_tree).unwrap(); - if let Some(subtree) = &mut supertree_root.child_subtree { - subtree.slot = None; + if let Some(shadow_tree) = &mut light_tree_root.child_subtree { + shadow_tree.slot = None; } debug_assert!( - supertree_root.children.is_empty(), - "Subtree root should have no children when slot is removed." + light_tree_root.children.is_empty(), + "ShadowTree root should have no children when slot is removed." ); } } @@ -137,7 +152,8 @@ impl<'a> TreeMut for TreeMutView<'a> { children: Vec::new(), height: 0, child_subtree: None, - slot_for_supertree: None, + slot_for_light_tree: None, + light_tree_root: None, }, ); } @@ -212,7 +228,7 @@ impl<'a> TreeMut for TreeMutView<'a> { let light_root_height; { - let subtree = Subtree { + let shadow_tree = ShadowTree { super_tree_root: id, shadow_roots: shadow_roots.clone(), slot, @@ -220,20 +236,20 @@ impl<'a> TreeMut for TreeMutView<'a> { let light_root = node_data_mut .get(id) - .expect("tried to create subtree with non-existent id"); + .expect("tried to create shadow_tree with non-existent id"); - light_root.child_subtree = Some(subtree); + 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 subtree with non-existent slot"); - slot.slot_for_supertree = Some(id); + .expect("tried to create shadow_tree with non-existent slot"); + slot.slot_for_light_tree = Some(id); } } - // Now that we have created the subtree, we need to update the height of the subtree roots + // Now that we have created the shadow_tree, we need to update the height of the shadow_tree roots for root in shadow_roots { set_height(self, root, light_root_height + 1); } @@ -243,13 +259,13 @@ impl<'a> TreeMut for TreeMutView<'a> { let (_, node_data_mut) = self; if let Ok(node) = node_data_mut.get(id) { - if let Some(subtree) = node.child_subtree.take() { - // Remove the slot's link to the subtree - if let Some(slot) = subtree.slot { + 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 subtree with non-existent slot"); - slot.slot_for_supertree = None; + .expect("tried to remove shadow_tree with non-existent slot"); + slot.slot_for_light_tree = None; } let node = node_data_mut.get(id).unwrap(); @@ -262,7 +278,7 @@ impl<'a> TreeMut for TreeMutView<'a> { } // Reset the height of the shadow roots - for root in &subtree.shadow_roots { + for root in &shadow_tree.shadow_roots { set_height(self, *root, 0); } } @@ -272,13 +288,13 @@ impl<'a> TreeMut for TreeMutView<'a> { fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { match &parent.child_subtree { - Some(subtree) => { - if let Some(slot) = subtree.slot { + 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 subtree without a slot"); + panic!("Attempted to read the height of a shadow_tree without a slot"); } } None => parent.height + 1, @@ -287,22 +303,22 @@ fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { /// 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 (subtree, supertree, children) = { + let (shadow_tree, light_tree, children) = { let mut node_data_mut = &mut tree.1; let mut node = (&mut node_data_mut).get(node).unwrap(); node.height = height; ( node.child_subtree.clone(), - node.slot_for_supertree, + node.slot_for_light_tree, node.children.clone(), ) }; - // If the children are actually part of a subtree, there height is determined by the height of the subtree - if let Some(subtree) = subtree { - // Set the height of the subtree roots - for &shadow_root in &subtree.shadow_roots { + // 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); } } else { @@ -312,9 +328,9 @@ fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) { } } - // If this nodes is a slot for a subtree, we need to go to the super tree and update the height of its children - if let Some(supertree) = supertree { - let children = (&tree.1).get(supertree).unwrap().children.clone(); + // 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); } @@ -344,10 +360,20 @@ impl<'a> TreeRef for TreeMutView<'a> { self.1.get(id).is_ok() } - fn subtree(&self, id: NodeId) -> Option<&Subtree> { + fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> { let node_data = &self.1; node_data.get(id).unwrap().child_subtree.as_ref() } + + fn slot_for_light_tree(&self, id: NodeId) -> Option { + let node_data = &self.1; + node_data.get(id).unwrap().slot_for_light_tree + } + + fn light_tree_root(&self, id: NodeId) -> Option { + let node_data = &self.1; + node_data.get(id).unwrap().light_tree_root + } } #[test] @@ -375,7 +401,7 @@ fn creation() { } #[test] -fn subtree() { +fn shadow_tree() { use shipyard::World; #[derive(Component)] struct Num(i32); @@ -432,7 +458,7 @@ fn subtree() { &[shadow_parent_id] ); assert_eq!( - tree.1.get(shadow_child_id).unwrap().slot_for_supertree, + tree.1.get(shadow_child_id).unwrap().slot_for_light_tree, Some(parent_id) ); diff --git a/packages/native-core/tests/passes.rs b/packages/native-core/tests/passes.rs index 9a0fbe51d..368333fb5 100644 --- a/packages/native-core/tests/passes.rs +++ b/packages/native-core/tests/passes.rs @@ -10,7 +10,6 @@ fn create_blank_element() -> NodeType { namespace: None, attributes: FxHashMap::default(), listeners: FxHashSet::default(), - shadow: None, }) } From 500a73abae25fb9749c30d2c4a29507fde453890 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 8 Apr 2023 19:01:48 -0500 Subject: [PATCH 07/35] allow registering custom element and fix deadlock --- packages/native-core/src/real_dom.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 9b8ae1983..490e04dc6 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -10,7 +10,7 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, RwLock}; -use crate::custom_element::{CustomElementManager, CustomElementRegistry}; +use crate::custom_element::{CustomElement, CustomElementManager, CustomElementRegistry}; use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; @@ -206,7 +206,7 @@ impl RealDom { if is_element { let custom_elements = self.custom_elements.clone(); custom_elements - .write() + .read() .unwrap() .add_shadow_dom(NodeMut::new(id, self)); } @@ -426,6 +426,11 @@ impl RealDom { pub fn raw_world_mut(&mut self) -> &mut World { &mut self.world } + + /// Registers a new custom element. + pub fn register_custom_element>(&mut self) { + self.custom_elements.write().unwrap().register::() + } } /// A reference to a tracked component in a node. From 05caa475b1d15bf8dd7e9a6dce357cbd1c1ff31b Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sun, 9 Apr 2023 22:54:18 +0800 Subject: [PATCH 08/35] Add timeout count example --- packages/web/Cargo.toml | 2 ++ packages/web/examples/README.md | 10 +++++++++ packages/web/examples/timeout_count.rs | 31 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/web/examples/README.md create mode 100644 packages/web/examples/timeout_count.rs diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7adfbf13a..12184644c 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -89,3 +89,5 @@ wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr", version = "0.3.0"} wasm-logger = "0.2.0" dioxus-web = { path = ".", features = ["hydrate"] } +gloo-timers = "0.2.3" +gloo-dialogs = "0.1.1" diff --git a/packages/web/examples/README.md b/packages/web/examples/README.md new file mode 100644 index 000000000..faa322df3 --- /dev/null +++ b/packages/web/examples/README.md @@ -0,0 +1,10 @@ +Examples +======== + +# Hydrate + +- `hydrate` show hydrate + +# Async + +- `timeout_count` button to add count and show count in the future diff --git a/packages/web/examples/timeout_count.rs b/packages/web/examples/timeout_count.rs new file mode 100644 index 000000000..742e6a49e --- /dev/null +++ b/packages/web/examples/timeout_count.rs @@ -0,0 +1,31 @@ +// https://jakelazaroff.com/words/were-react-hooks-a-mistake/ +use dioxus::prelude::*; + +fn main() { + dioxus_web::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_ref(cx, || 0); + let started = use_state(cx, || false); + + let start = move || { + if !*started.get() { + let count = count.clone(); // clone reference rather than value + let alert = move || gloo_dialogs::alert(&format!("Your score was {}!", count.read())); + gloo_timers::callback::Timeout::new(5_000, alert).forget(); + } + started.set(true); // this cannot be done inside condition or infinite loop + }; + + cx.render(rsx! { + button { + onclick: move |_event| { + start(); + *count.write() += 1; + }, + // format is needed as {count} does not seemed to work in `if` within content + if **started { format!("Current score: {}", count.write()) } else { "Start".to_string() } + } + }) +} From 4bc5d457ae2ed07e41f8813fef1c1ec8842c92a2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 10:03:55 -0500 Subject: [PATCH 09/35] create custom element test --- packages/native-core/tests/custom_element.rs | 195 +++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 packages/native-core/tests/custom_element.rs diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs new file mode 100644 index 000000000..f1285c8cc --- /dev/null +++ b/packages/native-core/tests/custom_element.rs @@ -0,0 +1,195 @@ +use dioxus::prelude::*; +use dioxus_native_core::{custom_element::CustomElement, prelude::*}; +use dioxus_native_core_macro::partial_derive_state; +use shipyard::Component; +use tokio::time::sleep; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Component)] +pub struct BlablaState { + count: usize, +} + +#[partial_derive_state] +impl State for BlablaState { + type ParentDependencies = (Self,); + type ChildDependencies = (); + type NodeDependencies = (); + + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() + .with_attrs(AttributeMaskBuilder::Some(&["blabla"])) + .with_element(); + + fn update<'a>( + &mut self, + _: NodeView, + _: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _: Vec<::ElementBorrowed<'a>>, + _: &SendAnyMap, + ) -> bool { + if let Some((parent,)) = parent { + if parent.count != 0 { + self.count += 1; + } + } + true + } + + fn create<'a>( + node_view: NodeView<()>, + node: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + children: Vec<::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; + + 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! { + blabla { + + }; + testing132 { + + }; + } +} + +#[test] +fn native_core_is_okay() { + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + fn app(cx: Scope) -> Element { + let colors = use_state(cx, || vec!["green", "blue", "red"]); + let padding = use_state(cx, || 10); + + use_effect(cx, colors, |colors| async move { + sleep(Duration::from_millis(1000)).await; + colors.with_mut(|colors| colors.reverse()); + }); + + use_effect(cx, padding, |padding| async move { + sleep(Duration::from_millis(10)).await; + padding.with_mut(|padding| { + if *padding < 65 { + *padding += 1; + } else { + *padding = 5; + } + }); + }); + + let _big = colors[0]; + let _mid = colors[1]; + let _small = colors[2]; + + cx.render(rsx! { + blabla { + blabla { + testing132 {} + } + } + }) + } + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + + rt.block_on(async { + let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()]))); + rdom.lock() + .unwrap() + .register_custom_element::(); + let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap()); + let mut dom = VirtualDom::new(app); + + let mutations = dom.rebuild(); + dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + + let ctx = SendAnyMap::new(); + rdom.lock().unwrap().update_state(ctx); + + for _ in 0..10 { + dom.wait_for_work().await; + + let mutations = dom.render_immediate(); + dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + + let ctx = SendAnyMap::new(); + rdom.lock().unwrap().update_state(ctx); + } + }); +} + +struct TestElement { + root: NodeId, +} + +impl CustomElement for TestElement { + const NAME: &'static str = "blabla"; + + fn create(dom: &mut RealDom<()>) -> Self { + let root = dom.create_node(ElementNode { + tag: "shadow_root".into(), + namespace: None, + attributes: Default::default(), + listeners: Default::default(), + }); + Self { root: root.id() } + } + + fn root(&self) -> NodeId { + self.root + } + + fn attributes_changed( + &mut self, + _dom: &mut RealDom<()>, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + println!("attributes_changed"); + println!("{:?}", attributes); + } +} From e7fb03f900d166995a2bfa18c4c385187873d7f5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 10:04:18 -0500 Subject: [PATCH 10/35] Merge branch 'upstream' into make-widgets-cross-framework --- examples/counter.rs | 36 ++++++++++++++++++++++++++++++++ packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 5 +++-- packages/desktop/src/lib.rs | 6 ++++-- packages/desktop/src/protocol.rs | 13 +++++++----- packages/desktop/src/webview.rs | 15 +++++++------ packages/ssr/src/cache.rs | 16 +++++++++++++- packages/ssr/src/renderer.rs | 23 ++++++++++++++++++-- packages/ssr/tests/inner_html.rs | 26 +++++++++++++++++++++++ packages/web/src/cfg.rs | 7 ++++--- 10 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 examples/counter.rs create mode 100644 packages/ssr/tests/inner_html.rs diff --git a/examples/counter.rs b/examples/counter.rs new file mode 100644 index 000000000..f51083a95 --- /dev/null +++ b/examples/counter.rs @@ -0,0 +1,36 @@ +//! Comparison example with leptos' counter example +//! https://github.com/leptos-rs/leptos/blob/main/examples/counters/src/lib.rs + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let counters = use_state(cx, || vec![0, 0, 0]); + let sum: usize = counters.iter().copied().sum(); + + render! { + div { + button { onclick: move |_| counters.make_mut().push(0), "Add counter" } + button { onclick: move |_| { counters.make_mut().pop(); }, "Remove counter" } + p { "Total: {sum}" } + for (i, counter) in counters.iter().enumerate() { + li { + button { onclick: move |_| counters.make_mut()[i] -= 1, "-1" } + input { + value: "{counter}", + oninput: move |e| { + if let Ok(value) = e.value.parse::() { + counters.make_mut()[i] = value; + } + } + } + button { onclick: move |_| counters.make_mut()[i] += 1, "+1" } + button { onclick: move |_| { counters.make_mut().remove(i); }, "x" } + } + } + } + } +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 7645e4de5..5722a42a2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -21,7 +21,7 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -wry = { version = "0.23.4" } +wry = { version = "0.27.2" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ "sync", diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 7b37b46d4..d7f1b5dfa 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::path::PathBuf; use wry::application::window::Icon; @@ -28,7 +29,7 @@ type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, - Box>) -> WryResult>> + 'static>, + Box>) -> WryResult>> + 'static>, ); impl Config { @@ -98,7 +99,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&HttpRequest>) -> WryResult>> + 'static, + F: Fn(&HttpRequest>) -> WryResult>> + 'static, { self.protocols.push((name, Box::new(handler))); self diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 75c67dd22..434cf3305 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -36,8 +36,8 @@ use tao::{ }; pub use wry; pub use wry::application as tao; -use wry::application::window::WindowId; use wry::webview::WebView; +use wry::{application::window::WindowId, webview::WebContext}; /// Launch the WebView and run the event loop. /// @@ -281,7 +281,7 @@ fn create_new_window( event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { - let webview = webview::build(&mut cfg, event_loop, proxy.clone()); + let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone()); dom.base_scope().provide_context(DesktopContext::new( webview.clone(), @@ -299,6 +299,7 @@ fn create_new_window( webview, dom, waker: waker::tao_waker(proxy, id), + web_context, } } @@ -306,6 +307,7 @@ struct WebviewHandler { dom: VirtualDom, webview: Rc, waker: Waker, + web_context: WebContext, } /// Poll the virtualdom until it's pending diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index f8c002a6a..efdc849a5 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,5 +1,8 @@ use dioxus_interpreter_js::INTERPRETER_JS; -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; use wry::{ http::{status::StatusCode, Request, Response}, Result, @@ -27,7 +30,7 @@ pub(super) fn desktop_handler( custom_head: Option, custom_index: Option, root_name: &str, -) -> Result>> { +) -> Result>> { // If the request is for the root, we'll serve the index.html file. if request.uri().path() == "/" { // If a custom index is provided, just defer to that, expecting the user to know what they're doing. @@ -53,7 +56,7 @@ pub(super) fn desktop_handler( return Response::builder() .header("Content-Type", "text/html") - .body(body) + .body(Cow::from(body)) .map_err(From::from); } @@ -72,13 +75,13 @@ pub(super) fn desktop_handler( if asset.exists() { return Response::builder() .header("Content-Type", get_mime_from_path(&asset)?) - .body(std::fs::read(asset)?) + .body(Cow::from(std::fs::read(asset)?)) .map_err(From::from); } Response::builder() .status(StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()) + .body(Cow::from(String::from("Not Found").into_bytes())) .map_err(From::from) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 099a2bd94..c6909972f 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -7,13 +7,13 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; use wry::application::window::Window; -use wry::webview::{WebView, WebViewBuilder}; +use wry::webview::{WebContext, WebView, WebViewBuilder}; pub fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> Rc { +) -> (Rc, WebContext) { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); @@ -33,6 +33,8 @@ pub fn build( )); } + let mut web_context = WebContext::new(cfg.data_dir.clone()); + let mut webview = WebViewBuilder::new(window) .unwrap() .with_transparent(cfg.window.window.transparent) @@ -52,11 +54,8 @@ pub fn build( .as_ref() .map(|handler| handler(window, evet)) .unwrap_or_default() - }); - - // These are commented out because wry is currently broken in wry - // let mut web_context = WebContext::new(cfg.data_dir.clone()); - // .with_web_context(&mut web_context); + }) + .with_web_context(&mut web_context); for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) @@ -82,5 +81,5 @@ pub fn build( webview = webview.with_devtools(true); } - Rc::new(webview.build().unwrap()) + (Rc::new(webview.build().unwrap()), web_context) } diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index e261d3b65..a67fd5f22 100644 --- a/packages/ssr/src/cache.rs +++ b/packages/ssr/src/cache.rs @@ -23,6 +23,8 @@ pub enum Segment { // This will be true if there are static styles inside_style_tag: bool, }, + /// A marker for where to insert a dynamic inner html + InnerHtmlMarker, } impl std::fmt::Write for StringChain { @@ -69,6 +71,9 @@ impl StringCache { write!(chain, "<{tag}")?; // we need to collect the styles and write them at the end let mut styles = Vec::new(); + // we need to collect the inner html and write it at the end + let mut inner_html = None; + // we need to keep track of if we have dynamic attrs to know if we need to insert a style and inner_html marker let mut has_dynamic_attrs = false; for attr in *attrs { match attr { @@ -77,7 +82,9 @@ impl StringCache { value, namespace, } => { - if let Some("style") = namespace { + if *name == "dangerous_inner_html" { + inner_html = Some(value); + } else if let Some("style") = namespace { styles.push((name, value)); } else { write!(chain, " {name}=\"{value}\"")?; @@ -110,6 +117,13 @@ impl StringCache { write!(chain, "/>")?; } else { write!(chain, ">")?; + // Write the static inner html, or insert a marker if dynamic inner html is possible + if let Some(inner_html) = inner_html { + chain.write_str(inner_html)?; + } else if has_dynamic_attrs { + chain.segments.push(Segment::InnerHtmlMarker); + } + for child in *children { Self::recurse(child, cur_path, root_idx, chain)?; } diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index be4025dba..cfa08aee4 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -70,6 +70,8 @@ impl Renderer { .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap())) .clone(); + let mut inner_html = None; + // We need to keep track of the dynamic styles so we can insert them into the right place let mut accumulated_dynamic_styles = Vec::new(); @@ -77,7 +79,9 @@ impl Renderer { match segment { Segment::Attr(idx) => { let attr = &template.dynamic_attrs[*idx]; - if attr.namespace == Some("style") { + if attr.name == "dangerous_inner_html" { + inner_html = Some(attr); + } else if attr.namespace == Some("style") { accumulated_dynamic_styles.push(attr); } else { match attr.value { @@ -165,6 +169,19 @@ impl Renderer { accumulated_dynamic_styles.clear(); } } + + Segment::InnerHtmlMarker => { + if let Some(inner_html) = inner_html.take() { + let inner_html = &inner_html.value; + match inner_html { + AttributeValue::Text(value) => write!(buf, "{}", value)?, + AttributeValue::Bool(value) => write!(buf, "{}", value)?, + AttributeValue::Float(f) => write!(buf, "{}", f)?, + AttributeValue::Int(i) => write!(buf, "{}", i)?, + _ => {} + } + } + } } } @@ -208,7 +225,9 @@ fn to_string_works() { StyleMarker { inside_style_tag: false, }, - PreRendered(">Hello world 1 -->".into(),), + PreRendered(">".into()), + InnerHtmlMarker, + PreRendered("Hello world 1 -->".into(),), Node(0,), PreRendered( "<-- Hello world 2
nest 1
nest 2
".into(), diff --git a/packages/ssr/tests/inner_html.rs b/packages/ssr/tests/inner_html.rs new file mode 100644 index 000000000..11fb94746 --- /dev/null +++ b/packages/ssr/tests/inner_html.rs @@ -0,0 +1,26 @@ +use dioxus::prelude::*; + +#[test] +fn static_inner_html() { + fn app(cx: Scope) -> Element { + render! { div { dangerous_inner_html: "
1234
" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!(dioxus_ssr::render(&dom), r#"
1234
"#); +} + +#[test] +fn dynamic_inner_html() { + fn app(cx: Scope) -> Element { + let inner_html = "
1234
"; + render! { div { dangerous_inner_html: "{inner_html}" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!(dioxus_ssr::render(&dom), r#"
1234
"#); +} diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 7e47ed461..7483a7a8b 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -49,15 +49,16 @@ impl Config { /// Set the name of the element that Dioxus will use as the root. /// - /// This is akint to calling React.render() on the element with the specified name. + /// This is akin to calling React.render() on the element with the specified name. pub fn rootname(mut self, name: impl Into) -> Self { self.rootname = name.into(); self } - /// Set the name of the element that Dioxus will use as the root. + /// Sets a string cache for wasm bindgen to [intern](https://docs.rs/wasm-bindgen/0.2.84/wasm_bindgen/fn.intern.html). This can help reduce the time it takes for wasm bindgen to pass + /// strings from rust to javascript. This can significantly improve pefromance when passing strings to javascript, but can have a negative impact on startup time. /// - /// This is akint to calling React.render() on the element with the specified name. + /// > Currently this cache is only used when creating static elements and attributes. pub fn with_string_cache(mut self, cache: Vec) -> Self { self.cached_strings = cache; self From 788c00274eb1ba6fe3136a31c21028fa64556599 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 10:05:09 -0500 Subject: [PATCH 11/35] create a shadow dom when a custom element is mounted --- packages/native-core/src/custom_element.rs | 72 +++-- packages/native-core/src/node_ref.rs | 6 +- packages/native-core/src/real_dom.rs | 9 +- packages/native-core/src/tree.rs | 2 +- packages/native-core/tests/custom_element.rs | 303 ++++++++++++++----- 5 files changed, 295 insertions(+), 97 deletions(-) diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index f829d89f0..62d7e5368 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -1,3 +1,6 @@ +//! A custom element is a controlled element that renders to a shadow DOM. +//! Each custom element is registered with a element name + use std::sync::{Arc, RwLock}; use rustc_hash::FxHashMap; @@ -7,11 +10,12 @@ use crate::{ node::{FromAnyValue, NodeType}, node_ref::AttributeMask, prelude::{NodeImmutable, NodeMut, RealDom}, + tree::TreeMut, NodeId, }; pub(crate) struct CustomElementRegistry { - builders: FxHashMap<&'static str, CustomElementBuilder>, + builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder>, } impl Default for CustomElementRegistry { @@ -25,7 +29,7 @@ impl Default for CustomElementRegistry { impl CustomElementRegistry { pub fn register>(&mut self) { self.builders.insert( - W::NAME, + (W::NAME, W::NAMESPACE), CustomElementBuilder { create: |dom| Box::new(W::create(dom)), }, @@ -34,17 +38,26 @@ impl CustomElementRegistry { pub fn add_shadow_dom(&self, mut node: NodeMut) { let element_tag = if let NodeType::Element(el) = &*node.node_type() { - Some(el.tag.clone()) + Some((el.tag.clone(), el.namespace.clone())) } else { None }; - if let Some(element_tag) = element_tag { - if let Some(builder) = self.builders.get(element_tag.as_str()) { + if let Some((tag, ns)) = element_tag { + if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) { let boxed_widget = { let dom = node.real_dom_mut(); (builder.create)(dom) }; + let shadow_roots = boxed_widget.roots(); + + let light_id = node.id(); + node.real_dom_mut().tree_mut().create_subtree( + light_id, + shadow_roots, + boxed_widget.slot(), + ); + let boxed_widget = CustomElementManager { inner: Arc::new(RwLock::new(boxed_widget)), }; @@ -64,11 +77,14 @@ pub trait CustomElement: Send + Sync + 'stat /// The tag the widget is registered under. const NAME: &'static str; - /// Create a new widget without mounting it. + /// The namespace the widget is registered under. + const NAMESPACE: Option<&'static str> = None; + + /// Create a new widget *without mounting* it. fn create(dom: &mut RealDom) -> Self; /// The root node of the widget. This must be static once the element is created. - fn root(&self) -> NodeId; + fn roots(&self) -> Vec; /// The slot to render children of the element into. This must be static once the element is created. fn slot(&self) -> Option { @@ -76,7 +92,7 @@ pub trait CustomElement: Send + Sync + 'stat } /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); + fn attributes_changed(&mut self, light_node: NodeMut, attributes: &AttributeMask); } /// A factory for creating widgets @@ -86,6 +102,9 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = /// The tag the widget is registered under. const NAME: &'static str; + /// The namespace the widget is registered under. + const NAMESPACE: Option<&'static str> = None; + /// Create a new widget. fn create(dom: &mut RealDom) -> W; } @@ -93,6 +112,8 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = impl, V: FromAnyValue + Send + Sync> ElementFactory for W { const NAME: &'static str = W::NAME; + const NAMESPACE: Option<&'static str> = W::NAMESPACE; + fn create(dom: &mut RealDom) -> Self { Self::create(dom) } @@ -101,36 +122,51 @@ impl, V: FromAnyValue + Send + Sync> ElementFactory fo /// A trait for updating widgets trait CustomElementUpdater: Send + Sync + 'static { /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, dom: &mut RealDom, attributes: &AttributeMask); + fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask); /// The root node of the widget. - fn root(&self) -> NodeId; + fn roots(&self) -> Vec; + + /// The slot to render children of the element into. + fn slot(&self) -> Option; } impl, V: FromAnyValue + Send + Sync> CustomElementUpdater for W { - fn attributes_changed(&mut self, root: &mut RealDom, attributes: &AttributeMask) { - self.attributes_changed(root, attributes); + fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask) { + self.attributes_changed(light_root, attributes); } - fn root(&self) -> NodeId { - self.root() + fn roots(&self) -> Vec { + self.roots() + } + + fn slot(&self) -> Option { + self.slot() } } +/// A concrete structure for managing a any widget. #[derive(Component, Clone)] pub struct CustomElementManager { inner: Arc>>>, } impl CustomElementManager { - pub fn root(&self) -> NodeId { - self.inner.read().unwrap().root() + /// The root node of the widget's shadow DOM. + pub fn roots(&self) -> Vec { + self.inner.read().unwrap().roots() } - pub fn on_attributes_changed(&self, dom: &mut RealDom, attributes: &AttributeMask) { + /// The slot to render children of the element into. + pub fn slot(&self) -> Option { + self.inner.read().unwrap().slot() + } + + /// Update the custom element based on attributes changed. + pub fn on_attributes_changed(&self, light_root: NodeMut, attributes: &AttributeMask) { self.inner .write() .unwrap() - .attributes_changed(dom, attributes); + .attributes_changed(light_root, attributes); } } diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index a1a309622..156ecdbd6 100644 --- a/packages/native-core/src/node_ref.rs +++ b/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), diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 490e04dc6..bdcd10d55 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -304,12 +304,11 @@ impl RealDom { }; // call custom element watchers - let custom_element_manager = { - let node = self.get_mut(*node_id).unwrap(); - node.get::>().map(|x| x.clone()) - }; + let node = self.get_mut(*node_id).unwrap(); + let custom_element_manager = + node.get::>().map(|x| x.clone()); if let Some(custom_element_manager) = custom_element_manager { - custom_element_manager.on_attributes_changed(self, mask.attributes()); + custom_element_manager.on_attributes_changed(node, mask.attributes()); } } } diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index e0b32f28d..1f46216b2 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -294,7 +294,7 @@ fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { .expect("Attempted to read a slot that does not exist") + 1 } else { - panic!("Attempted to read the height of a shadow_tree without a slot"); + 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, diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index f1285c8cc..3aa1b7462 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -2,21 +2,23 @@ use dioxus::prelude::*; use dioxus_native_core::{custom_element::CustomElement, prelude::*}; use dioxus_native_core_macro::partial_derive_state; use shipyard::Component; -use tokio::time::sleep; -#[derive(Debug, Clone, PartialEq, Eq, Default, Component)] -pub struct BlablaState { - count: usize, +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)] +pub struct ColorState { + color: usize, } #[partial_derive_state] -impl State for BlablaState { +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(&["blabla"])) + .with_attrs(AttributeMaskBuilder::Some(&["color"])) .with_element(); fn update<'a>( @@ -28,8 +30,70 @@ impl State for BlablaState { _: &SendAnyMap, ) -> bool { if let Some((parent,)) = parent { - if parent.count != 0 { - self.count += 1; + self.color = parent.color; + } + true + } + + fn create<'a>( + node_view: NodeView<()>, + node: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + children: Vec<::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, + _: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _: Vec<::ElementBorrowed<'a>>, + _: &SendAnyMap, + ) -> bool { + println!( + "Updating layout state @{:?} {:?}", + parent.as_ref().map(|(p,)| p.size), + view.node_id() + ); + 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 -= 1; } } true @@ -66,6 +130,7 @@ mod dioxus_elements { $(#[$attr])* pub struct $name; + #[allow(non_upper_case_globals)] impl $name { pub const TAG_NAME: &'static str = stringify!($name); pub const NAME_SPACE: Option<&'static str> = None; @@ -85,47 +150,110 @@ mod dioxus_elements { pub trait SvgAttributes {} builder_constructors! { - blabla { - + customelementslot { + size: attr, + }; + customelementnoslot { }; testing132 { - }; } } +struct CustomElementWithSlot { + root: NodeId, + slot: NodeId, +} + +impl CustomElement for CustomElementWithSlot { + const NAME: &'static str = "customelementslot"; + + fn create(dom: &mut RealDom<()>) -> Self { + 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 { + Some(self.slot) + } + + fn roots(&self) -> Vec { + 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(dom: &mut RealDom<()>) -> Self { + 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 { + 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 native_core_is_okay() { - use std::sync::{Arc, Mutex}; - use std::time::Duration; - +fn custom_elements_work() { fn app(cx: Scope) -> Element { - let colors = use_state(cx, || vec!["green", "blue", "red"]); - let padding = use_state(cx, || 10); + let count = use_state(cx, || 0); - use_effect(cx, colors, |colors| async move { - sleep(Duration::from_millis(1000)).await; - colors.with_mut(|colors| colors.reverse()); + use_future!(cx, |count| async move { + count.with_mut(|count| *count += 1); }); - use_effect(cx, padding, |padding| async move { - sleep(Duration::from_millis(10)).await; - padding.with_mut(|padding| { - if *padding < 65 { - *padding += 1; - } else { - *padding = 5; - } - }); - }); - - let _big = colors[0]; - let _mid = colors[1]; - let _small = colors[2]; - cx.render(rsx! { - blabla { - blabla { + customelementslot { + size: "{count}", + customelementslot { testing132 {} } } @@ -138,58 +266,93 @@ fn native_core_is_okay() { .unwrap(); rt.block_on(async { - let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()]))); - rdom.lock() - .unwrap() - .register_custom_element::(); - let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap()); + let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); + rdom.register_custom_element::(); + let mut dioxus_state = DioxusState::create(&mut rdom); let mut dom = VirtualDom::new(app); let mutations = dom.rebuild(); - dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); - rdom.lock().unwrap().update_state(ctx); + rdom.update_state(ctx); for _ in 0..10 { dom.wait_for_work().await; let mutations = dom.render_immediate(); - dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); - rdom.lock().unwrap().update_state(ctx); + rdom.update_state(ctx); + + // render... + rdom.traverse_depth_first(|node| { + let node_type = &*node.node_type(); + let indent = " ".repeat(node.height() as usize); + let color = *node.get::().unwrap(); + let size = *node.get::().unwrap(); + let id = node.id(); + println!("{indent}{id:?} {color:?} {size:?} {node_type:?}"); + }); } }); } -struct TestElement { - root: NodeId, +#[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::(); + 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); + }); } -impl CustomElement for TestElement { - const NAME: &'static str = "blabla"; - - fn create(dom: &mut RealDom<()>) -> Self { - let root = dom.create_node(ElementNode { - tag: "shadow_root".into(), - namespace: None, - attributes: Default::default(), - listeners: Default::default(), - }); - Self { root: root.id() } +#[test] +fn slotless_custom_element() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + customelementnoslot { + } + }) } - fn root(&self) -> NodeId { - self.root - } + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); - fn attributes_changed( - &mut self, - _dom: &mut RealDom<()>, - attributes: &dioxus_native_core::node_ref::AttributeMask, - ) { - println!("attributes_changed"); - println!("{:?}", attributes); - } + rt.block_on(async { + let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); + rdom.register_custom_element::(); + 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); + }); } From 4e582d0374509834a4b262957dca99da7813aff8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 11:35:57 -0500 Subject: [PATCH 12/35] custom element test passing --- packages/native-core-macro/src/lib.rs | 10 ++-- packages/native-core/examples/custom_attr.rs | 2 +- packages/native-core/examples/font_size.rs | 2 +- packages/native-core/examples/simple.rs | 2 +- .../native-core/examples/simple_dioxus.rs | 2 +- packages/native-core/src/passes.rs | 35 +++++++---- packages/native-core/src/real_dom.rs | 52 +++++++++++------ packages/native-core/src/tree.rs | 47 ++++++++++++--- .../tests/called_minimally_on_build.rs | 2 +- packages/native-core/tests/custom_element.rs | 58 ++++++++++++++----- 10 files changed, 155 insertions(+), 57 deletions(-) diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index e75a1485d..ff0e4629d 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/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::>(); 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::>(); 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 _,)*) @@ -336,7 +336,7 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { let (#(#split_views,)*) = data; let tree = run_view.tree.clone(); let node_types = run_view.node_type.clone(); - dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, Self::TRAVERSE_SHADOW_DOM, |id, context| { + dioxus_native_core::prelude::run_pass(type_id, dependants.clone(), pass_direction, run_view, |id, context| { let node_data: &NodeType<_> = node_types.get(id).unwrap_or_else(|err| panic!("Failed to get node type {:?}", err)); // get all of the states from the tree view // Safety: No node has itself as a parent or child. diff --git a/packages/native-core/examples/custom_attr.rs b/packages/native-core/examples/custom_attr.rs index 5a1fcfb8c..ee78a69f4 100644 --- a/packages/native-core/examples/custom_attr.rs +++ b/packages/native-core/examples/custom_attr.rs @@ -215,7 +215,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/font_size.rs b/packages/native-core/examples/font_size.rs index 937db0c17..4ff6b876d 100644 --- a/packages/native-core/examples/font_size.rs +++ b/packages/native-core/examples/font_size.rs @@ -160,7 +160,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first(true, |node| { let indent = " ".repeat(node.height() as usize); let font_size = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple.rs b/packages/native-core/examples/simple.rs index 469147c35..b44f05e63 100644 --- a/packages/native-core/examples/simple.rs +++ b/packages/native-core/examples/simple.rs @@ -206,7 +206,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple_dioxus.rs b/packages/native-core/examples/simple_dioxus.rs index fa1b202dd..c5aebd5d3 100644 --- a/packages/native-core/examples/simple_dioxus.rs +++ b/packages/native-core/examples/simple_dioxus.rs @@ -234,7 +234,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 383cdadd2..a76dbc61c 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -153,6 +153,7 @@ pub trait State: Any + Send + Sync { dependants: Default::default(), mask: node_mask, pass_direction: pass_direction::(), + enter_shadow_dom: Self::TRAVERSE_SHADOW_DOM, workload: Self::workload_system, phantom: PhantomData, } @@ -193,7 +194,6 @@ pub fn run_pass( dependants: Arc, pass_direction: PassDirection, view: RunPassView, - enter_shadow_dom: bool, mut update_node: impl FnMut(NodeId, &SendAnyMap) -> bool, ) { let RunPassView { @@ -232,28 +232,42 @@ pub fn run_pass( } } +#[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, + pub(crate) parent: Vec, /// The states in the child direction that should be invalidated when this state is invalidated - pub child: Vec, + pub(crate) child: Vec, /// The states in the node direction that should be invalidated when this state is invalidated - pub node: Vec, + pub(crate) node: Vec, } 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); } } @@ -273,6 +287,7 @@ pub struct TypeErasedState { pub(crate) mask: NodeMask, pub(crate) workload: fn(TypeId, Arc, PassDirection) -> WorkloadSystem, pub(crate) pass_direction: PassDirection, + pub(crate) enter_shadow_dom: bool, phantom: PhantomData, } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 8f5967789..e885963d1 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -16,7 +16,7 @@ use crate::node::{ }; use crate::node_ref::{NodeMask, NodeMaskBuilder}; use crate::node_watcher::{AttributeWatcher, NodeWatcher}; -use crate::passes::{DirtyNodeStates, PassDirection, TypeErasedState}; +use crate::passes::{Dependant, DirtyNodeStates, PassDirection, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; use crate::NodeId; @@ -128,19 +128,25 @@ impl RealDom { 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(¤t.this_type_id) + && !dependants.child.contains(¤t_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(¤t.this_type_id) + && !dependants.parent.contains(¤t_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) @@ -151,15 +157,19 @@ impl RealDom { } // 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(¤t.this_type_id) { - dependants.parent.push(current.this_type_id); + if !dependants.parent.contains(¤t_dependant) { + dependants.parent.push(current_dependant); } } PassDirection::ParentToChild => { - if !dependants.child.contains(¤t.this_type_id) { - dependants.child.push(current.this_type_id); + if !dependants.child.contains(¤t_dependant) { + dependants.child.push(current_dependant); } } _ => {} @@ -371,27 +381,27 @@ impl RealDom { } /// 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)) { + pub fn traverse_depth_first(&self, enter_shadow_doms: bool, mut f: impl FnMut(NodeRef)) { 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_doms); stack.extend(children.iter().copied().rev()); } } } /// 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)) { + pub fn traverse_breadth_first(&self, enter_shadow_doms: bool, mut f: impl FnMut(NodeRef)) { 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); } @@ -400,11 +410,15 @@ impl RealDom { } /// 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)) { + pub fn traverse_depth_first_mut( + &mut self, + enter_shadow_doms: bool, + mut f: impl FnMut(NodeMut), + ) { 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) { @@ -416,12 +430,16 @@ impl RealDom { } /// 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)) { + pub fn traverse_breadth_first_mut( + &mut self, + enter_shadow_doms: bool, + mut f: impl FnMut(NodeMut), + ) { 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); diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index 1f46216b2..1f26dc6a7 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -11,8 +11,6 @@ pub struct ShadowTree { pub shadow_roots: Vec, /// The node that children of the super tree should be inserted under. pub slot: Option, - /// The node in the super tree that the shadow_tree is attached to. - pub super_tree_root: NodeId, } /// A node in a tree. @@ -35,8 +33,43 @@ 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 { + // If this node is the root of a shadow_tree, return the node the shadow_tree is attached + let light_tree_root = self.light_tree_root(id); + match (light_tree_root, 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; + /// 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 { + 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; /// The shadow tree tree under the node. @@ -229,7 +262,6 @@ impl<'a> TreeMut for TreeMutView<'a> { let light_root_height; { let shadow_tree = ShadowTree { - super_tree_root: id, shadow_roots: shadow_roots.clone(), slot, }; @@ -251,6 +283,7 @@ impl<'a> TreeMut for TreeMutView<'a> { // 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().light_tree_root = Some(id); set_height(self, root, light_root_height + 1); } } @@ -319,7 +352,7 @@ fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) { 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); + 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 @@ -362,17 +395,17 @@ impl<'a> TreeRef for TreeMutView<'a> { fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree> { let node_data = &self.1; - node_data.get(id).unwrap().child_subtree.as_ref() + node_data.get(id).ok()?.child_subtree.as_ref() } fn slot_for_light_tree(&self, id: NodeId) -> Option { let node_data = &self.1; - node_data.get(id).unwrap().slot_for_light_tree + node_data.get(id).ok()?.slot_for_light_tree } fn light_tree_root(&self, id: NodeId) -> Option { let node_data = &self.1; - node_data.get(id).unwrap().light_tree_root + node_data.get(id).ok()?.light_tree_root } } diff --git a/packages/native-core/tests/called_minimally_on_build.rs b/packages/native-core/tests/called_minimally_on_build.rs index 394cf464a..f4e8cf6e1 100644 --- a/packages/native-core/tests/called_minimally_on_build.rs +++ b/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(false, |n| { $( assert_eq!(n.get::<$state>().unwrap().0, 1); )* diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index 3aa1b7462..b7c6e7c97 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -23,14 +23,26 @@ impl State for ColorState { fn update<'a>( &mut self, - _: NodeView, + view: NodeView, _: ::ElementBorrowed<'a>, parent: Option<::ElementBorrowed<'a>>, _: Vec<::ElementBorrowed<'a>>, _: &SendAnyMap, ) -> bool { - if let Some((parent,)) = parent { - self.color = parent.color; + 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 } @@ -74,11 +86,6 @@ impl State for LayoutState { _: Vec<::ElementBorrowed<'a>>, _: &SendAnyMap, ) -> bool { - println!( - "Updating layout state @{:?} {:?}", - parent.as_ref().map(|(p,)| p.size), - view.node_id() - ); if let Some(size) = view .attributes() .into_iter() @@ -93,7 +100,7 @@ impl State for LayoutState { .unwrap_or(0.0) as usize; } else if let Some((parent,)) = parent { if parent.size > 0 { - self.size -= 1; + self.size = parent.size - 1; } } true @@ -130,7 +137,7 @@ mod dioxus_elements { $(#[$attr])* pub struct $name; - #[allow(non_upper_case_globals)] + #[allow(non_upper_case_globals, unused)] impl $name { pub const TAG_NAME: &'static str = stringify!($name); pub const NAME_SPACE: Option<&'static str> = None; @@ -152,10 +159,14 @@ mod dioxus_elements { builder_constructors! { customelementslot { size: attr, + color: attr, }; customelementnoslot { + size: attr, + color: attr, }; testing132 { + color: attr, }; } } @@ -253,6 +264,7 @@ fn custom_elements_work() { cx.render(rsx! { customelementslot { size: "{count}", + color: "1", customelementslot { testing132 {} } @@ -277,7 +289,7 @@ fn custom_elements_work() { let ctx = SendAnyMap::new(); rdom.update_state(ctx); - for _ in 0..10 { + for i in 0..10usize { dom.wait_for_work().await; let mutations = dom.render_immediate(); @@ -287,13 +299,33 @@ fn custom_elements_work() { rdom.update_state(ctx); // render... - rdom.traverse_depth_first(|node| { + rdom.traverse_depth_first(true, |node| { let node_type = &*node.node_type(); - let indent = " ".repeat(node.height() as usize); + let height = node.height() as usize; + let indent = " ".repeat(height); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); let id = node.id(); println!("{indent}{id:?} {color:?} {size:?} {node_type:?}"); + match node_type { + NodeType::Element(el) => { + 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)); + } + } + _ => {} + } }); } }); From c2b873c650f68c8925cd01608af039de1c930cb0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 11:39:57 -0500 Subject: [PATCH 13/35] restore core changes --- packages/core/src/scopes.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5d1c661c0..5601d57fe 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -591,8 +591,6 @@ impl<'src> ScopeState { propagates: event.propagates, data, }); - } else { - println!("Failed to downcast event data") } })) }; From efa73057949afc1ca809d7072864062fd61d63fd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 13 Apr 2023 10:29:38 -0500 Subject: [PATCH 14/35] Port widgets to the new system --- packages/native-core/src/custom_element.rs | 33 ++++---- packages/native-core/src/real_dom.rs | 24 +++++- packages/native-core/tests/custom_element.rs | 6 +- packages/rink/examples/widgets.rs | 2 +- packages/rink/src/focus.rs | 4 +- packages/rink/src/hooks.rs | 42 +++------- packages/rink/src/layout.rs | 9 +-- packages/rink/src/lib.rs | 51 ++++++++++--- packages/rink/src/query.rs | 19 +---- packages/rink/src/render.rs | 7 +- packages/rink/src/widgets/button.rs | 42 +++++----- packages/rink/src/widgets/checkbox.rs | 27 ++++--- packages/rink/src/widgets/input.rs | 43 ++++++++--- packages/rink/src/widgets/mod.rs | 80 ++++++++++++++------ packages/rink/src/widgets/number.rs | 36 ++++----- packages/rink/src/widgets/password.rs | 36 ++++----- packages/rink/src/widgets/slider.rs | 28 ++++--- packages/rink/src/widgets/textbox.rs | 37 ++++----- 18 files changed, 286 insertions(+), 240 deletions(-) diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index 62d7e5368..675e928f1 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -9,7 +9,7 @@ use shipyard::Component; use crate::{ node::{FromAnyValue, NodeType}, node_ref::AttributeMask, - prelude::{NodeImmutable, NodeMut, RealDom}, + prelude::{NodeImmutable, NodeMut}, tree::TreeMut, NodeId, }; @@ -27,11 +27,15 @@ impl Default for CustomElementRegistry { } impl CustomElementRegistry { - pub fn register>(&mut self) { + pub fn register(&mut self) + where + F: CustomElementFactory, + U: CustomElementUpdater, + { self.builders.insert( - (W::NAME, W::NAMESPACE), + (F::NAME, F::NAMESPACE), CustomElementBuilder { - create: |dom| Box::new(W::create(dom)), + create: |node| Box::new(F::create(node)), }, ); } @@ -44,10 +48,7 @@ impl CustomElementRegistry { }; if let Some((tag, ns)) = element_tag { if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) { - let boxed_widget = { - let dom = node.real_dom_mut(); - (builder.create)(dom) - }; + let boxed_widget = { (builder.create)(node.reborrow()) }; let shadow_roots = boxed_widget.roots(); @@ -69,7 +70,7 @@ impl CustomElementRegistry { } struct CustomElementBuilder { - create: fn(&mut RealDom) -> Box>, + create: fn(NodeMut) -> Box>, } /// A controlled element that renders to a shadow DOM @@ -81,7 +82,7 @@ pub trait CustomElement: Send + Sync + 'stat const NAMESPACE: Option<&'static str> = None; /// Create a new widget *without mounting* it. - fn create(dom: &mut RealDom) -> Self; + fn create(node: NodeMut) -> Self; /// The root node of the widget. This must be static once the element is created. fn roots(&self) -> Vec; @@ -96,7 +97,7 @@ pub trait CustomElement: Send + Sync + 'stat } /// A factory for creating widgets -trait ElementFactory, V: FromAnyValue + Send + Sync = ()>: +pub trait CustomElementFactory, V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static { /// The tag the widget is registered under. @@ -106,21 +107,21 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = const NAMESPACE: Option<&'static str> = None; /// Create a new widget. - fn create(dom: &mut RealDom) -> W; + fn create(dom: NodeMut) -> W; } -impl, V: FromAnyValue + Send + Sync> ElementFactory for W { +impl, V: FromAnyValue + Send + Sync> CustomElementFactory for W { const NAME: &'static str = W::NAME; const NAMESPACE: Option<&'static str> = W::NAMESPACE; - fn create(dom: &mut RealDom) -> Self { - Self::create(dom) + fn create(node: NodeMut) -> Self { + Self::create(node) } } /// A trait for updating widgets -trait CustomElementUpdater: Send + Sync + 'static { +pub trait CustomElementUpdater: Send + Sync + 'static { /// Called when the attributes of the widget are changed. fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask); diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index e885963d1..61e9644a3 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -10,7 +10,10 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, RwLock}; -use crate::custom_element::{CustomElement, CustomElementManager, CustomElementRegistry}; +use crate::custom_element::{ + CustomElement, CustomElementFactory, CustomElementManager, CustomElementRegistry, + CustomElementUpdater, +}; use crate::node::{ ElementNode, FromAnyValue, NodeType, OwnedAttributeDiscription, OwnedAttributeValue, TextNode, }; @@ -478,7 +481,16 @@ impl RealDom { /// Registers a new custom element. pub fn register_custom_element>(&mut self) { - self.custom_elements.write().unwrap().register::() + self.register_custom_element_factory::() + } + + /// Registers a new custom element with a custom factory. + pub fn register_custom_element_factory(&mut self) + where + F: CustomElementFactory, + U: CustomElementUpdater, + { + self.custom_elements.write().unwrap().register::() } } @@ -678,6 +690,14 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeImmutable 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 { diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index b7c6e7c97..7bddeacc9 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -179,7 +179,8 @@ struct CustomElementWithSlot { impl CustomElement for CustomElementWithSlot { const NAME: &'static str = "customelementslot"; - fn create(dom: &mut RealDom<()>) -> Self { + fn create(mut node: NodeMut<()>) -> Self { + let dom = node.real_dom_mut(); let child = dom.create_node(ElementNode { tag: "div".into(), namespace: None, @@ -227,7 +228,8 @@ struct CustomElementWithNoSlot { impl CustomElement for CustomElementWithNoSlot { const NAME: &'static str = "customelementnoslot"; - fn create(dom: &mut RealDom<()>) -> Self { + fn create(mut node: NodeMut<()>) -> Self { + let dom = node.real_dom_mut(); let root = dom.create_node(ElementNode { tag: "div".into(), namespace: None, diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs index 3e709313f..1226d5c3d 100644 --- a/packages/rink/examples/widgets.rs +++ b/packages/rink/examples/widgets.rs @@ -77,7 +77,7 @@ impl Driver for Counter { _: bool, ) { match event_type { - "oninput" => { + "input" => { // when the button is clicked, increment the counter if let EventData::Form(input_event) = &*event { if let Ok(value) = input_event.value.parse::() { diff --git a/packages/rink/src/focus.rs b/packages/rink/src/focus.rs index e26f79b3d..4ede95485 100644 --- a/packages/rink/src/focus.rs +++ b/packages/rink/src/focus.rs @@ -183,7 +183,7 @@ impl FocusState { if forward { // find the closest focusable element after the current level - rdom.traverse_depth_first(|n| { + rdom.traverse_depth_first(true, |n| { let node_level = n.get::().unwrap().level; if node_level != *focus_level && node_level.focusable() @@ -200,7 +200,7 @@ impl FocusState { }); } else { // find the closest focusable element before the current level - rdom.traverse_depth_first(|n| { + rdom.traverse_depth_first(true, |n| { let node_level = n.get::().unwrap().level; if node_level != *focus_level && node_level.focusable() diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 341ab02ac..b54cc2eaf 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -25,7 +25,7 @@ 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}; #[derive(Debug, Clone, PartialEq)] pub struct Event { @@ -323,7 +323,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(); @@ -347,7 +347,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(); @@ -370,7 +370,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(); @@ -393,7 +393,7 @@ 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 { @@ -414,7 +414,7 @@ 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 { @@ -436,7 +436,7 @@ 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 { @@ -459,7 +459,7 @@ 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 { @@ -482,7 +482,7 @@ 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); @@ -505,7 +505,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(); @@ -528,7 +528,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(); @@ -550,7 +550,7 @@ impl InnerInputState { // update focus if was_released { let mut focus_id = None; - dom.traverse_depth_first(|node| { + dom.traverse_depth_first(true, |node| { let node_layout = layout .layout(node.get::().unwrap().node.unwrap()) .unwrap(); @@ -572,24 +572,6 @@ impl InnerInputState { // } } -fn get_abs_layout(node: NodeRef, dom: &RealDom, taffy: &Taffy) -> Layout { - let mut node_layout = *taffy - .layout(node.get::().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::().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>, queued_events: Rc>>, diff --git a/packages/rink/src/layout.rs b/packages/rink/src/layout.rs index b7eff57a6..3bb087596 100644 --- a/packages/rink/src/layout.rs +++ b/packages/rink/src/layout.rs @@ -25,12 +25,6 @@ impl PossiblyUninitalized { _ => panic!("uninitalized"), } } - pub fn ok(self) -> Option { - match self { - Self::Initialized(i) => Some(i), - _ => None, - } - } } impl Default for PossiblyUninitalized { 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, diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 2aab01664..18b1e62c7 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -6,7 +6,7 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -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}; @@ -262,7 +262,7 @@ pub fn render( } }, Some(evt) = event_reciever.next() => { - event_recieved=Some(evt); + event_recieved = Some(evt); } } } @@ -342,19 +342,50 @@ pub trait Driver { /// 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(rdom.get_mut(id).unwrap()); + 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) + }; - while let Some(mut node_mut) = node { - let parent_id = node_mut.parent_id(); - if let Some(mut widget) = node_mut - .get_mut::() - .map(|w| w.clone()) { - widget.handle_event(event, &mut node_mut) + // 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::() + .map(|w| w.clone()) + { + widget.handle_event(event, node_mut) + } } + if !event.bubbles { + // println!("event does not bubble"); break; } - node = parent_id.map(|id| rdom.get_mut(id).unwrap()); + node = parent_id; } } + +pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout { + let mut node_layout = *taffy + .layout(node.get::().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::().unwrap().node.unwrap()) + .unwrap(); + node_layout.location.x += parent_layout.location.x; + node_layout.location.y += parent_layout.location.y; + } + node_layout +} diff --git a/packages/rink/src/query.rs b/packages/rink/src/query.rs index c204e1953..3b8f319cf 100644 --- a/packages/rink/src/query.rs +++ b/packages/rink/src/query.rs @@ -8,7 +8,7 @@ use taffy::{ 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. @@ -95,21 +95,8 @@ impl<'a> ElementRef<'a> { } pub(crate) fn get_layout(node: NodeRef, stretch: &Taffy) -> Option { - let layout = stretch - .layout(node.get::().unwrap().node.ok()?) - .ok()?; - - let mut current_node_id = node.parent_id(); - let mut pos = layout.location; - let rdom = node.real_dom(); - while let Some(node) = current_node_id.and_then(|id| rdom.get(id)) { - let current_layout = stretch - .layout(node.get::().unwrap().node.ok()?) - .ok()?; - pos.x += current_layout.location.x; - pos.y += current_layout.location.y; - current_node_id = node.parent_id(); - } + let layout = get_abs_layout(node, stretch); + let pos = layout.location; Some(Layout { order: layout.order, diff --git a/packages/rink/src/render.rs b/packages/rink/src/render.rs index ef433976d..19d642720 100644 --- a/packages/rink/src/render.rs +++ b/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); } } diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index 2eaddfe59..e4d3754f1 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -2,14 +2,14 @@ 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, NodeTypeMut, RealDom}, - utils::widget_watcher::Widget, + real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom}, NodeId, }; -use shipyard::UniqueViewMut; +use shipyard::UniqueView; use crate::FormData; @@ -17,7 +17,6 @@ use super::{RinkWidget, WidgetContext}; #[derive(Debug, Default)] pub(crate) struct Button { - div_id: NodeId, text_id: NodeId, value: String, } @@ -88,14 +87,14 @@ impl Button { } } - fn switch(&mut self, ctx: &mut WidgetContext) { + 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: self.div_id, + id: node.id(), name: "input", data: crate::EventData::Form(data), bubbles: true, @@ -103,10 +102,14 @@ impl Button { } } -impl Widget for Button { +impl CustomElement for Button { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -127,11 +130,8 @@ impl Widget for Button { root.add_event_listener("keydown"); root.add_event_listener("click"); - let div_id = root.id(); - root.add_child(text_id); Self { - div_id, text_id, value: value.unwrap_or_default(), } @@ -139,7 +139,7 @@ impl Widget for Button { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -175,16 +175,18 @@ impl RinkWidget for Button { fn handle_event( &mut self, event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, + mut node: dioxus_native_core::real_dom::NodeMut, ) { - let mut ctx: UniqueViewMut = node - .real_dom_mut() - .raw_world_mut() - .borrow() - .expect("expected widget context"); + let mut ctx: WidgetContext = { + node.real_dom_mut() + .raw_world_mut() + .borrow::>() + .expect("expected widget context") + .clone() + }; match event.name { - "click" => self.switch(&mut ctx), + "click" => self.switch(&mut ctx, node), "keydown" => { if let crate::EventData::Keyboard(data) = &event.data { if !data.is_auto_repeating() @@ -194,7 +196,7 @@ impl RinkWidget for Button { _ => false, } { - self.switch(&mut ctx); + self.switch(&mut ctx, node); } } } diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index 943db5213..24b3f6244 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -2,11 +2,11 @@ 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}, - utils::widget_watcher::Widget, NodeId, }; use shipyard::UniqueView; @@ -91,7 +91,7 @@ impl CheckBox { == "true"; } - fn write_value(&self, root: &mut NodeMut) { + 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") }; @@ -117,7 +117,7 @@ impl CheckBox { } } - fn switch(&mut self, node: &mut NodeMut) { + fn switch(&mut self, mut node: NodeMut) { let new_state = !self.checked; let data = FormData { @@ -147,10 +147,14 @@ impl CheckBox { } } -impl Widget for CheckBox { +impl CustomElement for CheckBox { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -172,7 +176,6 @@ impl Widget for CheckBox { root.add_event_listener("click"); root.add_event_listener("keydown"); let div_id = root.id(); - root.add_child(text_id); let myself = Self { div_id, @@ -187,7 +190,7 @@ impl Widget for CheckBox { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -199,7 +202,7 @@ impl Widget for CheckBox { self.update_size_attr(&mut el); self.update_checked_attr(&el); } - self.write_value(&mut root); + self.write_value(root); } AttributeMask::Some(attrs) => { { @@ -216,7 +219,7 @@ impl Widget for CheckBox { } } if attrs.contains("checked") { - self.write_value(&mut root); + self.write_value(root); } } } @@ -224,11 +227,7 @@ impl Widget for CheckBox { } impl RinkWidget for CheckBox { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) { match event.name { "click" => self.switch(node), "keydown" => { diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs index 2253ae096..c6a3e56b9 100644 --- a/packages/rink/src/widgets/input.rs +++ b/packages/rink/src/widgets/input.rs @@ -1,6 +1,6 @@ use dioxus_native_core::{ - node::OwnedAttributeDiscription, prelude::NodeType, real_dom::NodeImmutable, - utils::widget_watcher::Widget, + custom_element::CustomElement, node::OwnedAttributeDiscription, prelude::NodeType, + real_dom::NodeImmutable, }; use super::{ @@ -18,10 +18,32 @@ pub(crate) enum Input { Slider(Slider), } -impl Widget for Input { +impl CustomElement for Input { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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 { + 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(); @@ -42,7 +64,10 @@ impl Widget for Input { namespace: None, }) .and_then(|value| value.as_text()); - match input_type { + match input_type + .map(|type_| type_.trim().to_lowercase()) + .as_deref() + { Some("button") => { drop(node_type); Input::Button(Button::create(root)) @@ -76,7 +101,7 @@ impl Widget for Input { fn attributes_changed( &mut self, - root: dioxus_native_core::real_dom::NodeMut<()>, + root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match self { @@ -103,11 +128,7 @@ impl Widget for Input { } impl RinkWidget for Input { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + 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); diff --git a/packages/rink/src/widgets/mod.rs b/packages/rink/src/widgets/mod.rs index 283ebbcd9..27dea751d 100644 --- a/packages/rink/src/widgets/mod.rs +++ b/packages/rink/src/widgets/mod.rs @@ -9,8 +9,8 @@ mod textbox; use std::sync::{Arc, RwLock}; use dioxus_native_core::{ - real_dom::RealDom, - utils::widget_watcher::{Widget, WidgetFactory, WidgetUpdater, WidgetWatcher}, + custom_element::{CustomElement, CustomElementUpdater}, + real_dom::{NodeMut, RealDom}, }; use futures_channel::mpsc::UnboundedSender; use shipyard::{Component, Unique}; @@ -21,42 +21,64 @@ pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender, RinkWidgetTraitObject>(); - - widget_watcher.attach(rdom); + rdom.register_custom_element::>(); } -trait RinkWidget: Sync + Send + Widget + 'static { - fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut); +trait RinkWidget: Sync + Send + CustomElement + 'static { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut); } -pub trait RinkWidgetResponder: WidgetUpdater { - fn handle_event(&mut self, event: &Event, node: &mut 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 RinkWidgetResponder for W { - fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) { RinkWidget::handle_event(self, event, node) } } -struct RinkWidgetTraitObjectFactory { +struct RinkWidgetWrapper { + inner: RinkWidgetTraitObject, _marker: std::marker::PhantomData, } -impl WidgetFactory for RinkWidgetTraitObjectFactory { +impl CustomElement for RinkWidgetWrapper { const NAME: &'static str = W::NAME; - fn create(node: &mut dioxus_native_core::real_dom::NodeMut) -> RinkWidgetTraitObject { + const NAMESPACE: Option<&'static str> = W::NAMESPACE; + + fn create(mut node: NodeMut) -> Self { let myself = RinkWidgetTraitObject { - widget: Arc::new(RwLock::new(W::create(node))), + 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()); - myself + + 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 { + let widget = self.inner.widget.read().unwrap(); + widget.roots() + } + + fn slot(&self) -> Option { + let widget = self.inner.widget.read().unwrap(); + widget.slot() } } @@ -65,25 +87,35 @@ pub(crate) struct RinkWidgetTraitObject { widget: Arc>, } -impl WidgetUpdater for RinkWidgetTraitObject { +impl CustomElementUpdater for RinkWidgetTraitObject { fn attributes_changed( &mut self, - root: dioxus_native_core::real_dom::NodeMut, + 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(root, attributes); + widget.attributes_changed(light_root, attributes); + } + + fn roots(&self) -> Vec { + let widget = self.widget.read().unwrap(); + widget.roots() + } + + fn slot(&self) -> Option { + let widget = self.widget.read().unwrap(); + widget.slot() } } impl RinkWidgetResponder for RinkWidgetTraitObject { - fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) { + 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)] +#[derive(Unique, Clone)] pub(crate) struct WidgetContext { sender: UnboundedSender, } diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs index 2925e2966..c83d514b5 100644 --- a/packages/rink/src/widgets/number.rs +++ b/packages/rink/src/widgets/number.rs @@ -3,14 +3,12 @@ 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}, - widget_watcher::Widget, - }, + utils::cursor::{Cursor, Pos}, NodeId, }; use shipyard::UniqueView; @@ -169,7 +167,7 @@ impl Number { self.text = (num - 1.0).to_string(); } - fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { let key = data.key(); let is_text = match key.clone() { Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true, @@ -235,14 +233,11 @@ impl Number { } } - fn handle_mousemove(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } // textboxs are only one line tall new.row = 0; @@ -254,12 +249,9 @@ impl Number { } } - fn handle_mousedown(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } // textboxs are only one line tall new.row = 0; @@ -297,10 +289,14 @@ impl Number { } } -impl Widget for Number { +impl CustomElement for Number { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -364,8 +360,6 @@ impl Widget for Number { root.add_event_listener("keydown"); root.add_event_listener("focusout"); - root.add_child(div_wrapper_id); - Self { pre_cursor_text: pre_text_id, highlighted_text: highlighted_text_id, @@ -379,7 +373,7 @@ impl Widget for Number { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -418,11 +412,7 @@ impl Widget for Number { } impl RinkWidget for Number { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + 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 { diff --git a/packages/rink/src/widgets/password.rs b/packages/rink/src/widgets/password.rs index 483b1e175..7e570a631 100644 --- a/packages/rink/src/widgets/password.rs +++ b/packages/rink/src/widgets/password.rs @@ -3,14 +3,12 @@ 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}, - widget_watcher::Widget, - }, + utils::cursor::{Cursor, Pos}, NodeId, }; use shipyard::UniqueView; @@ -159,7 +157,7 @@ impl Password { } } - fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { let key = data.key(); let modifiers = data.modifiers(); let code = data.code(); @@ -203,14 +201,11 @@ impl Password { } } - fn handle_mousemove(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } // textboxs are only one line tall new.row = 0; @@ -222,12 +217,9 @@ impl Password { } } - fn handle_mousedown(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } // textboxs are only one line tall new.row = 0; @@ -265,10 +257,14 @@ impl Password { } } -impl Widget for Password { +impl CustomElement for Password { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -332,8 +328,6 @@ impl Widget for Password { root.add_event_listener("keydown"); root.add_event_listener("focusout"); - root.add_child(div_wrapper_id); - Self { pre_cursor_text: pre_text_id, highlighted_text: highlighted_text_id, @@ -347,7 +341,7 @@ impl Widget for Password { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -386,11 +380,7 @@ impl Widget for Password { } impl RinkWidget for Password { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + 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 { diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index e1fa4f980..f97c12bf6 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -2,11 +2,11 @@ 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}, - utils::widget_watcher::Widget, NodeId, }; use shipyard::UniqueView; @@ -205,7 +205,7 @@ impl Slider { } } - fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { let key = data.key(); let step = self.step(); @@ -227,7 +227,7 @@ impl Slider { self.write_value(rdom, id); } - fn handle_mousemove(&mut self, root: &mut NodeMut, data: &MouseData) { + 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(); @@ -250,10 +250,14 @@ impl Slider { } } -impl Widget for Slider { +impl CustomElement for Slider { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -364,12 +368,10 @@ impl Widget for Slider { div_wrapper.add_child(cursor_span_id); div_wrapper.add_child(post_cursor_div_id); - div_wrapper.add_event_listener("mousemove"); - div_wrapper.add_event_listener("mousedown"); + root.add_event_listener("mousemove"); + root.add_event_listener("mousedown"); root.add_event_listener("keydown"); - root.add_child(div_wrapper_id); - Self { pre_cursor_div: pre_cursor_div_id, post_cursor_div: post_cursor_div_id, @@ -381,7 +383,7 @@ impl Widget for Slider { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -428,11 +430,7 @@ impl Widget for Slider { } impl RinkWidget for Slider { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + 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 { diff --git a/packages/rink/src/widgets/textbox.rs b/packages/rink/src/widgets/textbox.rs index 16f73fd78..8eaadb7a7 100644 --- a/packages/rink/src/widgets/textbox.rs +++ b/packages/rink/src/widgets/textbox.rs @@ -3,14 +3,12 @@ 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}, - widget_watcher::Widget, - }, + utils::cursor::{Cursor, Pos}, NodeId, }; use shipyard::UniqueView; @@ -159,7 +157,7 @@ impl TextBox { } } - fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) { + fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { let key = data.key(); let modifiers = data.modifiers(); let code = data.code(); @@ -203,14 +201,12 @@ impl TextBox { } } - fn handle_mousemove(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } + // textboxs are only one line tall new.row = 0; @@ -222,12 +218,9 @@ impl TextBox { } } - fn handle_mousedown(&mut self, root: &mut NodeMut, data: &MouseData) { + 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); - if self.border { - new.col = new.col.saturating_sub(1); - } // textboxs are only one line tall new.row = 0; @@ -265,10 +258,14 @@ impl TextBox { } } -impl Widget for TextBox { +impl CustomElement for TextBox { const NAME: &'static str = "input"; - fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self { + fn roots(&self) -> Vec { + 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") }; @@ -332,8 +329,6 @@ impl Widget for TextBox { root.add_event_listener("keydown"); root.add_event_listener("focusout"); - root.add_child(div_wrapper_id); - Self { pre_cursor_text: pre_text_id, highlighted_text: highlighted_text_id, @@ -347,7 +342,7 @@ impl Widget for TextBox { fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut<()>, + mut root: dioxus_native_core::real_dom::NodeMut, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { match attributes { @@ -386,11 +381,7 @@ impl Widget for TextBox { } impl RinkWidget for TextBox { - fn handle_event( - &mut self, - event: &crate::Event, - node: &mut dioxus_native_core::real_dom::NodeMut, - ) { + fn handle_event(&mut self, event: &crate::Event, node: NodeMut) { match event.name { "keydown" => { if let EventData::Keyboard(data) = &event.data { From baeb4251bab9b601be4f994534627f98ecf6df1f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 16 Apr 2023 14:05:27 -0500 Subject: [PATCH 15/35] refactor text inputs --- packages/rink/src/widgets/mod.rs | 1 + packages/rink/src/widgets/number.rs | 465 +++---------------------- packages/rink/src/widgets/password.rs | 422 +--------------------- packages/rink/src/widgets/text_like.rs | 444 +++++++++++++++++++++++ packages/rink/src/widgets/textbox.rs | 424 +--------------------- 5 files changed, 504 insertions(+), 1252 deletions(-) create mode 100644 packages/rink/src/widgets/text_like.rs diff --git a/packages/rink/src/widgets/mod.rs b/packages/rink/src/widgets/mod.rs index 27dea751d..c3ecaad78 100644 --- a/packages/rink/src/widgets/mod.rs +++ b/packages/rink/src/widgets/mod.rs @@ -4,6 +4,7 @@ mod input; mod number; mod password; mod slider; +mod text_like; mod textbox; use std::sync::{Arc, RwLock}; diff --git a/packages/rink/src/widgets/number.rs b/packages/rink/src/widgets/number.rs index c83d514b5..371c8b41d 100644 --- a/packages/rink/src/widgets/number.rs +++ b/packages/rink/src/widgets/number.rs @@ -1,291 +1,28 @@ -use std::{collections::HashMap, io::stdout}; - -use crossterm::{cursor::MoveTo, execute}; -use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData}; +use dioxus_html::input_data::keyboard_types::Key; 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}, + real_dom::{NodeImmutable, RealDom}, NodeId, }; -use shipyard::UniqueView; -use taffy::geometry::Point; -use crate::{query::get_layout, Event, EventData, FormData, Query}; +use crate::EventData; -use super::{RinkWidget, WidgetContext}; +use super::{text_like::TextLike, RinkWidget}; #[derive(Debug, Default)] pub(crate) struct Number { - 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, + text: TextLike, } impl Number { - 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 increase(&mut self, rdom: &mut RealDom, id: NodeId) { + let num = self.text.text().parse::().unwrap_or(0.0); + self.text.set_text((num + 1.0).to_string(), rdom, id); } - 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::() { - 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::().ok().filter(|num| *num > 3)) - .is_some() - || height - .strip_prefix("px") - .and_then(|n| n.parse::().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; - } - } - - 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() = text_before_first_cursor.to_string(); - } - - 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() = text_highlighted.to_string(); - } - - 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() = text_after_second_cursor.to_string(); - } - - // send the event - { - let world = rdom.raw_world_mut(); - let data = FormData { - value: self.text.clone(), - values: HashMap::new(), - files: None, - }; - let ctx: UniqueView = world.borrow().expect("expected widget context"); - - ctx.send(Event { - id, - name: "input", - data: EventData::Form(data), - bubbles: true, - }); - } - } - - fn increase(&mut self) { - let num = self.text.parse::().unwrap_or(0.0); - self.text = (num + 1.0).to_string(); - } - - fn decrease(&mut self) { - let num = self.text.parse::().unwrap_or(0.0); - self.text = (num - 1.0).to_string(); - } - - fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) { - let key = data.key(); - let is_text = 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_text { - 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 = 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(); - } - } else { - match key { - Key::ArrowUp => { - self.increase(); - } - Key::ArrowDown => { - self.decrease(); - } - _ => (), - } - - 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 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 = 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) + fn decrease(&mut self, rdom: &mut RealDom, id: NodeId) { + let num = self.text.text().parse::().unwrap_or(0.0); + self.text.set_text((num - 1.0).to_string(), rdom, id); } } @@ -293,162 +30,62 @@ impl CustomElement for Number { const NAME: &'static str = "input"; fn roots(&self) -> Vec { - vec![self.div_wrapper] + self.text.roots() } 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() + Number { + text: TextLike::create(root.reborrow()), } } fn attributes_changed( &mut self, - mut root: dioxus_native_core::real_dom::NodeMut, + 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); - } - } - } + self.text.attributes_changed(root, attributes) } } impl RinkWidget for Number { - 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); + 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; } - - "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(); - } - - _ => {} } + + self.text.handle_event(event, node) } } diff --git a/packages/rink/src/widgets/password.rs b/packages/rink/src/widgets/password.rs index 7e570a631..9a8228ad1 100644 --- a/packages/rink/src/widgets/password.rs +++ b/packages/rink/src/widgets/password.rs @@ -1,422 +1,12 @@ -use std::{collections::HashMap, io::stdout}; +use super::text_like::{TextLike, TextLikeController}; -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) type Password = TextLike; #[derive(Debug, Default)] -pub(crate) struct Password { - 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, -} +pub(crate) struct PasswordController; -impl Password { - 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::() { - 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::().ok().filter(|num| *num > 3)) - .is_some() - || height - .strip_prefix("px") - .and_then(|n| n.parse::().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; - } - } - - 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() = ".".repeat(text_before_first_cursor.len()); - } - - 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() = ".".repeat(text_highlighted.len()); - } - - 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() = ".".repeat(text_after_second_cursor.len()); - } - - // send the event - { - let world = rdom.raw_world_mut(); - let data = FormData { - value: self.text.clone(), - values: HashMap::new(), - files: None, - }; - let ctx: UniqueView = 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 = 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 = 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 CustomElement for Password { - const NAME: &'static str = "input"; - - fn roots(&self) -> Vec { - 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 RinkWidget for Password { - 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_mousedown(node, data); - } - } - - "mouseup" => { - self.dragging = false; - } - - "mouseleave" => { - self.dragging = false; - } - - "mouseenter" => { - self.dragging = false; - } - - "focusout" => { - execute!(stdout(), MoveTo(0, 1000)).unwrap(); - } - - _ => {} - } +impl TextLikeController for PasswordController { + fn display_text(&self, text: &str) -> String { + text.chars().map(|_| '.').collect() } } diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs new file mode 100644 index 000000000..b5874c776 --- /dev/null +++ b/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 { + 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, + controller: C, +} + +impl TextLike { + 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::() { + 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::().ok().filter(|num| *num > 3)) + .is_some() + || height + .strip_prefix("px") + .and_then(|n| n.parse::().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 = 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 = 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 = 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 CustomElement for TextLike { + const NAME: &'static str = "input"; + + fn roots(&self) -> Vec { + 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 RinkWidget for TextLike { + 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(); + } + + _ => {} + } + } +} diff --git a/packages/rink/src/widgets/textbox.rs b/packages/rink/src/widgets/textbox.rs index 8eaadb7a7..df9bd7758 100644 --- a/packages/rink/src/widgets/textbox.rs +++ b/packages/rink/src/widgets/textbox.rs @@ -1,423 +1,3 @@ -use std::{collections::HashMap, io::stdout}; +use super::text_like::TextLike; -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}; - -#[derive(Debug, Default)] -pub(crate) struct TextBox { - 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, -} - -impl TextBox { - 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::() { - 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::().ok().filter(|num| *num > 3)) - .is_some() - || height - .strip_prefix("px") - .and_then(|n| n.parse::().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; - } - } - - 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() = text_before_first_cursor.to_string(); - } - - 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() = text_highlighted.to_string(); - } - - 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() = text_after_second_cursor.to_string(); - } - - // send the event - { - let world = rdom.raw_world_mut(); - let data = FormData { - value: self.text.clone(), - values: HashMap::new(), - files: None, - }; - let ctx: UniqueView = 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 = 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 = 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 CustomElement for TextBox { - const NAME: &'static str = "input"; - - fn roots(&self) -> Vec { - 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 RinkWidget for TextBox { - 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(); - } - - _ => {} - } - } -} +pub(crate) type TextBox = TextLike; From 9877b0f0b12270b1041b7be06bf6604aa9be7738 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Apr 2023 12:41:54 -0500 Subject: [PATCH 16/35] finish shadow dom docs --- packages/native-core/examples/custom_attr.rs | 2 +- packages/native-core/examples/font_size.rs | 2 +- packages/native-core/examples/simple.rs | 2 +- .../native-core/examples/simple_dioxus.rs | 2 +- packages/native-core/src/custom_element.rs | 81 ++++++++++--------- packages/native-core/src/real_dom.rs | 62 ++++++++++++-- packages/native-core/src/tree.rs | 31 ++++--- .../tests/called_minimally_on_build.rs | 2 +- packages/native-core/tests/custom_element.rs | 2 +- packages/rink/src/focus.rs | 4 +- packages/rink/src/hooks.rs | 2 +- 11 files changed, 123 insertions(+), 69 deletions(-) diff --git a/packages/native-core/examples/custom_attr.rs b/packages/native-core/examples/custom_attr.rs index ee78a69f4..e7bbb36ea 100644 --- a/packages/native-core/examples/custom_attr.rs +++ b/packages/native-core/examples/custom_attr.rs @@ -215,7 +215,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(true, |node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/font_size.rs b/packages/native-core/examples/font_size.rs index 4ff6b876d..40261baa2 100644 --- a/packages/native-core/examples/font_size.rs +++ b/packages/native-core/examples/font_size.rs @@ -160,7 +160,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(true, |node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let font_size = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple.rs b/packages/native-core/examples/simple.rs index b44f05e63..276d06d08 100644 --- a/packages/native-core/examples/simple.rs +++ b/packages/native-core/examples/simple.rs @@ -206,7 +206,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(true, |node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/examples/simple_dioxus.rs b/packages/native-core/examples/simple_dioxus.rs index c5aebd5d3..af0e4ca00 100644 --- a/packages/native-core/examples/simple_dioxus.rs +++ b/packages/native-core/examples/simple_dioxus.rs @@ -234,7 +234,7 @@ fn main() -> Result<(), Box> { let _to_rerender = rdom.update_state(ctx); // render... - rdom.traverse_depth_first(true, |node| { + rdom.traverse_depth_first_advanced(true, |node| { let indent = " ".repeat(node.height() as usize); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index 675e928f1..3675089f6 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -1,5 +1,10 @@ -//! A custom element is a controlled element that renders to a shadow DOM. -//! Each custom element is registered with a element name +//! 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}; @@ -48,22 +53,22 @@ impl CustomElementRegistry { }; if let Some((tag, ns)) = element_tag { if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) { - let boxed_widget = { (builder.create)(node.reborrow()) }; + let boxed_custom_element = { (builder.create)(node.reborrow()) }; - let shadow_roots = boxed_widget.roots(); + 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_widget.slot(), + boxed_custom_element.slot(), ); - let boxed_widget = CustomElementManager { - inner: Arc::new(RwLock::new(boxed_widget)), + let boxed_custom_element = CustomElementManager { + inner: Arc::new(RwLock::new(boxed_custom_element)), }; - node.insert(boxed_widget); + node.insert(boxed_custom_element); } } } @@ -73,40 +78,49 @@ struct CustomElementBuilder { create: fn(NodeMut) -> Box>, } -/// A controlled element that renders to a shadow DOM +/// 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: Send + Sync + 'static { - /// The tag the widget is registered under. + /// The tag of the element const NAME: &'static str; - /// The namespace the widget is registered under. + /// The namespace of the element const NAMESPACE: Option<&'static str> = None; - /// Create a new widget *without mounting* it. - fn create(node: NodeMut) -> Self; + /// 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) -> Self; - /// The root node of the widget. This must be static once the element is created. + /// The root node of the custom element. These roots must be not change once the element is created. fn roots(&self) -> Vec; - /// The slot to render children of the element into. This must be static once the element is created. + /// The slot to render children of the element into. The slot must be not change once the element is created. fn slot(&self) -> Option { None } - /// Called when the attributes of the widget are changed. + /// 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, attributes: &AttributeMask); } -/// A factory for creating widgets +/// A factory for creating custom elements +/// +/// Register with [`RealDom::register_custom_element_with_factory`] pub trait CustomElementFactory, V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static { - /// The tag the widget is registered under. + /// The tag of the element const NAME: &'static str; - /// The namespace the widget is registered under. + /// The namespace of the element const NAMESPACE: Option<&'static str> = None; - /// Create a new widget. + /// 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) -> W; } @@ -120,16 +134,19 @@ impl, V: FromAnyValue + Send + Sync> CustomElementFactory: Send + Sync + 'static { - /// Called when the attributes of the widget are changed. + /// 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, attributes: &AttributeMask); - /// The root node of the widget. + /// The root node of the custom element. These roots must be not change once the element is created. fn roots(&self) -> Vec; - /// The slot to render children of the element into. - fn slot(&self) -> Option; + /// The slot to render children of the element into. The slot must be not change once the element is created. + fn slot(&self) -> Option { + None + } } impl, V: FromAnyValue + Send + Sync> CustomElementUpdater for W { @@ -146,23 +163,13 @@ impl, V: FromAnyValue + Send + Sync> CustomElementUpdater } } -/// A concrete structure for managing a any widget. +/// A dynamic trait object wrapper for [`CustomElementUpdater`] #[derive(Component, Clone)] -pub struct CustomElementManager { +pub(crate) struct CustomElementManager { inner: Arc>>>, } impl CustomElementManager { - /// The root node of the widget's shadow DOM. - pub fn roots(&self) -> Vec { - self.inner.read().unwrap().roots() - } - - /// The slot to render children of the element into. - pub fn slot(&self) -> Option { - self.inner.read().unwrap().slot() - } - /// Update the custom element based on attributes changed. pub fn on_attributes_changed(&self, light_root: NodeMut, attributes: &AttributeMask) { self.inner diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 61e9644a3..a0eff4b2c 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -384,20 +384,35 @@ impl RealDom { } /// Traverses the dom in a depth first manner, calling the provided function on each node. - pub fn traverse_depth_first(&self, enter_shadow_doms: bool, mut f: impl FnMut(NodeRef)) { + /// 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), + ) { 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_advanced(id, enter_shadow_doms); + 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)) { + 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, enter_shadow_doms: bool, mut f: impl FnMut(NodeRef)) { + /// 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), + ) { let mut queue = VecDeque::new(); queue.push_back(self.root_id()); let tree = self.tree_ref(); @@ -412,8 +427,14 @@ impl RealDom { } } + /// 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)) { + 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( + /// 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), @@ -432,8 +453,14 @@ impl RealDom { } } + /// 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)) { + 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( + /// 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), @@ -453,6 +480,11 @@ impl RealDom { } } + /// 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)) { + 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 + 'static + Send + Sync) { self.node_watchers.write().unwrap().push(Box::new(watcher)); @@ -481,11 +513,11 @@ impl RealDom { /// Registers a new custom element. pub fn register_custom_element>(&mut self) { - self.register_custom_element_factory::() + self.register_custom_element_with_factory::() } /// Registers a new custom element with a custom factory. - pub fn register_custom_element_factory(&mut self) + pub fn register_custom_element_with_factory(&mut self) where F: CustomElementFactory, U: CustomElementUpdater, @@ -563,6 +595,14 @@ pub trait NodeImmutable: 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 { + 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 { @@ -581,6 +621,14 @@ pub trait NodeImmutable: 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 { + 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 { diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index 1f26dc6a7..3f3777607 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -4,10 +4,10 @@ use crate::NodeId; use shipyard::{Component, EntitiesViewMut, Get, View, ViewMut}; use std::fmt::Debug; -/// A shadow_tree of a tree. +/// 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 + /// The root of the shadow tree pub shadow_roots: Vec, /// The node that children of the super tree should be inserted under. pub slot: Option, @@ -22,7 +22,7 @@ pub struct Node { /// 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, /// If this node is a root of a shadow_tree, this is the node whose child_subtree is that shadow_tree. - light_tree_root: Option, + root_for_light_tree: Option, height: u16, } @@ -37,8 +37,8 @@ pub trait TreeRef { #[inline] fn parent_id_advanced(&self, id: NodeId, enter_shadow_dom: bool) -> Option { // If this node is the root of a shadow_tree, return the node the shadow_tree is attached - let light_tree_root = self.light_tree_root(id); - match (light_tree_root, enter_shadow_dom) { + 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); @@ -74,11 +74,10 @@ pub trait TreeRef { fn children_ids(&self, id: NodeId) -> Vec; /// The shadow tree tree under the node. fn shadow_tree(&self, id: NodeId) -> Option<&ShadowTree>; - // TODO: rethink naming /// The node that contains the shadow tree this node is a slot for fn slot_for_light_tree(&self, id: NodeId) -> Option; /// The node that contains the shadow tree this node is a root of - fn light_tree_root(&self, id: NodeId) -> Option; + fn root_for_light_tree(&self, id: NodeId) -> Option; /// The height of the node. fn height(&self, id: NodeId) -> Option; /// Returns true if the node exists. @@ -132,8 +131,8 @@ impl<'a> TreeRef for TreeRefView<'a> { self.get(id).ok()?.slot_for_light_tree } - fn light_tree_root(&self, id: NodeId) -> Option { - self.get(id).ok()?.light_tree_root + fn root_for_light_tree(&self, id: NodeId) -> Option { + self.get(id).ok()?.root_for_light_tree } } @@ -151,14 +150,14 @@ impl<'a> TreeMut for TreeMutView<'a> { // If this node is a slot in a shadow_tree, remove it from the shadow_tree. if let Some(light_tree) = light_tree { - let light_tree_root = (&mut tree.1).get(light_tree).unwrap(); + let root_for_light_tree = (&mut tree.1).get(light_tree).unwrap(); - if let Some(shadow_tree) = &mut light_tree_root.child_subtree { + if let Some(shadow_tree) = &mut root_for_light_tree.child_subtree { shadow_tree.slot = None; } debug_assert!( - light_tree_root.children.is_empty(), + root_for_light_tree.children.is_empty(), "ShadowTree root should have no children when slot is removed." ); } @@ -186,7 +185,7 @@ impl<'a> TreeMut for TreeMutView<'a> { height: 0, child_subtree: None, slot_for_light_tree: None, - light_tree_root: None, + root_for_light_tree: None, }, ); } @@ -283,7 +282,7 @@ impl<'a> TreeMut for TreeMutView<'a> { // 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().light_tree_root = Some(id); + (&mut self.1).get(root).unwrap().root_for_light_tree = Some(id); set_height(self, root, light_root_height + 1); } } @@ -403,9 +402,9 @@ impl<'a> TreeRef for TreeMutView<'a> { node_data.get(id).ok()?.slot_for_light_tree } - fn light_tree_root(&self, id: NodeId) -> Option { + fn root_for_light_tree(&self, id: NodeId) -> Option { let node_data = &self.1; - node_data.get(id).ok()?.light_tree_root + node_data.get(id).ok()?.root_for_light_tree } } diff --git a/packages/native-core/tests/called_minimally_on_build.rs b/packages/native-core/tests/called_minimally_on_build.rs index f4e8cf6e1..3fa789a6c 100644 --- a/packages/native-core/tests/called_minimally_on_build.rs +++ b/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(false, |n| { + dom.traverse_depth_first_advanced(false, |n| { $( assert_eq!(n.get::<$state>().unwrap().0, 1); )* diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index 7bddeacc9..935ac94d0 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -301,7 +301,7 @@ fn custom_elements_work() { rdom.update_state(ctx); // render... - rdom.traverse_depth_first(true, |node| { + rdom.traverse_depth_first_advanced(true, |node| { let node_type = &*node.node_type(); let height = node.height() as usize; let indent = " ".repeat(height); diff --git a/packages/rink/src/focus.rs b/packages/rink/src/focus.rs index 4ede95485..e26f79b3d 100644 --- a/packages/rink/src/focus.rs +++ b/packages/rink/src/focus.rs @@ -183,7 +183,7 @@ impl FocusState { if forward { // find the closest focusable element after the current level - rdom.traverse_depth_first(true, |n| { + rdom.traverse_depth_first(|n| { let node_level = n.get::().unwrap().level; if node_level != *focus_level && node_level.focusable() @@ -200,7 +200,7 @@ impl FocusState { }); } else { // find the closest focusable element before the current level - rdom.traverse_depth_first(true, |n| { + rdom.traverse_depth_first(|n| { let node_level = n.get::().unwrap().level; if node_level != *focus_level && node_level.focusable() diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index b54cc2eaf..55a9274aa 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -550,7 +550,7 @@ impl InnerInputState { // update focus if was_released { let mut focus_id = None; - dom.traverse_depth_first(true, |node| { + dom.traverse_depth_first(|node| { let node_layout = layout .layout(node.get::().unwrap().node.unwrap()) .unwrap(); From 5d9d9e744167332bd90e8fca418a6af85d1c8e4a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Apr 2023 13:44:44 -0500 Subject: [PATCH 17/35] fix clippy --- packages/native-core/tests/custom_element.rs | 27 +++++++++----------- packages/rink/examples/widgets.rs | 13 ++++------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index 935ac94d0..e875342d9 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -309,24 +309,21 @@ fn custom_elements_work() { let size = *node.get::().unwrap(); let id = node.id(); println!("{indent}{id:?} {color:?} {size:?} {node_type:?}"); - match node_type { - NodeType::Element(el) => { - 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 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); } - if el.tag != "Root" { - assert_eq!(size.size, (i + 2).saturating_sub(height)); + // 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)); } - _ => {} } }); } diff --git a/packages/rink/examples/widgets.rs b/packages/rink/examples/widgets.rs index 1226d5c3d..b39a3b4c2 100644 --- a/packages/rink/examples/widgets.rs +++ b/packages/rink/examples/widgets.rs @@ -76,16 +76,13 @@ impl Driver for Counter { event: Rc, _: bool, ) { - match 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::() { - self.count = value; - } + 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::() { + self.count = value; } } - _ => {} } } From 60b25e9c2f32fb8524dfbf08c736aaba836aaf06 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 30 Jun 2023 08:54:08 -0700 Subject: [PATCH 18/35] add on unmount hook --- packages/hooks/src/lib.rs | 3 ++ packages/hooks/src/use_on_unmount.rs | 78 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/hooks/src/use_on_unmount.rs diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 1e718eda3..ff94fec1e 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -21,6 +21,9 @@ macro_rules! to_owned { )*} } +mod use_on_unmount; +pub use use_on_unmount; + mod usecontext; pub use usecontext::*; diff --git a/packages/hooks/src/use_on_unmount.rs b/packages/hooks/src/use_on_unmount.rs new file mode 100644 index 000000000..528491746 --- /dev/null +++ b/packages/hooks/src/use_on_unmount.rs @@ -0,0 +1,78 @@ +/// Creats a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect) +/// +/// Example: +/// ```rust +/// use dioxus::prelude::*; + +/// fn main() { +/// dioxus_web::launch(app) +/// } + +/// fn app(cx: Scope) -> Element { +/// let state = use_state(cx, || true); +/// render! { +/// for _ in 0..100 { +/// h1 { +/// "spacer" +/// } +/// } +/// if **state { +/// render! { +/// child_component {} +/// } +/// } +/// button { +/// onclick: move |_| { +/// state.set(!*state.get()); +/// }, +/// "Unmount element" +/// } +/// } +/// } + +/// fn child_component(cx: Scope) -> Element { +/// let original_scroll_position = use_state(cx, || 0.0); +/// use_effect(cx, (), move |_| { +/// to_owned![original_scroll_position]; +/// async move { +/// let window = web_sys::window().unwrap(); +/// let document = window.document().unwrap(); +/// let element = document.get_element_by_id("my_element").unwrap(); +/// element.scroll_into_view(); +/// original_scroll_position.set(window.scroll_y().unwrap()); +/// } +/// }); + +/// use_on_unmount(cx, { +/// to_owned![original_scroll_position]; +/// /// restore scroll to the top of the page +/// move || { +/// let window = web_sys::window().unwrap(); +/// window.scroll_with_x_and_y(*original_scroll_position.current(), 0.0); +/// } +/// }); + +/// render!{ +/// div { +/// id: "my_element", +/// "hello" +/// } +/// } +/// } +/// ``` +pub fn use_on_unmount(cx: &ScopeState, destroy: D) { + cx.use_hook(|| LifeCycle { + ondestroy: Some(destroy), + }); +} + +struct LifeCycle { + ondestroy: Option, +} + +impl Drop for LifeCycle { + fn drop(&mut self) { + let f = self.ondestroy.take().unwrap(); + f(); + } +} From 81d32800f890d6e0eaae449f6066607114a6dd89 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 30 Jun 2023 12:46:47 -0700 Subject: [PATCH 19/35] fix widget events with form event updates --- packages/rink/src/hooks.rs | 2 +- packages/rink/src/lib.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rink/src/hooks.rs b/packages/rink/src/hooks.rs index 55a9274aa..8c46d1d44 100644 --- a/packages/rink/src/hooks.rs +++ b/packages/rink/src/hooks.rs @@ -60,7 +60,7 @@ impl EventData { pub struct FormData { pub value: String, - pub values: HashMap, + pub values: HashMap>, pub files: Option, } diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index d4e64cc49..22467337a 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -20,7 +20,6 @@ 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}; From cf1b801e7dfb2da4c9a5d3ce212c56455bef5172 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 30 Jun 2023 12:49:21 -0700 Subject: [PATCH 20/35] fix shadowing issue --- packages/rink/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 22467337a..91b8112e6 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -22,7 +22,7 @@ use std::{rc::Rc, sync::RwLock}; use style_attributes::StyleModifier; 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; @@ -176,7 +176,7 @@ pub fn render( 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 @@ -218,7 +218,7 @@ pub fn render( } else { let rdom = rdom.read().unwrap(); resize( - Rect { + tui::layout::Rect { x: 0, y: 0, width: 1000, From 5c7b1a742fb7300efef9388eb408838ac9df35ca Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 30 Jun 2023 12:49:37 -0700 Subject: [PATCH 21/35] remove debug regression --- packages/dioxus-tui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dioxus-tui/src/lib.rs b/packages/dioxus-tui/src/lib.rs index 50a08945f..8804336be 100644 --- a/packages/dioxus-tui/src/lib.rs +++ b/packages/dioxus-tui/src/lib.rs @@ -45,7 +45,7 @@ pub fn launch_cfg_with_props(app: Component, 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); From a6e87cf3400a79f3f1d41a6a3ee45d599a3adc77 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 30 Jun 2023 12:56:22 -0700 Subject: [PATCH 22/35] fix tests --- packages/hooks/Cargo.toml | 1 + packages/hooks/src/lib.rs | 2 +- packages/hooks/src/use_on_unmount.rs | 6 +----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 19d5e53bd..87adebbe4 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -21,3 +21,4 @@ log = { workspace = true } futures-util = { workspace = true, default-features = false } dioxus-core = { workspace = true } dioxus = { workspace = true } +web-sys = { version = "0.3.64", features = ["Document", "Window", "Element"] } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index ff94fec1e..de9409650 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -22,7 +22,7 @@ macro_rules! to_owned { } mod use_on_unmount; -pub use use_on_unmount; +pub use use_on_unmount::*; mod usecontext; pub use usecontext::*; diff --git a/packages/hooks/src/use_on_unmount.rs b/packages/hooks/src/use_on_unmount.rs index 528491746..1fa4449c1 100644 --- a/packages/hooks/src/use_on_unmount.rs +++ b/packages/hooks/src/use_on_unmount.rs @@ -4,10 +4,6 @@ /// ```rust /// use dioxus::prelude::*; -/// fn main() { -/// dioxus_web::launch(app) -/// } - /// fn app(cx: Scope) -> Element { /// let state = use_state(cx, || true); /// render! { @@ -60,7 +56,7 @@ /// } /// } /// ``` -pub fn use_on_unmount(cx: &ScopeState, destroy: D) { +pub fn use_on_unmount(cx: &dioxus_core::ScopeState, destroy: D) { cx.use_hook(|| LifeCycle { ondestroy: Some(destroy), }); From db93252faf2530d76cf62658d9251987cfaa1b61 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Tue, 11 Jul 2023 00:25:16 -0400 Subject: [PATCH 23/35] feat: cli use local certificate --- packages/cli/Cargo.toml | 1 + packages/cli/src/config.rs | 15 ++++++ packages/cli/src/server/mod.rs | 92 ++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 7e5e60eed..c947baa63 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -39,6 +39,7 @@ indicatif = "0.17.0-rc.11" subprocess = "0.2.9" axum = { version = "0.5.1", features = ["ws", "headers"] } +axum-server = { version = "0.5.1", features = ["tls-rustls"] } tower-http = { version = "0.2.2", features = ["full"] } headers = "0.3.7" diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 6324f725f..4d3da9b65 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -77,6 +77,12 @@ impl Default for DioxusConfig { style: Some(vec![]), script: Some(vec![]), }, + https: WebHttpsConfig { + enabled: None, + mkcert: None, + key_path: None, + cert_path: None, + }, }, plugin: toml::Value::Table(toml::map::Map::new()), } @@ -101,6 +107,7 @@ pub struct WebConfig { pub proxy: Option>, pub watcher: WebWatcherConfig, pub resource: WebResourceConfig, + pub https: WebHttpsConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -134,6 +141,14 @@ pub struct WebDevResourceConfig { pub script: Option>, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebHttpsConfig { + pub enabled: Option, + pub mkcert: Option, + pub key_path: Option, + pub cert_path: Option, +} + #[derive(Debug, Clone)] pub struct CrateConfig { pub out_dir: PathBuf, diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 6575875f7..28d38ed04 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -10,6 +10,7 @@ use axum::{ routing::{get, get_service}, Router, }; +use axum_server::tls_rustls::RustlsConfig; use cargo_metadata::diagnostic::Diagnostic; use colored::Colorize; use dioxus_core::Template; @@ -84,6 +85,34 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res Ok(()) } +/// Returns an enum of rustls config and a bool if mkcert isn't installed +async fn get_rustls(config: &CrateConfig) -> (Option, bool) { + let config = &config.dioxus_config.web.https; + if config.enabled != Some(true) { + return (None, false); + } + + match config.mkcert { + // mkcert, use it + Some(true) => { + todo!() + } + // not mkcert + Some(false) => { + // get paths to cert & key + if let (Some(key), Some(cert)) = (config.key_path.clone(), config.cert_path.clone()) { + let rustls = RustlsConfig::from_pem_file(cert, key).await.unwrap(); + return (Some(rustls), false); + } else { + // missing cert or key + return (None, false); + } + } + // other + _ => return (None, false), + } +} + pub struct HotReloadState { pub messages: broadcast::Sender>, pub build_manager: Arc, @@ -274,6 +303,9 @@ pub async fn startup_hot_reload( } } + // HTTPS + let rustls_config = get_rustls(&config).await; + // start serve dev-server at 0.0.0.0:8080 print_console_info( &ip, @@ -368,13 +400,19 @@ pub async fn startup_hot_reload( let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - let server = axum::Server::bind(&addr).serve(router.into_make_service()); - if start_browser { - let _ = open::that(format!("http://{}", addr)); + if config.dioxus_config.web.https.enabled == Some(true) { + let _ = open::that(format!("https://{}", addr)); + } else { + let _ = open::that(format!("http://{}", addr)); + } } - server.await?; + if let (Some(rustls), _) = rustls_config { + axum_server::bind_rustls(addr, rustls).serve(router.into_make_service()).await?; + } else { + axum::Server::bind(&addr).serve(router.into_make_service()).await?; + } Ok(()) } @@ -457,6 +495,9 @@ pub async fn startup_default( .unwrap(); } + // HTTPS + let rustls_config = get_rustls(&config).await; + // start serve dev-server at 0.0.0.0 print_console_info( &ip, @@ -550,13 +591,20 @@ pub async fn startup_default( .layer(Extension(ws_reload_state)); let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - let server = axum::Server::bind(&addr).serve(router.into_make_service()); if start_browser { - let _ = open::that(format!("http://{}", addr)); + if config.dioxus_config.web.https.enabled == Some(true) { + let _ = open::that(format!("https://{}", addr)); + } else { + let _ = open::that(format!("http://{}", addr)); + } } - server.await?; + if let (Some(rustls), _) = rustls_config { + axum_server::bind_rustls(addr, rustls).serve(router.into_make_service()).await?; + } else { + axum::Server::bind(&addr).serve(router.into_make_service()).await?; + } Ok(()) } @@ -634,14 +682,28 @@ fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: Pre .bold() ); } - println!( - "\t> Local : {}", - format!("http://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("http://{}:{}/", ip, port).blue() - ); + + if config.dioxus_config.web.https.enabled == Some(true) { + println!( + "\t> Local : {}", + format!("https://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("https://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", format!("Enabled").green()); + } else { + println!( + "\t> Local : {}", + format!("http://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("http://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", format!("Disabled").red()); + } println!(); println!("\t> Profile : {}", profile.green()); println!("\t> Hot Reload : {}", hot_reload.cyan()); From ffb056798ac0d3df650580005135b064e5f08b7b Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Tue, 11 Jul 2023 15:51:56 -0400 Subject: [PATCH 24/35] revision: organize --- packages/cli/src/server/hot_reload.rs | 70 ++ packages/cli/src/server/mod.rs | 915 ++++++++++---------------- packages/cli/src/server/output.rs | 127 ++++ 3 files changed, 540 insertions(+), 572 deletions(-) create mode 100644 packages/cli/src/server/hot_reload.rs create mode 100644 packages/cli/src/server/output.rs diff --git a/packages/cli/src/server/hot_reload.rs b/packages/cli/src/server/hot_reload.rs new file mode 100644 index 000000000..167e712af --- /dev/null +++ b/packages/cli/src/server/hot_reload.rs @@ -0,0 +1,70 @@ +use std::sync::{Arc, Mutex}; + +use axum::{ + extract::{ws::Message, WebSocketUpgrade}, + response::IntoResponse, + Extension, TypedHeader, +}; +use dioxus_core::Template; +use dioxus_html::HtmlCtx; +use dioxus_rsx::hot_reload::FileMap; +use tokio::sync::broadcast; + +use super::BuildManager; +use crate::CrateConfig; + +pub struct HotReloadState { + pub messages: broadcast::Sender>, + pub build_manager: Arc, + pub file_map: Arc>>, + pub watcher_config: CrateConfig, +} + +pub async fn hot_reload_handler( + ws: WebSocketUpgrade, + _: Option>, + Extension(state): Extension>, +) -> impl IntoResponse { + ws.on_upgrade(|mut socket| async move { + log::info!("🔥 Hot Reload WebSocket connected"); + { + // update any rsx calls that changed before the websocket connected. + { + log::info!("🔮 Finding updates since last compile..."); + let templates: Vec<_> = { + state + .file_map + .lock() + .unwrap() + .map + .values() + .filter_map(|(_, template_slot)| *template_slot) + .collect() + }; + for template in templates { + if socket + .send(Message::Text(serde_json::to_string(&template).unwrap())) + .await + .is_err() + { + return; + } + } + } + log::info!("finished"); + } + + let mut rx = state.messages.subscribe(); + loop { + if let Ok(rsx) = rx.recv().await { + if socket + .send(Message::Text(serde_json::to_string(&rsx).unwrap())) + .await + .is_err() + { + break; + }; + } + } + }) +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 28d38ed04..6e50510ea 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -12,7 +12,6 @@ use axum::{ }; use axum_server::tls_rustls::RustlsConfig; use cargo_metadata::diagnostic::Diagnostic; -use colored::Colorize; use dioxus_core::Template; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; @@ -20,10 +19,9 @@ use notify::{RecommendedWatcher, Watcher}; use std::{ net::UdpSocket, path::PathBuf, - process::Command, sync::{Arc, Mutex}, }; -use tokio::sync::broadcast; +use tokio::sync::broadcast::{self, Sender}; use tower::ServiceBuilder; use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; use tower_http::{ @@ -36,6 +34,12 @@ use plugin::PluginManager; mod proxy; +mod hot_reload; +use hot_reload::*; + +mod output; +use output::*; + pub struct BuildManager { config: CrateConfig, reload_tx: broadcast::Sender<()>, @@ -77,11 +81,128 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res let ip = get_ip().unwrap_or(String::from("0.0.0.0")); - if config.hot_reload { - startup_hot_reload(ip, port, config, start_browser).await? - } else { - startup_default(ip, port, config, start_browser).await? + match config.hot_reload { + true => serve_hot_reload(ip, port, config, start_browser).await?, + false => serve_default(ip, port, config, start_browser).await?, } + + Ok(()) +} + +/// Start the server without hot reload +pub async fn serve_default( + ip: String, + port: u16, + config: CrateConfig, + start_browser: bool, +) -> Result<()> { + let first_build_result = crate::builder::build(&config, false)?; + + log::info!("🚀 Starting development server..."); + + // WS Reload Watching + let (reload_tx, _) = broadcast::channel(100); + setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?; + let ws_reload_state = Arc::new(WsReloadState { + update: reload_tx.clone(), + }); + + // start serve dev-server at 0.0.0.0 + print_console_info( + &ip, + port, + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + ); + + // HTTPS + let rustls_config = get_rustls(&config).await; + + // Router + let router = setup_router(config, ws_reload_state, None).await?; + + // Start server + start_server(port, router, start_browser, rustls_config.0).await?; + + Ok(()) +} + +/// Start dx serve with hot reload +pub async fn serve_hot_reload( + ip: String, + port: u16, + config: CrateConfig, + start_browser: bool, +) -> Result<()> { + let first_build_result = crate::builder::build(&config, false)?; + + log::info!("🚀 Starting development server..."); + + // Setup hot reload + let (reload_tx, _) = broadcast::channel(100); + let FileMapBuildResult { map, errors } = + FileMap::::create(config.crate_dir.clone()).unwrap(); + + for err in errors { + log::error!("{}", err); + } + + let file_map = Arc::new(Mutex::new(map)); + let build_manager = Arc::new(BuildManager { + config: config.clone(), + reload_tx: reload_tx.clone(), + }); + + let hot_reload_tx = broadcast::channel(100).0; + + // States + let hot_reload_state = Arc::new(HotReloadState { + messages: hot_reload_tx.clone(), + build_manager: build_manager.clone(), + file_map: file_map.clone(), + watcher_config: config.clone(), + }); + + let ws_reload_state = Arc::new(WsReloadState { + update: reload_tx.clone(), + }); + + // Setup file watcher + setup_file_watcher_hot_reload( + &config, + port, + ip.clone(), + hot_reload_tx, + file_map, + build_manager, + ) + .await?; + + // Print serve info + print_console_info( + &ip, + port, + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + ); + + // HTTPS + let rustls_config = get_rustls(&config).await; + + // Router + let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?; + + // Start server + start_server(port, router, start_browser, rustls_config.0).await?; + Ok(()) } @@ -102,111 +223,169 @@ async fn get_rustls(config: &CrateConfig) -> (Option, bool) { // get paths to cert & key if let (Some(key), Some(cert)) = (config.key_path.clone(), config.cert_path.clone()) { let rustls = RustlsConfig::from_pem_file(cert, key).await.unwrap(); - return (Some(rustls), false); + (Some(rustls), false) } else { // missing cert or key - return (None, false); + (None, false) } } // other - _ => return (None, false), + _ => (None, false), } } -pub struct HotReloadState { - pub messages: broadcast::Sender>, - pub build_manager: Arc, - pub file_map: Arc>>, - pub watcher_config: CrateConfig, -} - -pub async fn hot_reload_handler( - ws: WebSocketUpgrade, - _: Option>, - Extension(state): Extension>, -) -> impl IntoResponse { - ws.on_upgrade(|mut socket| async move { - log::info!("🔥 Hot Reload WebSocket connected"); - { - // update any rsx calls that changed before the websocket connected. - { - log::info!("🔮 Finding updates since last compile..."); - let templates: Vec<_> = { - state - .file_map - .lock() - .unwrap() - .map - .values() - .filter_map(|(_, template_slot)| *template_slot) - .collect() - }; - for template in templates { - if socket - .send(Message::Text(serde_json::to_string(&template).unwrap())) - .await - .is_err() - { - return; - } - } - } - log::info!("finished"); - } - - let mut rx = state.messages.subscribe(); - loop { - if let Ok(rsx) = rx.recv().await { - if socket - .send(Message::Text(serde_json::to_string(&rsx).unwrap())) - .await - .is_err() - { - break; - }; - } - } - }) -} - -#[allow(unused_assignments)] -pub async fn startup_hot_reload( - ip: String, - port: u16, +/// Sets up and returns a router +async fn setup_router( config: CrateConfig, + ws_reload: Arc, + hot_reload: Option>, +) -> Result { + // Setup cors + let cors = CorsLayer::new() + // allow `GET` and `POST` when accessing the resource + .allow_methods([Method::GET, Method::POST]) + // allow requests from any origin + .allow_origin(Any) + .allow_headers(Any); + + let (coep, coop) = if config.cross_origin_policy { + ( + HeaderValue::from_static("require-corp"), + HeaderValue::from_static("same-origin"), + ) + } else { + ( + HeaderValue::from_static("unsafe-none"), + HeaderValue::from_static("unsafe-none"), + ) + }; + + // Create file service + let file_service_config = config.clone(); + let file_service = ServiceBuilder::new() + .override_response_header( + HeaderName::from_static("cross-origin-embedder-policy"), + coep, + ) + .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) + .and_then( + move |response: Response| async move { + let response = if file_service_config + .dioxus_config + .web + .watcher + .index_on_404 + .unwrap_or(false) + && response.status() == StatusCode::NOT_FOUND + { + let body = Full::from( + // TODO: Cache/memoize this. + std::fs::read_to_string( + file_service_config + .crate_dir + .join(file_service_config.out_dir) + .join("index.html"), + ) + .ok() + .unwrap(), + ) + .map_err(|err| match err {}) + .boxed(); + Response::builder() + .status(StatusCode::OK) + .body(body) + .unwrap() + } else { + response.map(|body| body.boxed()) + }; + Ok(response) + }, + ) + .service(ServeDir::new(config.crate_dir.join(&config.out_dir))); + + // Setup websocket + let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); + + // Setup proxy + for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { + router = proxy::add_proxy(router, &proxy_config)?; + } + + // Route file service + router = router.fallback(get_service(file_service).handle_error( + |error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }, + )); + + // Setup routes + router = router + .route("/_dioxus/hot_reload", get(hot_reload_handler)) + .layer(cors) + .layer(Extension(ws_reload)); + + if let Some(hot_reload) = hot_reload { + router = router.layer(Extension(hot_reload)) + } + + Ok(router) +} + +/// Starts dx serve with no hot reload +async fn start_server( + port: u16, + router: Router, start_browser: bool, + rustls: Option, ) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; - - log::info!("🚀 Starting development server..."); - + // If plugins, call on_serve_start event #[cfg(feature = "plugin")] PluginManager::on_serve_start(&config)?; - let dist_path = config.out_dir.clone(); - let (reload_tx, _) = broadcast::channel(100); - let FileMapBuildResult { map, errors } = - FileMap::::create(config.crate_dir.clone()).unwrap(); - for err in errors { - log::error!("{}", err); - } - let file_map = Arc::new(Mutex::new(map)); - let build_manager = Arc::new(BuildManager { - config: config.clone(), - reload_tx: reload_tx.clone(), - }); - let hot_reload_tx = broadcast::channel(100).0; - let hot_reload_state = Arc::new(HotReloadState { - messages: hot_reload_tx.clone(), - build_manager: build_manager.clone(), - file_map: file_map.clone(), - watcher_config: config.clone(), - }); + // Parse address + let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - let crate_dir = config.crate_dir.clone(); - let ws_reload_state = Arc::new(WsReloadState { - update: reload_tx.clone(), - }); + // Open the browser + if start_browser { + match rustls { + Some(_) => _ = open::that(format!("https://{}", addr)), + None => _ = open::that(format!("http://{}", addr)), + } + } + + // Start the server with or without rustls + match rustls { + Some(rustls) => { + axum_server::bind_rustls(addr, rustls) + .serve(router.into_make_service()) + .await? + } + None => { + axum::Server::bind(&addr) + .serve(router.into_make_service()) + .await? + } + } + + Ok(()) +} + +/// Sets up a file watcher +async fn setup_file_watcher( + config: &CrateConfig, + port: u16, + watcher_ip: String, + reload_tx: Sender<()>, +) -> Result<()> { + let build_manager = BuildManager { + config: config.clone(), + reload_tx, + }; + + let mut last_update_time = chrono::Local::now().timestamp(); // file watcher: check file change let allow_watch_path = config @@ -218,7 +397,70 @@ pub async fn startup_hot_reload( .unwrap_or_else(|| vec![PathBuf::from("src")]); let watcher_config = config.clone(); - let watcher_ip = ip.clone(); + let mut watcher = notify::recommended_watcher(move |info: notify::Result| { + let config = watcher_config.clone(); + if let Ok(e) = info { + if chrono::Local::now().timestamp() > last_update_time { + match build_manager.rebuild() { + Ok(res) => { + last_update_time = chrono::Local::now().timestamp(); + + #[allow(clippy::redundant_clone)] + print_console_info( + &watcher_ip, + port, + &config, + PrettierOptions { + changed: e.paths.clone(), + warnings: res.warnings, + elapsed_time: res.elapsed_time, + }, + ); + + #[cfg(feature = "plugin")] + let _ = PluginManager::on_serve_rebuild( + chrono::Local::now().timestamp(), + e.paths, + ); + } + Err(e) => log::error!("{}", e), + } + } + } + }) + .unwrap(); + + for sub_path in allow_watch_path { + watcher + .watch( + &config.crate_dir.join(sub_path), + notify::RecursiveMode::Recursive, + ) + .unwrap(); + } + Ok(()) +} + +// Todo: reduce duplication and merge with setup_file_watcher() +/// Sets up a file watcher with hot reload +async fn setup_file_watcher_hot_reload( + config: &CrateConfig, + port: u16, + watcher_ip: String, + hot_reload_tx: Sender>, + file_map: Arc>>, + build_manager: Arc, +) -> Result<()> { + // file watcher: check file change + let allow_watch_path = config + .dioxus_config + .web + .watcher + .watch_path + .clone() + .unwrap_or_else(|| vec![PathBuf::from("src")]); + + let watcher_config = config.clone(); let mut last_update_time = chrono::Local::now().timestamp(); let mut watcher = RecommendedWatcher::new( @@ -254,7 +496,7 @@ pub async fn startup_hot_reload( // find changes to the rsx in the file let mut map = file_map.lock().unwrap(); - match map.update_rsx(&path, &crate_dir) { + match map.update_rsx(&path, &config.crate_dir) { Ok(UpdateResult::UpdatedRsx(msgs)) => { messages.extend(msgs); } @@ -303,482 +545,10 @@ pub async fn startup_hot_reload( } } - // HTTPS - let rustls_config = get_rustls(&config).await; - - // start serve dev-server at 0.0.0.0:8080 - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - let cors = CorsLayer::new() - // allow `GET` and `POST` when accessing the resource - .allow_methods([Method::GET, Method::POST]) - // allow requests from any origin - .allow_origin(Any) - .allow_headers(Any); - - let (coep, coop) = if config.cross_origin_policy { - ( - HeaderValue::from_static("require-corp"), - HeaderValue::from_static("same-origin"), - ) - } else { - ( - HeaderValue::from_static("unsafe-none"), - HeaderValue::from_static("unsafe-none"), - ) - }; - - let file_service_config = config.clone(); - let file_service = ServiceBuilder::new() - .override_response_header( - HeaderName::from_static("cross-origin-embedder-policy"), - coep, - ) - .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) - .and_then( - move |response: Response| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - && response.status() == StatusCode::NOT_FOUND - { - let body = Full::from( - // TODO: Cache/memoize this. - std::fs::read_to_string( - file_service_config - .crate_dir - .join(file_service_config.out_dir) - .join("index.html"), - ) - .ok() - .unwrap(), - ) - .map_err(|err| match err {}) - .boxed(); - Response::builder() - .status(StatusCode::OK) - .body(body) - .unwrap() - } else { - response.map(|body| body.boxed()) - }; - Ok(response) - }, - ) - .service(ServeDir::new(config.crate_dir.join(&dist_path))); - - let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { - router = proxy::add_proxy(router, &proxy_config)?; - } - router = router.fallback(get_service(file_service).handle_error( - |error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }, - )); - - let router = router - .route("/_dioxus/hot_reload", get(hot_reload_handler)) - .layer(cors) - .layer(Extension(ws_reload_state)) - .layer(Extension(hot_reload_state)); - - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - - if start_browser { - if config.dioxus_config.web.https.enabled == Some(true) { - let _ = open::that(format!("https://{}", addr)); - } else { - let _ = open::that(format!("http://{}", addr)); - } - } - - if let (Some(rustls), _) = rustls_config { - axum_server::bind_rustls(addr, rustls).serve(router.into_make_service()).await?; - } else { - axum::Server::bind(&addr).serve(router.into_make_service()).await?; - } - Ok(()) } -pub async fn startup_default( - ip: String, - port: u16, - config: CrateConfig, - start_browser: bool, -) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; - - log::info!("🚀 Starting development server..."); - - let dist_path = config.out_dir.clone(); - - let (reload_tx, _) = broadcast::channel(100); - - let build_manager = BuildManager { - config: config.clone(), - reload_tx: reload_tx.clone(), - }; - - let ws_reload_state = Arc::new(WsReloadState { - update: reload_tx.clone(), - }); - - let mut last_update_time = chrono::Local::now().timestamp(); - - // file watcher: check file change - let allow_watch_path = config - .dioxus_config - .web - .watcher - .watch_path - .clone() - .unwrap_or_else(|| vec![PathBuf::from("src")]); - - let watcher_config = config.clone(); - let watcher_ip = ip.clone(); - let mut watcher = notify::recommended_watcher(move |info: notify::Result| { - let config = watcher_config.clone(); - if let Ok(e) = info { - if chrono::Local::now().timestamp() > last_update_time { - match build_manager.rebuild() { - Ok(res) => { - last_update_time = chrono::Local::now().timestamp(); - - #[allow(clippy::redundant_clone)] - print_console_info( - &watcher_ip, - port, - &config, - PrettierOptions { - changed: e.paths.clone(), - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - ); - - #[cfg(feature = "plugin")] - let _ = PluginManager::on_serve_rebuild( - chrono::Local::now().timestamp(), - e.paths, - ); - } - Err(e) => log::error!("{}", e), - } - } - } - }) - .unwrap(); - - for sub_path in allow_watch_path { - watcher - .watch( - &config.crate_dir.join(sub_path), - notify::RecursiveMode::Recursive, - ) - .unwrap(); - } - - // HTTPS - let rustls_config = get_rustls(&config).await; - - // start serve dev-server at 0.0.0.0 - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - #[cfg(feature = "plugin")] - PluginManager::on_serve_start(&config)?; - - let cors = CorsLayer::new() - // allow `GET` and `POST` when accessing the resource - .allow_methods([Method::GET, Method::POST]) - // allow requests from any origin - .allow_origin(Any) - .allow_headers(Any); - - let (coep, coop) = if config.cross_origin_policy { - ( - HeaderValue::from_static("require-corp"), - HeaderValue::from_static("same-origin"), - ) - } else { - ( - HeaderValue::from_static("unsafe-none"), - HeaderValue::from_static("unsafe-none"), - ) - }; - - let file_service_config = config.clone(); - let file_service = ServiceBuilder::new() - .override_response_header( - HeaderName::from_static("cross-origin-embedder-policy"), - coep, - ) - .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) - .and_then( - move |response: Response| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - && response.status() == StatusCode::NOT_FOUND - { - let body = Full::from( - // TODO: Cache/memoize this. - std::fs::read_to_string( - file_service_config - .crate_dir - .join(file_service_config.out_dir) - .join("index.html"), - ) - .ok() - .unwrap(), - ) - .map_err(|err| match err {}) - .boxed(); - Response::builder() - .status(StatusCode::OK) - .body(body) - .unwrap() - } else { - response.map(|body| body.boxed()) - }; - Ok(response) - }, - ) - .service(ServeDir::new(config.crate_dir.join(&dist_path))); - - let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { - router = proxy::add_proxy(router, &proxy_config)?; - } - router = router - .fallback( - get_service(file_service).handle_error(|error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }), - ) - .layer(cors) - .layer(Extension(ws_reload_state)); - - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - - if start_browser { - if config.dioxus_config.web.https.enabled == Some(true) { - let _ = open::that(format!("https://{}", addr)); - } else { - let _ = open::that(format!("http://{}", addr)); - } - } - - if let (Some(rustls), _) = rustls_config { - axum_server::bind_rustls(addr, rustls).serve(router.into_make_service()).await?; - } else { - axum::Server::bind(&addr).serve(router.into_make_service()).await?; - } - - Ok(()) -} - -#[derive(Debug, Default)] -pub struct PrettierOptions { - changed: Vec, - warnings: Vec, - elapsed_time: u128, -} - -fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { - if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { - "cls" - } else { - "clear" - }) - .output() - { - print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); - } else { - // Try ANSI-Escape characters - print!("\x1b[2J\x1b[H"); - } - - // for path in &changed { - // let path = path - // .strip_prefix(crate::crate_root().unwrap()) - // .unwrap() - // .to_path_buf(); - // log::info!("Updated {}", format!("{}", path.to_str().unwrap()).green()); - // } - - let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); - if config.custom_profile.is_some() { - profile = config.custom_profile.as_ref().unwrap().to_string(); - } - let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; - let crate_root = crate::cargo::crate_root().unwrap(); - let custom_html_file = if crate_root.join("index.html").is_file() { - "Custom [index.html]" - } else { - "Default" - }; - let url_rewrite = if config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - { - "True" - } else { - "False" - }; - - let proxies = config.dioxus_config.web.proxy.as_ref(); - - if options.changed.is_empty() { - println!( - "{} @ v{} [{}] \n", - "Dioxus".bold().green(), - crate::DIOXUS_CLI_VERSION, - chrono::Local::now().format("%H:%M:%S").to_string().dimmed() - ); - } else { - println!( - "Project Reloaded: {}\n", - format!( - "Changed {} files. [{}]", - options.changed.len(), - chrono::Local::now().format("%H:%M:%S").to_string().dimmed() - ) - .purple() - .bold() - ); - } - - if config.dioxus_config.web.https.enabled == Some(true) { - println!( - "\t> Local : {}", - format!("https://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("https://{}:{}/", ip, port).blue() - ); - println!("\t> HTTPS : {}", format!("Enabled").green()); - } else { - println!( - "\t> Local : {}", - format!("http://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("http://{}:{}/", ip, port).blue() - ); - println!("\t> HTTPS : {}", format!("Disabled").red()); - } - println!(); - println!("\t> Profile : {}", profile.green()); - println!("\t> Hot Reload : {}", hot_reload.cyan()); - if let Some(proxies) = proxies { - if !proxies.is_empty() { - println!("\t> Proxies :"); - for proxy in proxies { - println!("\t\t- {}", proxy.backend.blue()); - } - } - } - println!("\t> Index Template : {}", custom_html_file.green()); - println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple()); - println!(); - println!( - "\t> Build Time Use : {} millis", - options.elapsed_time.to_string().green().bold() - ); - println!(); - - if options.warnings.is_empty() { - log::info!("{}\n", "A perfect compilation!".green().bold()); - } else { - log::warn!( - "{}", - format!( - "There were {} warning messages during the build.", - options.warnings.len() - 1 - ) - .yellow() - .bold() - ); - // for info in &options.warnings { - // let message = info.message.clone(); - // if message == format!("{} warnings emitted", options.warnings.len() - 1) { - // continue; - // } - // let mut console = String::new(); - // for span in &info.spans { - // let file = &span.file_name; - // let line = (span.line_start, span.line_end); - // let line_str = if line.0 == line.1 { - // line.0.to_string() - // } else { - // format!("{}~{}", line.0, line.1) - // }; - // let code = span.text.clone(); - // let span_info = if code.len() == 1 { - // let code = code.get(0).unwrap().text.trim().blue().bold().to_string(); - // format!( - // "[{}: {}]: '{}' --> {}", - // file, - // line_str, - // code, - // message.yellow().bold() - // ) - // } else { - // let code = code - // .iter() - // .enumerate() - // .map(|(_i, s)| format!("\t{}\n", s.text).blue().bold().to_string()) - // .collect::(); - // format!("[{}: {}]:\n{}\n#:{}", file, line_str, code, message) - // }; - // console = format!("{console}\n\t{span_info}"); - // } - // println!("{console}"); - // } - // println!( - // "\n{}\n", - // "Resolving all warnings will help your code run better!".yellow() - // ); - } -} - +/// Get the network ip fn get_ip() -> Option { let socket = match UdpSocket::bind("0.0.0.0:0") { Ok(s) => s, @@ -796,6 +566,7 @@ fn get_ip() -> Option { } } +/// Handle websockets async fn ws_handler( ws: WebSocketUpgrade, _: Option>, diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs new file mode 100644 index 000000000..4c6e1f3b2 --- /dev/null +++ b/packages/cli/src/server/output.rs @@ -0,0 +1,127 @@ +use crate::server::Diagnostic; +use crate::CrateConfig; +use colored::Colorize; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Default)] +pub struct PrettierOptions { + pub changed: Vec, + pub warnings: Vec, + pub elapsed_time: u128, +} + +pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { + if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { + "cls" + } else { + "clear" + }) + .output() + { + print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); + } else { + // Try ANSI-Escape characters + print!("\x1b[2J\x1b[H"); + } + + let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); + if config.custom_profile.is_some() { + profile = config.custom_profile.as_ref().unwrap().to_string(); + } + let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; + let crate_root = crate::cargo::crate_root().unwrap(); + let custom_html_file = if crate_root.join("index.html").is_file() { + "Custom [index.html]" + } else { + "Default" + }; + let url_rewrite = if config + .dioxus_config + .web + .watcher + .index_on_404 + .unwrap_or(false) + { + "True" + } else { + "False" + }; + + let proxies = config.dioxus_config.web.proxy.as_ref(); + + if options.changed.is_empty() { + println!( + "{} @ v{} [{}] \n", + "Dioxus".bold().green(), + crate::DIOXUS_CLI_VERSION, + chrono::Local::now().format("%H:%M:%S").to_string().dimmed() + ); + } else { + println!( + "Project Reloaded: {}\n", + format!( + "Changed {} files. [{}]", + options.changed.len(), + chrono::Local::now().format("%H:%M:%S").to_string().dimmed() + ) + .purple() + .bold() + ); + } + + if config.dioxus_config.web.https.enabled == Some(true) { + println!( + "\t> Local : {}", + format!("https://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("https://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Enabled".to_string().green()); + } else { + println!( + "\t> Local : {}", + format!("http://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("http://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Disabled".to_string().red()); + } + println!(); + println!("\t> Profile : {}", profile.green()); + println!("\t> Hot Reload : {}", hot_reload.cyan()); + if let Some(proxies) = proxies { + if !proxies.is_empty() { + println!("\t> Proxies :"); + for proxy in proxies { + println!("\t\t- {}", proxy.backend.blue()); + } + } + } + println!("\t> Index Template : {}", custom_html_file.green()); + println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple()); + println!(); + println!( + "\t> Build Time Use : {} millis", + options.elapsed_time.to_string().green().bold() + ); + println!(); + + if options.warnings.is_empty() { + log::info!("{}\n", "A perfect compilation!".green().bold()); + } else { + log::warn!( + "{}", + format!( + "There were {} warning messages during the build.", + options.warnings.len() - 1 + ) + .yellow() + .bold() + ); + } +} From d83b00ff0f4a51e27e4fab33fd1c6d25a7caac0f Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Tue, 11 Jul 2023 16:38:34 -0400 Subject: [PATCH 25/35] feat: mkcert --- packages/cli/src/server/mod.rs | 77 ++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 6e50510ea..a732db1f3 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -19,6 +19,7 @@ use notify::{RecommendedWatcher, Watcher}; use std::{ net::UdpSocket, path::PathBuf, + process::Command, sync::{Arc, Mutex}, }; use tokio::sync::broadcast::{self, Sender}; @@ -107,7 +108,11 @@ pub async fn serve_default( update: reload_tx.clone(), }); - // start serve dev-server at 0.0.0.0 + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + + // Print serve info print_console_info( &ip, port, @@ -119,14 +124,11 @@ pub async fn serve_default( }, ); - // HTTPS - let rustls_config = get_rustls(&config).await; - // Router let router = setup_router(config, ws_reload_state, None).await?; // Start server - start_server(port, router, start_browser, rustls_config.0).await?; + start_server(port, router, start_browser, rustls_config).await?; Ok(()) } @@ -182,6 +184,10 @@ pub async fn serve_hot_reload( ) .await?; + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + // Print serve info print_console_info( &ip, @@ -194,44 +200,73 @@ pub async fn serve_hot_reload( }, ); - // HTTPS - let rustls_config = get_rustls(&config).await; - // Router let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?; // Start server - start_server(port, router, start_browser, rustls_config.0).await?; + start_server(port, router, start_browser, rustls_config).await?; Ok(()) } /// Returns an enum of rustls config and a bool if mkcert isn't installed -async fn get_rustls(config: &CrateConfig) -> (Option, bool) { - let config = &config.dioxus_config.web.https; - if config.enabled != Some(true) { - return (None, false); +async fn get_rustls(config: &CrateConfig) -> Result> { + let web_config = &config.dioxus_config.web.https; + if web_config.enabled != Some(true) { + return Ok(None); } - match config.mkcert { + let (cert_path, key_path) = match web_config.mkcert { // mkcert, use it Some(true) => { - todo!() + _ = fs::create_dir("ssl"); + let cmd = Command::new("mkcert") + .args([ + "-install", + "-key-file", + "ssl/key.pem", + "-cert-file", + "ssl/cert.pem", + "localhost", + "::1", + "127.0.0.1", + ]) + .spawn(); + + match cmd { + Err(e) => { + match e.kind() { + io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."), + e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()), + }; + return Err("failed to generate mkcert certificates".into()); + } + Ok(mut cmd) => { + cmd.wait()?; + } + } + + (PathBuf::from("ssl/cert.pem"), PathBuf::from("ssl/key.pem")) } // not mkcert Some(false) => { // get paths to cert & key - if let (Some(key), Some(cert)) = (config.key_path.clone(), config.cert_path.clone()) { - let rustls = RustlsConfig::from_pem_file(cert, key).await.unwrap(); - (Some(rustls), false) + if let (Some(key), Some(cert)) = + (web_config.key_path.clone(), web_config.cert_path.clone()) + { + (cert, key) } else { // missing cert or key - (None, false) + return Err("https is enabled but cert or key path is missing".into()); } } // other - _ => (None, false), - } + _ => return Ok(None), + }; + + Ok(Some( + RustlsConfig::from_pem_file(cert_path, key_path).await?, + )) } /// Sets up and returns a router From b47e4fedccc3cd402bcf276390d17bca3c245c72 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Tue, 11 Jul 2023 19:49:41 -0400 Subject: [PATCH 26/35] feat: default web.https config --- packages/cli/src/config.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 4d3da9b65..19cc3c7b5 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -107,6 +107,7 @@ pub struct WebConfig { pub proxy: Option>, pub watcher: WebWatcherConfig, pub resource: WebResourceConfig, + #[serde(default)] pub https: WebHttpsConfig, } @@ -141,7 +142,7 @@ pub struct WebDevResourceConfig { pub script: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct WebHttpsConfig { pub enabled: Option, pub mkcert: Option, From 5bc9e4556b7e02b67c1347c39739d3f2c5af4ded Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Tue, 11 Jul 2023 23:01:37 -0400 Subject: [PATCH 27/35] feat: custom mkcert paths --- packages/cli/src/config.rs | 4 ++-- packages/cli/src/server/mod.rs | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 19cc3c7b5..f9117f6e5 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -146,8 +146,8 @@ pub struct WebDevResourceConfig { pub struct WebHttpsConfig { pub enabled: Option, pub mkcert: Option, - pub key_path: Option, - pub cert_path: Option, + pub key_path: Option, + pub cert_path: Option, } #[derive(Debug, Clone)] diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index a732db1f3..c5b6fb57a 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -209,6 +209,9 @@ pub async fn serve_hot_reload( Ok(()) } +const DEFAULT_KEY_PATH: &str = "ssl/key.pem"; +const DEFAULT_CERT_PATH: &str = "ssl/cert.pem"; + /// Returns an enum of rustls config and a bool if mkcert isn't installed async fn get_rustls(config: &CrateConfig) -> Result> { let web_config = &config.dioxus_config.web.https; @@ -219,14 +222,29 @@ async fn get_rustls(config: &CrateConfig) -> Result> { let (cert_path, key_path) = match web_config.mkcert { // mkcert, use it Some(true) => { - _ = fs::create_dir("ssl"); + // Get paths to store certs, otherwise use ssl/item.pem + let key_path = web_config + .key_path + .clone() + .unwrap_or(DEFAULT_KEY_PATH.to_string()); + + let cert_path = web_config + .cert_path + .clone() + .unwrap_or(DEFAULT_CERT_PATH.to_string()); + + // Create ssl directory if using defaults + if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { + fs::create_dir("ssl")?; + } + let cmd = Command::new("mkcert") .args([ "-install", "-key-file", - "ssl/key.pem", + &key_path, "-cert-file", - "ssl/cert.pem", + &cert_path, "localhost", "::1", "127.0.0.1", @@ -246,7 +264,7 @@ async fn get_rustls(config: &CrateConfig) -> Result> { } } - (PathBuf::from("ssl/cert.pem"), PathBuf::from("ssl/key.pem")) + (cert_path, key_path) } // not mkcert Some(false) => { From 120fccc53da92a3ba28d02737e3d2d8965fd394e Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Wed, 12 Jul 2023 02:09:13 -0400 Subject: [PATCH 28/35] fix: full & hot reload --- packages/cli/src/server/mod.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index c5b6fb57a..3bd434005 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -103,7 +103,11 @@ pub async fn serve_default( // WS Reload Watching let (reload_tx, _) = broadcast::channel(100); - setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?; + + // We got to own watcher so that it exists for the duration of serve + // Otherwise full reload won't work. + let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?; + let ws_reload_state = Arc::new(WsReloadState { update: reload_tx.clone(), }); @@ -174,7 +178,9 @@ pub async fn serve_hot_reload( }); // Setup file watcher - setup_file_watcher_hot_reload( + // We got to own watcher so that it exists for the duration of serve + // Otherwise hot reload won't work. + let _watcher = setup_file_watcher_hot_reload( &config, port, ip.clone(), @@ -235,7 +241,7 @@ async fn get_rustls(config: &CrateConfig) -> Result> { // Create ssl directory if using defaults if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { - fs::create_dir("ssl")?; + _ = fs::create_dir("ssl"); } let cmd = Command::new("mkcert") @@ -432,7 +438,7 @@ async fn setup_file_watcher( port: u16, watcher_ip: String, reload_tx: Sender<()>, -) -> Result<()> { +) -> Result { let build_manager = BuildManager { config: config.clone(), reload_tx, @@ -491,7 +497,7 @@ async fn setup_file_watcher( ) .unwrap(); } - Ok(()) + Ok(watcher) } // Todo: reduce duplication and merge with setup_file_watcher() @@ -503,7 +509,7 @@ async fn setup_file_watcher_hot_reload( hot_reload_tx: Sender>, file_map: Arc>>, build_manager: Arc, -) -> Result<()> { +) -> Result { // file watcher: check file change let allow_watch_path = config .dioxus_config @@ -598,7 +604,7 @@ async fn setup_file_watcher_hot_reload( } } - Ok(()) + Ok(watcher) } /// Get the network ip From 17f2de81d6444789b4a3fe868f62e822c5a92dce Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jul 2023 14:22:47 -0700 Subject: [PATCH 29/35] Remove indexmap from core --- packages/core/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 48391456d..0b668f43d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -27,8 +27,6 @@ slab = { workspace = true } futures-channel = { workspace = true } -indexmap = "1.7" - smallbox = "0.8.1" log = { workspace = true } From 2d8841cf70eaaf5990507c889d74e650c43ad585 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 12:52:49 -0700 Subject: [PATCH 30/35] Add lazy contexts to signals --- examples/signals.rs | 5 +---- packages/core/src/scopes.rs | 19 +++++++++++++++++++ packages/hooks/src/lib.rs | 3 +++ packages/hooks/src/userootcontext.rs | 9 +++++++++ packages/signals/src/lib.rs | 10 ++++++++-- 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 packages/hooks/src/userootcontext.rs diff --git a/examples/signals.rs b/examples/signals.rs index 988b9af41..725577206 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -1,5 +1,4 @@ use dioxus::prelude::*; -use dioxus_signals::{use_init_signal_rt, use_signal}; use std::time::Duration; fn main() { @@ -7,9 +6,7 @@ fn main() { } fn app(cx: Scope) -> Element { - use_init_signal_rt(cx); - - let mut count = use_signal(cx, || 0); + let mut count = dioxus_signals::use_signal(cx, || 0); use_future!(cx, || async move { loop { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 2b9f32f8d..ff94c456d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -418,6 +418,25 @@ impl<'src> ScopeState { value } + /// Provide a context to the root and then consume it + /// + /// This is intended for "global" state management solutions that would rather be implicit for the entire app. + /// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization. + /// + /// Note that you should be checking if the context existed before trying to provide a new one. Providing a context + /// when a context already exists will swap the context out for the new one, which may not be what you want. + pub fn provide_root_context(&self, context: T) -> T { + let mut parent = self; + + // Walk upwards until there is no more parent - and tada we have the root + while let Some(next_parent) = parent.parent { + parent = unsafe { &*next_parent }; + debug_assert_eq!(parent.scope_id(), ScopeId(0)); + } + + parent.provide_context(context) + } + /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(&self, fut: impl Future + 'static) -> TaskId { let id = self.tasks.spawn(self.id, fut); diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 58b6c82f5..a2fb23ed1 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -79,3 +79,6 @@ pub use usecallback::*; mod usememo; pub use usememo::*; + +mod userootcontext; +pub use userootcontext::*; diff --git a/packages/hooks/src/userootcontext.rs b/packages/hooks/src/userootcontext.rs new file mode 100644 index 000000000..9b2fab719 --- /dev/null +++ b/packages/hooks/src/userootcontext.rs @@ -0,0 +1,9 @@ +use dioxus_core::ScopeState; + +/// +pub fn use_root_context(cx: &ScopeState, new: impl FnOnce() -> T) -> &T { + cx.use_hook(|| { + cx.consume_context::() + .unwrap_or_else(|| cx.provide_root_context(new())) + }) +} diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index e4b499ed8..4e950e0dc 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -10,16 +10,22 @@ mod rt; use dioxus_core::ScopeState; pub use rt::*; +use crate::rt::claim_rt; + pub fn use_init_signal_rt(cx: &ScopeState) { cx.use_hook(|| { - let rt = crate::rt::claim_rt(cx.schedule_update_any()); + let rt = claim_rt(cx.schedule_update_any()); cx.provide_context(rt); }); } pub fn use_signal(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal { cx.use_hook(|| { - let rt: &'static SignalRt = cx.consume_context().unwrap(); + let rt: &'static SignalRt = match cx.consume_context() { + Some(rt) => rt, + None => cx.provide_context(claim_rt(cx.schedule_update_any())), + }; + let id = rt.init(f()); rt.subscribe(id, cx.scope_id()); From 3b3020edd2cf2dc3ca226973e858694485935f30 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 13:13:33 -0700 Subject: [PATCH 31/35] Make clippy ignore dumb lint --- packages/core-macro/tests/ifmt.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core-macro/tests/ifmt.rs b/packages/core-macro/tests/ifmt.rs index 3e03d37a9..195a8651c 100644 --- a/packages/core-macro/tests/ifmt.rs +++ b/packages/core-macro/tests/ifmt.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] + use std::borrow::Borrow; use dioxus_core_macro::*; From f62af4057fe3b01f164e35d2dd160ac2b12f2643 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 13:22:08 -0700 Subject: [PATCH 32/35] Fix clippy lint --- packages/core-macro/tests/ifmt.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core-macro/tests/ifmt.rs b/packages/core-macro/tests/ifmt.rs index 195a8651c..2883b8fd6 100644 --- a/packages/core-macro/tests/ifmt.rs +++ b/packages/core-macro/tests/ifmt.rs @@ -1,7 +1,3 @@ -#![allow(clippy::all)] - -use std::borrow::Borrow; - use dioxus_core_macro::*; #[test] @@ -22,8 +18,8 @@ fn formatting_compiles() { // function calls in formatings work assert_eq!( - format_args_f!("{x.borrow():?}").to_string(), - format!("{:?}", x.borrow()) + format_args_f!("{blah(&x):?}").to_string(), + format!("{:?}", blah(&x)) ); // allows duplicate format args @@ -32,3 +28,7 @@ fn formatting_compiles() { format!("{x:?} {x:?}") ); } + +fn blah(hi: &(i32, i32)) -> String { + format_args_f!("{hi.0} {hi.1}").to_string() +} From 8a9bf294b44e123927dc9a19ad2d4238aea6ccb9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 13:24:28 -0700 Subject: [PATCH 33/35] Use local CLI for playright --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index c43c1dc33..604562f6a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -33,7 +33,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: install - args: --git https://github.com/DioxusLabs/cli + args: --path packages/cli - name: Run Playwright tests run: npx playwright test - uses: actions/upload-artifact@v3 From 17ae835f9e03936033dce0bc9a545a3fbe817ed1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 14:14:51 -0700 Subject: [PATCH 34/35] Try pinning ubuntu version to fix CI --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 604562f6a..84891f403 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -8,7 +8,7 @@ jobs: test: if: github.event.pull_request.draft == false timeout-minutes: 60 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From f2cd6b8af65fc4c8f1e9f109884a2e8696986f15 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 14:54:21 -0700 Subject: [PATCH 35/35] Disable LTO on CLI --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d69a74828..b7a6e96e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,8 +115,3 @@ fern = { version = "0.6.0", features = ["colored"] } thiserror = "1.0.30" env_logger = "0.10.0" simple_logger = "4.0.0" - -[profile.release] -opt-level = 3 -lto = true -debug = true