From 625dc662d2112e1926cf60a058168601ab8dca5c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 12:43:51 -0500 Subject: [PATCH] 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(); + } + + _ => {} + } + } +}