Explorar el Código

Merge remote-tracking branch 'upstream/master' into fix-non-str-attributes

= hace 2 años
padre
commit
73ec4abfdf

+ 1 - 2
examples/rsx_usage.rs

@@ -1,5 +1,4 @@
 #![allow(non_snake_case)]
-
 //! A tour of the rsx! macro
 //! ------------------------
 //!
@@ -39,7 +38,7 @@
 //! - Accept a list of vnodes as children for a Fragment component
 //! - Allow keyed fragments in iterators
 //! - Allow top-level fragments
-//!
+
 fn main() {
     dioxus_desktop::launch(app);
 }

+ 1 - 1
packages/core-macro/Cargo.toml

@@ -18,7 +18,7 @@ proc-macro = true
 proc-macro2 = { version = "1.0" }
 quote = "1.0"
 syn = { version = "1.0", features = ["full", "extra-traits"] }
-dioxus-rsx = {  path = "../rsx" }
+dioxus-rsx = {  path = "../rsx", version = "0.0.1" }
 
 # testing
 [dev-dependencies]

+ 0 - 10
packages/core-macro/src/lib.rs

@@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
 }
 
 /// The rsx! macro makes it easy for developers to write jsx-style markup in their components.
-///
-/// ## Complete Reference Guide:
-/// ```ignore
-#[doc = include_str!("../../../examples/rsx_usage.rs")]
-/// ```
 #[proc_macro]
 pub fn rsx(s: TokenStream) -> TokenStream {
     match syn::parse::<rsx::CallBody>(s) {
@@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream {
 /// The render! macro makes it easy for developers to write jsx-style markup in their components.
 ///
 /// The render macro automatically renders rsx - making it unhygenic.
-///
-/// ## Complete Reference Guide:
-/// ```ignore
-#[doc = include_str!("../../../examples/rsx_usage.rs")]
-/// ```
 #[proc_macro]
 pub fn render(s: TokenStream) -> TokenStream {
     match syn::parse::<rsx::CallBody>(s) {

+ 1 - 1
packages/core/Cargo.toml

@@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 bumpalo = { version = "3.6", features = ["collections", "boxed"] }
 
 # faster hashmaps
-fxhash = "0.2"
+rustc-hash = "1.1.0"
 
 # Used in diffing
 longest-increasing-subsequence = "0.1.0"

+ 2 - 2
packages/core/src/diff.rs

@@ -11,7 +11,7 @@ use crate::{
     AttributeValue, TemplateNode,
 };
 
-use fxhash::{FxHashMap, FxHashSet};
+use rustc_hash::{FxHashMap, FxHashSet};
 use DynamicNode::*;
 
 impl<'b> VirtualDom {
@@ -428,7 +428,7 @@ impl<'b> VirtualDom {
     // The stack is empty upon entry.
     fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
         if cfg!(debug_assertions) {
-            let mut keys = fxhash::FxHashSet::default();
+            let mut keys = rustc_hash::FxHashSet::default();
             let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
                 keys.clear();
                 for child in children {

+ 0 - 3
packages/core/src/lazynodes.rs

@@ -55,9 +55,6 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
     /// ```
     #[must_use]
     pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> {
-        if self.inner.is_heap() {
-            panic!();
-        }
         (self.inner)(f)
     }
 }

+ 1 - 1
packages/core/src/mutations.rs

@@ -1,4 +1,4 @@
-use fxhash::FxHashSet;
+use rustc_hash::FxHashSet;
 
 use crate::{arena::ElementId, AttributeValue, ScopeId, Template};
 

+ 1 - 1
packages/core/src/scopes.rs

@@ -10,7 +10,7 @@ use crate::{
     Attribute, AttributeValue, Element, Event, Properties, TaskId,
 };
 use bumpalo::{boxed::Box as BumpBox, Bump};
-use fxhash::{FxHashMap, FxHashSet};
+use rustc_hash::{FxHashMap, FxHashSet};
 use std::{
     any::{Any, TypeId},
     cell::{Cell, RefCell},

+ 1 - 1
packages/core/src/virtual_dom.rs

@@ -14,7 +14,7 @@ use crate::{
     AttributeValue, Element, Event, Scope, SuspenseContext,
 };
 use futures_util::{pin_mut, StreamExt};
-use fxhash::FxHashMap;
+use rustc_hash::FxHashMap;
 use slab::Slab;
 use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
 

+ 4 - 4
packages/desktop/src/protocol.rs

@@ -104,10 +104,10 @@ fn get_asset_root() -> Option<PathBuf> {
     #[cfg(target_os = "macos")]
     {
         let bundle = core_foundation::bundle::CFBundle::main_bundle();
-        let bundle_path = dbg!(bundle.path()?);
-        let resources_path = dbg!(bundle.resources_path()?);
-        let absolute_resources_root = dbg!(bundle_path.join(resources_path));
-        let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?);
+        let bundle_path = bundle.path()?;
+        let resources_path = bundle.resources_path()?;
+        let absolute_resources_root = bundle_path.join(resources_path);
+        let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
 
         return Some(canonical_resources_root);
     }

+ 0 - 2
packages/dioxus/src/lib.rs

@@ -1,5 +1,3 @@
-#![doc = include_str!("../../../notes/README.md")]
-
 pub use dioxus_core as core;
 
 #[cfg(feature = "hooks")]

+ 1 - 1
packages/interpreter/Cargo.toml

@@ -17,7 +17,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 wasm-bindgen = { version = "0.2.79", optional = true }
 js-sys = { version = "0.3.56", optional = true }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
-sledgehammer_bindgen = { version = "0.1.2", optional = true }
+sledgehammer_bindgen = { version = "0.1.3", optional = true }
 sledgehammer_utils = { version = "0.1.0", optional = true }
 
 [features]

+ 3 - 2
packages/interpreter/src/interpreter.js

@@ -89,8 +89,9 @@ export class Interpreter {
   PopRoot() {
     this.stack.pop();
   }
-  AppendChildren(id, many) {
-    let root = this.nodes[id];
+  AppendChildren(many) {
+    // let root = this.nodes[id];
+    let root = this.stack[this.stack.length - 1 - many];
     let to_add = this.stack.splice(this.stack.length - many);
     for (let i = 0; i < many; i++) {
       root.appendChild(to_add[i]);

+ 484 - 0
packages/native-core/src/utils/cursor.rs

@@ -0,0 +1,484 @@
+use std::cmp::Ordering;
+
+use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Pos {
+    pub col: usize,
+    pub row: usize,
+}
+
+impl Pos {
+    pub fn new(col: usize, row: usize) -> Self {
+        Self { row, col }
+    }
+
+    pub fn up(&mut self, rope: &str) {
+        self.move_row(-1, rope);
+    }
+
+    pub fn down(&mut self, rope: &str) {
+        self.move_row(1, rope);
+    }
+
+    pub fn right(&mut self, rope: &str) {
+        self.move_col(1, rope);
+    }
+
+    pub fn left(&mut self, rope: &str) {
+        self.move_col(-1, rope);
+    }
+
+    pub fn move_row(&mut self, change: i32, rope: &str) {
+        let new = self.row as i32 + change;
+        if new >= 0 && new < rope.lines().count() as i32 {
+            self.row = new as usize;
+        }
+    }
+
+    pub fn move_col(&mut self, change: i32, rope: &str) {
+        self.realize_col(rope);
+        let idx = self.idx(rope) as i32;
+        if idx + change >= 0 && idx + change <= rope.len() as i32 {
+            let len_line = self.len_line(rope) as i32;
+            let new_col = self.col as i32 + change;
+            let diff = new_col - len_line;
+            if diff > 0 {
+                self.down(rope);
+                self.col = 0;
+                self.move_col(diff - 1, rope);
+            } else if new_col < 0 {
+                self.up(rope);
+                self.col = self.len_line(rope);
+                self.move_col(new_col + 1, rope);
+            } else {
+                self.col = new_col as usize;
+            }
+        }
+    }
+
+    pub fn col(&self, rope: &str) -> usize {
+        self.col.min(self.len_line(rope))
+    }
+
+    pub fn row(&self) -> usize {
+        self.row
+    }
+
+    fn len_line(&self, rope: &str) -> usize {
+        let line = rope.lines().nth(self.row).unwrap_or_default();
+        let len = line.len();
+        if len > 0 && line.chars().nth(len - 1) == Some('\n') {
+            len - 1
+        } else {
+            len
+        }
+    }
+
+    pub fn idx(&self, rope: &str) -> usize {
+        rope.lines().take(self.row).map(|l| l.len()).sum::<usize>() + self.col(rope)
+    }
+
+    // the column can be more than the line length, cap it
+    pub fn realize_col(&mut self, rope: &str) {
+        self.col = self.col(rope);
+    }
+}
+
+impl Ord for Pos {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.row.cmp(&other.row).then(self.col.cmp(&other.col))
+    }
+}
+
+impl PartialOrd for Pos {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Cursor {
+    pub start: Pos,
+    pub end: Option<Pos>,
+}
+
+impl Cursor {
+    pub fn from_start(pos: Pos) -> Self {
+        Self {
+            start: pos,
+            end: None,
+        }
+    }
+
+    pub fn new(start: Pos, end: Pos) -> Self {
+        Self {
+            start,
+            end: Some(end),
+        }
+    }
+
+    fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
+        if shift {
+            self.with_end(f);
+        } else {
+            f(&mut self.start);
+            self.end = None;
+        }
+    }
+
+    fn delete_selection(&mut self, text: &mut String) -> [i32; 2] {
+        let first = self.first();
+        let last = self.last();
+        let dr = first.row as i32 - last.row as i32;
+        let dc = if dr != 0 {
+            -(last.col as i32)
+        } else {
+            first.col as i32 - last.col as i32
+        };
+        text.replace_range(first.idx(text)..last.idx(text), "");
+        if let Some(end) = self.end.take() {
+            if self.start > end {
+                self.start = end;
+            }
+        }
+        [dc, dr]
+    }
+
+    pub fn handle_input(
+        &mut self,
+        data: &dioxus_html::KeyboardData,
+        text: &mut String,
+        max_width: usize,
+    ) {
+        use Code::*;
+        match data.code() {
+            ArrowUp => {
+                self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT));
+            }
+            ArrowDown => {
+                self.move_cursor(
+                    |c| c.down(text),
+                    data.modifiers().contains(Modifiers::SHIFT),
+                );
+            }
+            ArrowRight => {
+                if data.modifiers().contains(Modifiers::CONTROL) {
+                    self.move_cursor(
+                        |c| {
+                            let mut change = 1;
+                            let idx = c.idx(text);
+                            let length = text.len();
+                            while idx + change < length {
+                                let chr = text.chars().nth(idx + change).unwrap();
+                                if chr.is_whitespace() {
+                                    break;
+                                }
+                                change += 1;
+                            }
+                            c.move_col(change as i32, text);
+                        },
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                } else {
+                    self.move_cursor(
+                        |c| c.right(text),
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                }
+            }
+            ArrowLeft => {
+                if data.modifiers().contains(Modifiers::CONTROL) {
+                    self.move_cursor(
+                        |c| {
+                            let mut change = -1;
+                            let idx = c.idx(text) as i32;
+                            while idx + change > 0 {
+                                let chr = text.chars().nth((idx + change) as usize).unwrap();
+                                if chr == ' ' {
+                                    break;
+                                }
+                                change -= 1;
+                            }
+                            c.move_col(change as i32, text);
+                        },
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                } else {
+                    self.move_cursor(
+                        |c| c.left(text),
+                        data.modifiers().contains(Modifiers::SHIFT),
+                    );
+                }
+            }
+            End => {
+                self.move_cursor(
+                    |c| c.col = c.len_line(text),
+                    data.modifiers().contains(Modifiers::SHIFT),
+                );
+            }
+            Home => {
+                self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
+            }
+            Backspace => {
+                self.start.realize_col(text);
+                let mut start_idx = self.start.idx(text);
+                if self.end.is_some() {
+                    self.delete_selection(text);
+                } else if start_idx > 0 {
+                    self.start.left(text);
+                    text.replace_range(start_idx - 1..start_idx, "");
+                    if data.modifiers().contains(Modifiers::CONTROL) {
+                        start_idx = self.start.idx(text);
+                        while start_idx > 0
+                            && text
+                                .chars()
+                                .nth(start_idx - 1)
+                                .filter(|c| *c != ' ')
+                                .is_some()
+                        {
+                            self.start.left(text);
+                            text.replace_range(start_idx - 1..start_idx, "");
+                            start_idx = self.start.idx(text);
+                        }
+                    }
+                }
+            }
+            Enter => {
+                if text.len() + 1 - self.selection_len(text) <= max_width {
+                    text.insert(self.start.idx(text), '\n');
+                    self.start.col = 0;
+                    self.start.down(text);
+                }
+            }
+            Tab => {
+                if text.len() + 1 - self.selection_len(text) <= max_width {
+                    self.start.realize_col(text);
+                    self.delete_selection(text);
+                    text.insert(self.start.idx(text), '\t');
+                    self.start.right(text);
+                }
+            }
+            _ => {
+                self.start.realize_col(text);
+                if let Key::Character(character) = data.key() {
+                    if text.len() + 1 - self.selection_len(text) <= max_width {
+                        self.delete_selection(text);
+                        let character = character.chars().next().unwrap();
+                        text.insert(self.start.idx(text), character);
+                        self.start.right(text);
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
+        let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
+        f(&mut new);
+        self.end.replace(new);
+    }
+
+    pub fn first(&self) -> &Pos {
+        if let Some(e) = &self.end {
+            e.min(&self.start)
+        } else {
+            &self.start
+        }
+    }
+
+    pub fn last(&self) -> &Pos {
+        if let Some(e) = &self.end {
+            e.max(&self.start)
+        } else {
+            &self.start
+        }
+    }
+
+    pub fn selection_len(&self, text: &str) -> usize {
+        self.last().idx(text) - self.first().idx(text)
+    }
+}
+
+impl Default for Cursor {
+    fn default() -> Self {
+        Self {
+            start: Pos::new(0, 0),
+            end: None,
+        }
+    }
+}
+
+#[test]
+fn pos_direction_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+    pos.down(text);
+    assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len());
+    pos.up(text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+    pos.left(text);
+    assert_eq!(
+        pos.col(text),
+        text.lines().next().unwrap_or_default().len() - 1
+    );
+    pos.right(text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+}
+
+#[test]
+fn pos_col_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    // move inside a row
+    pos.move_col(-5, text);
+    assert_eq!(
+        pos.col(text),
+        text.lines().next().unwrap_or_default().len() - 5
+    );
+    pos.move_col(5, text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+
+    // move between rows
+    pos.move_col(3, text);
+    assert_eq!(pos.col(text), 2);
+    pos.move_col(-3, text);
+    assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len());
+
+    // don't panic if moving out of range
+    pos.move_col(-100, text);
+    pos.move_col(1000, text);
+}
+
+#[test]
+fn cursor_row_movement() {
+    let mut pos = Pos::new(100, 0);
+    let text = "hello world\nhi";
+
+    pos.move_row(1, text);
+    assert_eq!(pos.row(), 1);
+    pos.move_row(-1, text);
+    assert_eq!(pos.row(), 0);
+
+    // don't panic if moving out of range
+    pos.move_row(-100, text);
+    pos.move_row(1000, text);
+}
+
+#[test]
+fn cursor_input() {
+    let mut cursor = Cursor::from_start(Pos::new(0, 0));
+    let mut text = "hello world\nhi".to_string();
+
+    for _ in 0..5 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::ArrowRight,
+                dioxus_html::input_data::keyboard_types::Code::ArrowRight,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            10,
+        );
+    }
+
+    for _ in 0..5 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::Backspace,
+                dioxus_html::input_data::keyboard_types::Code::Backspace,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            10,
+        );
+    }
+
+    assert_eq!(text, " world\nhi");
+
+    let goal_text = "hello world\nhi";
+    let max_width = goal_text.len();
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyH,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyE,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyL,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyL,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    cursor.handle_input(
+        &dioxus_html::KeyboardData::new(
+            dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
+            dioxus_html::input_data::keyboard_types::Code::KeyO,
+            dioxus_html::input_data::keyboard_types::Location::Standard,
+            false,
+            Modifiers::empty(),
+        ),
+        &mut text,
+        max_width,
+    );
+
+    // these should be ignored
+    for _ in 0..10 {
+        cursor.handle_input(
+            &dioxus_html::KeyboardData::new(
+                dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()),
+                dioxus_html::input_data::keyboard_types::Code::KeyO,
+                dioxus_html::input_data::keyboard_types::Location::Standard,
+                false,
+                Modifiers::empty(),
+            ),
+            &mut text,
+            max_width,
+        );
+    }
+
+    assert_eq!(text.to_string(), goal_text);
+}

+ 3 - 0
packages/native-core/src/utils/mod.rs

@@ -0,0 +1,3 @@
+mod persistant_iterator;
+pub use persistant_iterator::*;
+pub mod cursor;

+ 0 - 0
packages/native-core/src/utils.rs → packages/native-core/src/utils/persistant_iterator.rs


+ 6 - 2
packages/rsx/Cargo.toml

@@ -1,8 +1,13 @@
 [package]
 name = "dioxus-rsx"
-version = "0.0.0"
+version = "0.0.1"
 edition = "2018"
 license = "MIT/Apache-2.0"
+description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+documentation = "https://docs.rs/dioxus-rsx"
+keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
@@ -10,5 +15,4 @@ license = "MIT/Apache-2.0"
 proc-macro2 = { version = "1.0", features = ["span-locations"] }
 syn = { version = "1.0", features = ["full", "extra-traits"] }
 quote = { version = "1.0" }
-dioxus-core = { path = "../core", features = ["serialize"] }
 serde = { version = "1.0", features = ["derive"] }

+ 3 - 3
packages/rsx/src/lib.rs

@@ -182,7 +182,7 @@ impl<'a> DynamicContext<'a> {
 
                 let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
                     ElementAttr::AttrText { name, value } if value.is_static() => {
-                        let value = value.source.as_ref().unwrap();
+                        let value = value.to_static().unwrap();
                         quote! {
                             ::dioxus::core::TemplateAttribute::Static {
                                 name: dioxus_elements::#el_name::#name.0,
@@ -196,7 +196,7 @@ impl<'a> DynamicContext<'a> {
                     }
 
                     ElementAttr::CustomAttrText { name, value } if value.is_static() => {
-                        let value = value.source.as_ref().unwrap();
+                        let value = value.to_static().unwrap();
                         quote! {
                             ::dioxus::core::TemplateAttribute::Static {
                                 name: #name,
@@ -244,7 +244,7 @@ impl<'a> DynamicContext<'a> {
             }
 
             BodyNode::Text(text) if text.is_static() => {
-                let text = text.source.as_ref().unwrap();
+                let text = text.to_static().unwrap();
                 quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
             }
 

+ 1 - 0
packages/tui/Cargo.toml

@@ -13,6 +13,7 @@ license = "MIT/Apache-2.0"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+dioxus = { path = "../dioxus", version = "^0.2.1" }
 dioxus-core = { path = "../core", version = "^0.2.1" }
 dioxus-html = { path = "../html", version = "^0.2.1" }
 dioxus-native-core = { path = "../native-core", version = "^0.2.0" }

+ 93 - 0
packages/tui/examples/tui_widgets.rs

@@ -0,0 +1,93 @@
+use dioxus::prelude::*;
+use dioxus_html::FormData;
+use dioxus_tui::prelude::*;
+use dioxus_tui::Config;
+
+fn main() {
+    dioxus_tui::launch_cfg(app, Config::new());
+}
+
+fn app(cx: Scope) -> Element {
+    let bg_green = use_state(cx, || false);
+
+    let color = if *bg_green.get() { "green" } else { "red" };
+    cx.render(rsx! {
+        div{
+            width: "100%",
+            background_color: "{color}",
+            flex_direction: "column",
+            align_items: "center",
+            justify_content: "center",
+
+            Input{
+                oninput: |data: FormData| if &data.value == "good"{
+                    bg_green.set(true);
+                } else{
+                    bg_green.set(false);
+                },
+                r#type: "checkbox",
+                value: "good",
+                width: "50%",
+                height: "10%",
+                checked: "true",
+            }
+            Input{
+                oninput: |data: FormData| if &data.value == "hello world"{
+                    bg_green.set(true);
+                } else{
+                    bg_green.set(false);
+                },
+                width: "50%",
+                height: "10%",
+                maxlength: "11",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "range",
+                width: "50%",
+                height: "10%",
+                min: "20",
+                max: "80",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if data.value == "10"{
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "number",
+                width: "50%",
+                height: "10%",
+                maxlength: "4",
+            }
+            Input{
+                oninput: |data: FormData| {
+                    if data.value == "hello world"{
+                        bg_green.set(true);
+                    } else{
+                        bg_green.set(false);
+                    }
+                },
+                r#type: "password",
+                width: "50%",
+                height: "10%",
+                maxlength: "11",
+            }
+            Input{
+                onclick: |_: FormData| bg_green.set(true),
+                r#type: "button",
+                value: "green",
+                width: "50%",
+                height: "10%",
+            }
+        }
+    })
+}

+ 4 - 1
packages/tui/src/hooks.rs

@@ -287,7 +287,10 @@ impl InnerInputState {
 
         fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
             let Point { x, y } = layout.location;
-            let node_origin = ClientPoint::new(x.into(), y.into());
+            let node_origin = ClientPoint::new(
+                layout_to_screen_space(x).into(),
+                layout_to_screen_space(y).into(),
+            );
 
             let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
                 .to_point()

+ 54 - 1
packages/tui/src/layout.rs

@@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState;
 use dioxus_native_core_macro::sorted_str_slice;
 use taffy::prelude::*;
 
-use crate::screen_to_layout_space;
+use crate::{screen_to_layout_space, unit_to_layout_space};
 
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub(crate) enum PossiblyUninitalized<T> {
@@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout {
                 child_layout.push(l.node.unwrap());
             }
 
+            fn scale_dimention(d: Dimension) -> Dimension {
+                match d {
+                    Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
+                    Dimension::Percent(p) => Dimension::Percent(p),
+                    Dimension::Auto => Dimension::Auto,
+                    Dimension::Undefined => Dimension::Undefined,
+                }
+            }
+            let style = Style {
+                position: Rect {
+                    left: scale_dimention(style.position.left),
+                    right: scale_dimention(style.position.right),
+                    top: scale_dimention(style.position.top),
+                    bottom: scale_dimention(style.position.bottom),
+                },
+                margin: Rect {
+                    left: scale_dimention(style.margin.left),
+                    right: scale_dimention(style.margin.right),
+                    top: scale_dimention(style.margin.top),
+                    bottom: scale_dimention(style.margin.bottom),
+                },
+                padding: Rect {
+                    left: scale_dimention(style.padding.left),
+                    right: scale_dimention(style.padding.right),
+                    top: scale_dimention(style.padding.top),
+                    bottom: scale_dimention(style.padding.bottom),
+                },
+                border: Rect {
+                    left: scale_dimention(style.border.left),
+                    right: scale_dimention(style.border.right),
+                    top: scale_dimention(style.border.top),
+                    bottom: scale_dimention(style.border.bottom),
+                },
+                gap: Size {
+                    width: scale_dimention(style.gap.width),
+                    height: scale_dimention(style.gap.height),
+                },
+                flex_basis: scale_dimention(style.flex_basis),
+                size: Size {
+                    width: scale_dimention(style.size.width),
+                    height: scale_dimention(style.size.height),
+                },
+                min_size: Size {
+                    width: scale_dimention(style.min_size.width),
+                    height: scale_dimention(style.min_size.height),
+                },
+                max_size: Size {
+                    width: scale_dimention(style.max_size.width),
+                    height: scale_dimention(style.max_size.height),
+                },
+                ..style
+            };
+
             if let PossiblyUninitalized::Initialized(n) = self.node {
                 if self.style != style {
                     taffy.set_style(n, style).unwrap();

+ 16 - 1
packages/tui/src/lib.rs

@@ -1,5 +1,6 @@
 use anyhow::Result;
 use crossterm::{
+    cursor::{MoveTo, RestorePosition, SavePosition, Show},
     event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
     execute,
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@@ -28,11 +29,13 @@ mod focus;
 mod hooks;
 mod layout;
 mod node;
+pub mod prelude;
 pub mod query;
 mod render;
 mod style;
 mod style_attributes;
 mod widget;
+mod widgets;
 
 pub use config::*;
 pub use hooks::*;
@@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
     screen as f32 * 10.0
 }
 
+pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
+    screen * 10.0
+}
+
 pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
     layout / 10.0
 }
@@ -136,7 +143,13 @@ fn render_vdom(
             let mut terminal = (!cfg.headless).then(|| {
                 enable_raw_mode().unwrap();
                 let mut stdout = std::io::stdout();
-                execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
+                execute!(
+                    stdout,
+                    EnterAlternateScreen,
+                    EnableMouseCapture,
+                    MoveTo(0, 1000)
+                )
+                .unwrap();
                 let backend = CrosstermBackend::new(io::stdout());
                 Terminal::new(backend).unwrap()
             });
@@ -181,6 +194,7 @@ fn render_vdom(
                         taffy.compute_layout(root_node, size).unwrap();
                     }
                     if let Some(terminal) = &mut terminal {
+                        execute!(terminal.backend_mut(), SavePosition).unwrap();
                         terminal.draw(|frame| {
                             let rdom = rdom.borrow();
                             let mut taffy = taffy.lock().expect("taffy lock poisoned");
@@ -189,6 +203,7 @@ fn render_vdom(
                             let root = &rdom[NodeId(0)];
                             render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO);
                         })?;
+                        execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
                     } else {
                         let rdom = rdom.borrow();
                         resize(

+ 1 - 0
packages/tui/src/prelude/mod.rs

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

+ 1 - 1
packages/tui/src/render.rs

@@ -41,7 +41,7 @@ pub(crate) fn render_vnode(
     let x = layout_to_screen_space(fx).round() as u16;
     let y = layout_to_screen_space(fy).round() as u16;
     let Size { width, height } = *size;
-    let width = layout_to_screen_space(fx + width).round() as u16 + x;
+    let width = layout_to_screen_space(fx + width).round() as u16 - x;
     let height = layout_to_screen_space(fy + height).round() as u16 - y;
 
     match &node.node_data.node_type {

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

@@ -0,0 +1,59 @@
+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);
+    };
+    cx.render(rsx! {
+        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}"
+        }
+    })
+}

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

@@ -0,0 +1,82 @@
+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);
+    };
+    cx.render(rsx! {
+        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}"
+        }
+    })
+}

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

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

+ 18 - 0
packages/tui/src/widgets/mod.rs

@@ -0,0 +1,18 @@
+mod button;
+mod checkbox;
+mod input;
+mod number;
+mod password;
+mod slider;
+mod textbox;
+
+use dioxus_core::{ElementId, RenderReturn, Scope};
+pub use input::*;
+
+pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
+    if let RenderReturn::Sync(Ok(sync)) = cx.root_node() {
+        sync.root_ids.get(0).map(|id| id.get())
+    } else {
+        None
+    }
+}

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

@@ -0,0 +1,209 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+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 std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct NumbericInputProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    let update = |text: String| {
+        if let Some(input_handler) = &cx.props.raw_oninput {
+            input_handler.call(FormData {
+                value: text,
+                values: HashMap::new(),
+                files: None,
+            });
+        }
+    };
+    let increase = move || {
+        let mut text = text_ref.write();
+        *text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
+        update(text.clone());
+    };
+    let decrease = move || {
+        let mut text = text_ref.write();
+        *text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
+        update(text.clone());
+    };
+
+    render! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border}",
+
+            onkeydown: move |k| {
+                let is_text = match k.key(){
+                    Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
+                    Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true,
+                    _  => false,
+                };
+                if is_text{
+                    let mut text = text_ref.write();
+                    cursor.write().handle_input(&k, &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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                    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());
+                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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                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}"
+        }
+    }
+}

+ 186 - 0
packages/tui/src/widgets/password.rs

@@ -0,0 +1,186 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+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 std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct PasswordProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
+    let text_highlighted = ".".repeat(text_highlighted.len());
+    let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    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, &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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                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());
+                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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                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}"
+        }
+    }
+}

+ 108 - 0
packages/tui/src/widgets/slider.rs

@@ -0,0 +1,108 @@
+use std::collections::HashMap;
+
+use crate::widgets::get_root_id;
+use crate::Query;
+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 SliderProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+    #[props(!optional)]
+    min: Option<&'a str>,
+    #[props(!optional)]
+    max: Option<&'a str>,
+    #[props(!optional)]
+    step: Option<&'a str>,
+}
+
+#[allow(non_snake_case)]
+pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+
+    let value_state = use_state(cx, || 0.0);
+    let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
+    let width = cx.props.width.unwrap_or("20px");
+    let height = cx.props.height.unwrap_or("1px");
+    let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
+    let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
+    let size = max - min;
+    let step = cx
+        .props
+        .step
+        .and_then(|v| v.parse().ok())
+        .unwrap_or(size / 10.0);
+
+    let current_value = if let Some(value) = value {
+        value
+    } else {
+        *value_state.get()
+    }
+    .max(min)
+    .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).max(min).min(max));
+                        update(value_state.current().to_string());
+                    }
+                    Key::ArrowRight => {
+                        value_state.set((current_value + step).max(min).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)",
+            }
+        }
+    }
+}

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

