Przeglądaj źródła

Merge pull request #646 from Demonthos/tui_widgets

Tui widgets
Jon Kelley 2 lat temu
rodzic
commit
5dc86fe0b7

+ 1 - 2
packages/native-core-macro/src/lib.rs

@@ -306,8 +306,7 @@ impl Member {
                     + field.ty.to_token_stream().to_string().as_str())
                 .as_str(),
                 Span::call_site(),
-            )
-            .into(),
+            ),
             ident: field.ident.as_ref()?.clone(),
         })
     }

+ 10 - 23
packages/native-core/src/real_dom.rs

@@ -102,11 +102,7 @@ impl<S: State> RealDom<S> {
         self.tree.add_child(node_id, child_id);
     }
 
-    fn create_template_node(
-        &mut self,
-        node: &TemplateNode,
-        mutations_vec: &mut FxHashMap<RealNodeId, NodeMask>,
-    ) -> RealNodeId {
+    fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId {
         match node {
             TemplateNode::Element {
                 tag,
@@ -139,27 +135,18 @@ impl<S: State> RealDom<S> {
                 });
                 let node_id = self.create_node(node);
                 for child in *children {
-                    let child_id = self.create_template_node(child, mutations_vec);
+                    let child_id = self.create_template_node(child);
                     self.add_child(node_id, child_id);
                 }
                 node_id
             }
-            TemplateNode::Text { text } => {
-                let node_id = self.create_node(Node::new(NodeType::Text {
-                    text: text.to_string(),
-                }));
-                node_id
-            }
-            TemplateNode::Dynamic { .. } => {
-                let node_id = self.create_node(Node::new(NodeType::Placeholder));
-                node_id
-            }
-            TemplateNode::DynamicText { .. } => {
-                let node_id = self.create_node(Node::new(NodeType::Text {
-                    text: String::new(),
-                }));
-                node_id
-            }
+            TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text {
+                text: text.to_string(),
+            })),
+            TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)),
+            TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text {
+                text: String::new(),
+            })),
         }
     }
 
@@ -172,7 +159,7 @@ impl<S: State> RealDom<S> {
         for template in mutations.templates {
             let mut template_root_ids = Vec::new();
             for root in template.roots {
-                let id = self.create_template_node(root, &mut nodes_updated);
+                let id = self.create_template_node(root);
                 template_root_ids.push(id);
             }
             self.templates

+ 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


+ 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();

+ 18 - 3
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,14 +194,16 @@ 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");
                             // size is guaranteed to not change when rendering
-                            resize(frame.size(), &mut *taffy, &rdom);
+                            resize(frame.size(), &mut taffy, &rdom);
                             let root = &rdom[NodeId(0)];
-                            render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO);
+                            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/query.rs

@@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> {
             .ok();
         layout.map(|layout| Layout {
             order: layout.order,
-            size: layout.size.map(|v| layout_to_screen_space(v)),
+            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),

+ 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}"
+        }
+    }
+}