@@ -0,0 +1,182 @@
+use crate::widgets::get_root_id;
+use crate::Query;
+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 std::{collections::HashMap, io::stdout};
+use taffy::geometry::Point;
+
+#[derive(Props)]
+pub(crate) struct TextBoxProps<'a> {
+    #[props(!optional)]
+    raw_oninput: Option<&'a EventHandler<'a, FormData>>,
+    #[props(!optional)]
+    value: Option<&'a str>,
+    #[props(!optional)]
+    size: Option<&'a str>,
+    #[props(!optional)]
+    max_length: Option<&'a str>,
+    #[props(!optional)]
+    width: Option<&'a str>,
+    #[props(!optional)]
+    height: Option<&'a str>,
+}
+#[allow(non_snake_case)]
+pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
+    let tui_query: Query = cx.consume_context().unwrap();
+    let tui_query_clone = tui_query.clone();
+
+    let text_ref = use_ref(cx, || {
+        if let Some(intial_text) = cx.props.value {
+            intial_text.to_string()
+        } else {
+            String::new()
+        }
+    });
+    let cursor = use_ref(cx, Cursor::default);
+    let dragging = use_state(cx, || false);
+
+    let text = text_ref.read().clone();
+    let start_highlight = cursor.read().first().idx(&text);
+    let end_highlight = cursor.read().last().idx(&text);
+    let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
+    let (text_highlighted, text_after_second_cursor) =
+        text_after_first_cursor.split_at(end_highlight - start_highlight);
+
+    let max_len = cx
+        .props
+        .max_length
+        .as_ref()
+        .and_then(|s| s.parse().ok())
+        .unwrap_or(usize::MAX);
+
+    let width = cx
+        .props
+        .width
+        .map(|s| s.to_string())
+        // px is the same as em in tui
+        .or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
+        .unwrap_or_else(|| "10px".to_string());
+    let height = cx.props.height.unwrap_or("3px");
+
+    // don't draw a border unless there is enough space
+    let border = if width
+        .strip_suffix("px")
+        .and_then(|w| w.parse::<i32>().ok())
+        .filter(|w| *w < 3)
+        .is_some()
+        || height
+            .strip_suffix("px")
+            .and_then(|h| h.parse::<i32>().ok())
+            .filter(|h| *h < 3)
+            .is_some()
+    {
+        "none"
+    } else {
+        "solid"
+    };
+
+    render! {
+        div{
+            width: "{width}",
+            height: "{height}",
+            border_style: "{border}",
+
+            onkeydown: move |k| {
+                if k.key() == Key::Enter {
+                    return;
+                }
+                let mut text = text_ref.write();
+                cursor.write().handle_input(&k, &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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                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());
+                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 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1});
+                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}"
+        }
+    }
+}