diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs new file mode 100644 index 000000000..31d2e1ce0 --- /dev/null +++ b/examples/tui_buttons.rs @@ -0,0 +1,93 @@ +use dioxus::{events::KeyCode, prelude::*}; + +fn main() { + dioxus::tui::launch(app); +} + +#[derive(PartialEq, Props)] +struct ButtonProps { + color_offset: u32, + layer: u16, +} + +#[allow(non_snake_case)] +fn Button(cx: Scope) -> Element { + let toggle = use_state(&cx, || false); + let hovered = use_state(&cx, || false); + + let hue = cx.props.color_offset % 255; + let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 }; + let brightness = saturation / 2; + let color = format!("hsl({hue}, {saturation}, {brightness})"); + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + background_color: "{color}", + tabindex: "{cx.props.layer}", + onkeydown: |e| { + if let KeyCode::Space = e.data.key_code{ + toggle.modify(|f| !f); + } + }, + onclick: |_| { + toggle.modify(|f| !f); + }, + onmouseenter: |_|{ + hovered.set(true); + }, + onmouseleave: |_|{ + hovered.set(false); + }, + justify_content: "center", + align_items: "center", + display: "flex", + flex_direction: "column", + + p{"tabindex: {cx.props.layer}"} + } + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + display: "flex", + flex_direction: "column", + width: "100%", + height: "100%", + + (1..8).map(|y| + cx.render(rsx!{ + div{ + display: "flex", + flex_direction: "row", + width: "100%", + height: "100%", + (1..8).map(|x|{ + if (x + y) % 2 == 0{ + cx.render(rsx!{ + div{ + width: "100%", + height: "100%", + background_color: "rgb(100, 100, 100)", + } + }) + } + else{ + let layer = (x + y) % 3; + cx.render(rsx!{ + Button{ + color_offset: x * y, + layer: layer as u16, + } + }) + } + }) + } + }) + ) + } + }) +} diff --git a/packages/native-core-macro/tests/peristant_iterator.rs b/packages/native-core-macro/tests/peristant_iterator.rs new file mode 100644 index 000000000..29df3afe5 --- /dev/null +++ b/packages/native-core-macro/tests/peristant_iterator.rs @@ -0,0 +1,342 @@ +use dioxus_native_core::{ + real_dom::{NodeType, RealDom}, + state::State, + utils::PersistantElementIter, +}; +use dioxus_native_core_macro::State; + +#[derive(State, Default, Clone)] +struct Empty {} + +#[test] +#[allow(unused_variables)] +fn traverse() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let mutations = vdom.create_vnodes(rsx! { + div{ + div{ + "hello" + p{ + "world" + } + "hello world" + } + } + }); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![mutations]); + + let mut iter = PersistantElementIter::new(); + let div_tag = "div".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + let text1 = "hello".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + let p_tag = "p".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text2 = "world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + let text3 = "hello world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_removes() { + use dioxus_core::VNode; + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + // this will end on the node that is removed + let mut iter1 = PersistantElementIter::new(); + // this will end on the after node that is removed + let mut iter2 = PersistantElementIter::new(); + // div + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "hello" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "world" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter2.next(&rdom).id(); + // "hello world" + iter2.next(&rdom).id(); + + iter1.prune(&update, &rdom); + iter2.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); + let div_tag = "div".to_string(); + let idx = iter2.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: div_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_before() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "div".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_after() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); +} diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index a6e011fa9..2319ca4ac 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -2,3 +2,4 @@ pub mod layout_attributes; pub mod node_ref; pub mod real_dom; pub mod state; +pub mod utils; diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs new file mode 100644 index 000000000..143149521 --- /dev/null +++ b/packages/native-core/src/utils.rs @@ -0,0 +1,230 @@ +use crate::{ + real_dom::{NodeType, RealDom}, + state::State, +}; +use dioxus_core::{DomEdit, ElementId, Mutations}; + +pub enum ElementProduced { + Progressed(ElementId), + Looped(ElementId), +} +impl ElementProduced { + pub fn id(&self) -> ElementId { + match self { + ElementProduced::Progressed(id) => *id, + ElementProduced::Looped(id) => *id, + } + } +} + +#[derive(Debug)] +enum NodePosition { + AtNode, + InChild(usize), +} + +impl NodePosition { + fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self { + match self { + Self::AtNode => Self::AtNode, + Self::InChild(i) => Self::InChild(f(*i)), + } + } + + fn get_or_insert(&mut self, child_idx: usize) -> usize { + match self { + Self::AtNode => { + *self = Self::InChild(child_idx); + child_idx + } + Self::InChild(i) => *i, + } + } +} + +/// The focus system needs a iterator that can persist through changes in the [VirtualDom]. +/// Iterate through it with [ElementIter::next] [ElementIter::prev], and update it with [ElementIter::update] (with data from [`VirtualDom::work_with_deadline`]). +/// The iterator loops around when it reaches the end or the beginning. +pub struct PersistantElementIter { + // stack of elements and fragments + stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>, +} + +impl Default for PersistantElementIter { + fn default() -> Self { + PersistantElementIter { + stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)], + } + } +} + +impl PersistantElementIter { + pub fn new() -> Self { + Self::default() + } + + /// remove stale element refreneces + /// returns true if the focused element is removed + pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { + let mut changed = false; + let ids_removed: Vec<_> = mutations + .edits + .iter() + .filter_map(|e| { + if let DomEdit::Remove { root } = e { + Some(*root) + } else { + None + } + }) + .collect(); + // if any element is removed in the chain, remove it and its children from the stack + if let Some(r) = self + .stack + .iter() + .position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id)) + { + self.stack.truncate(r); + changed = true; + } + // if a child is removed or inserted before or at the current element, update the child index + for (el_id, child_idx) in self.stack.iter_mut() { + if let NodePosition::InChild(child_idx) = child_idx { + if let NodeType::Element { children, .. } = &rdom[*el_id].node_type { + for m in &mutations.edits { + match m { + DomEdit::Remove { root } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx -= 1; + } + } + DomEdit::InsertBefore { root, n } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + DomEdit::InsertAfter { root, n } => { + if children + .iter() + .take(*child_idx) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + _ => (), + } + } + } + } + } + changed + } + + /// get the next element + pub fn next(&mut self, rdom: &RealDom) -> ElementProduced { + if self.stack.is_empty() { + let id = ElementId(0); + let new = (id, NodePosition::AtNode); + self.stack.push(new); + ElementProduced::Looped(id) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + *o_child_idx = o_child_idx.map(|i| i + 1); + // if we have children, go to the next child + let child_idx = o_child_idx.get_or_insert(0); + if child_idx >= children.len() { + self.pop(); + self.next(rdom) + } else { + let id = children[child_idx]; + if let NodeType::Element { .. } = &rdom[id].node_type { + self.stack.push((id, NodePosition::AtNode)); + } + ElementProduced::Progressed(id) + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + /// get the previous element + pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + // recursively add the last child element to the stack + fn push_back( + stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>, + new_node: ElementId, + rdom: &RealDom, + ) -> ElementId { + match &rdom[new_node].node_type { + NodeType::Element { children, .. } => { + if children.is_empty() { + new_node + } else { + stack.push((new_node, NodePosition::InChild(children.len() - 1))); + push_back(stack, *children.last().unwrap(), rdom) + } + } + _ => new_node, + } + } + if self.stack.is_empty() { + let new_node = ElementId(0); + ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom)) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + // if we have children, go to the next child + if let NodePosition::InChild(0) = o_child_idx { + ElementProduced::Progressed(self.pop()) + } else { + *o_child_idx = o_child_idx.map(|i| i - 1); + if let NodePosition::InChild(child_idx) = o_child_idx { + if *child_idx >= children.len() || children.is_empty() { + self.pop(); + self.prev(rdom) + } else { + let new_node = children[*child_idx]; + ElementProduced::Progressed(push_back( + &mut self.stack, + new_node, + rdom, + )) + } + } else { + self.pop(); + self.prev(rdom) + } + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + fn pop(&mut self) -> ElementId { + self.stack.pop().unwrap().0 + } +} diff --git a/packages/tui/README.md b/packages/tui/README.md index 20adaeb6c..ccce60ea3 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -85,11 +85,10 @@ Rink features: - [x] Flexbox based layout system - [ ] CSS selectors - [x] inline CSS support -- [ ] Built-in focusing system +- [x] Built-in focusing system - [ ] high-quality keyboard support -- [ ] Support for events, hooks, and callbacks -* [ ] Html tags1 - -1 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. - +* [ ] Support for events, hooks, and callbacks1 +* [ ] Html tags2 +1 Basic keyboard and mouse events are implemented. +2 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs new file mode 100644 index 000000000..18e2fbd3f --- /dev/null +++ b/packages/tui/src/focus.rs @@ -0,0 +1,282 @@ +use crate::{node::PreventDefault, Dom}; + +use dioxus_core::ElementId; +use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; +use dioxus_native_core_macro::sorted_str_slice; + +use std::{cmp::Ordering, num::NonZeroU16}; + +use dioxus_native_core::{ + node_ref::{AttributeMask, NodeMask, NodeView}, + real_dom::NodeType, + state::NodeDepState, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum FocusLevel { + Unfocusable, + Focusable, + Ordered(std::num::NonZeroU16), +} + +impl FocusLevel { + pub fn focusable(&self) -> bool { + match self { + FocusLevel::Unfocusable => false, + FocusLevel::Focusable => true, + FocusLevel::Ordered(_) => true, + } + } +} + +impl PartialOrd for FocusLevel { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (FocusLevel::Unfocusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Equal), + (FocusLevel::Unfocusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Less), + (FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Less), + (FocusLevel::Focusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Focusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Equal), + (FocusLevel::Focusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Ordered(_), FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Ordered(_), FocusLevel::Focusable) => Some(std::cmp::Ordering::Less), + (FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.partial_cmp(b), + } + } +} + +impl Ord for FocusLevel { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl Default for FocusLevel { + fn default() -> Self { + FocusLevel::Unfocusable + } +} + +#[derive(Clone, PartialEq, Debug, Default)] +pub(crate) struct Focus { + pub level: FocusLevel, +} + +impl NodeDepState for Focus { + type Ctx = (); + type DepState = (); + const NODE_MASK: NodeMask = + NodeMask::new_with_attrs(AttributeMask::Static(FOCUS_ATTRIBUTES)).with_listeners(); + + fn reduce(&mut self, node: NodeView<'_>, _sibling: &Self::DepState, _: &Self::Ctx) -> bool { + let new = Focus { + level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { + if let Some(index) = a + .value + .as_int32() + .or(a.value.as_text().and_then(|v| v.parse::().ok())) + { + match index.cmp(&0) { + Ordering::Less => FocusLevel::Unfocusable, + Ordering::Equal => FocusLevel::Focusable, + Ordering::Greater => { + FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap()) + } + } + } else { + FocusLevel::Unfocusable + } + } else if node + .listeners() + .iter() + .any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok()) + { + FocusLevel::Focusable + } else { + FocusLevel::Unfocusable + }, + }; + if *self != new { + *self = new; + true + } else { + false + } + } +} + +const FOCUS_EVENTS: &[&str] = &sorted_str_slice!(["keydown", "keypress", "keyup"]); +const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]); + +#[derive(Default)] +pub(crate) struct FocusState { + pub(crate) focus_iter: PersistantElementIter, + pub(crate) last_focused_id: Option, + pub(crate) focus_level: FocusLevel, + pub(crate) dirty: bool, +} + +impl FocusState { + /// Returns true if the focus has changed. + pub fn progress(&mut self, rdom: &mut Dom, forward: bool) -> bool { + if let Some(last) = self.last_focused_id { + if rdom[last].state.prevent_default == PreventDefault::KeyDown { + return false; + } + } + // the id that started focused to track when a loop has happened + let mut loop_marker_id = self.last_focused_id; + let focus_level = &mut self.focus_level; + let mut next_focus = None; + + loop { + let new = if forward { + self.focus_iter.next(rdom) + } else { + self.focus_iter.prev(rdom) + }; + let new_id = new.id(); + if let ElementProduced::Looped(_) = new { + let mut closest_level = None; + + if forward { + // find the closest focusable element after the current level + rdom.traverse_depth_first(|n| { + let node_level = n.state.focus.level; + if node_level != *focus_level + && node_level.focusable() + && node_level > *focus_level + { + if let Some(level) = &mut closest_level { + if node_level < *level { + *level = node_level; + } + } else { + closest_level = Some(node_level); + } + } + }); + } else { + // find the closest focusable element before the current level + rdom.traverse_depth_first(|n| { + let node_level = n.state.focus.level; + if node_level != *focus_level + && node_level.focusable() + && node_level < *focus_level + { + if let Some(level) = &mut closest_level { + if node_level > *level { + *level = node_level; + } + } else { + closest_level = Some(node_level); + } + } + }); + } + + // extend the loop_marker_id to allow for another pass + loop_marker_id = None; + + if let Some(level) = closest_level { + *focus_level = level; + } else if forward { + *focus_level = FocusLevel::Unfocusable; + } else { + *focus_level = FocusLevel::Focusable; + } + } + + // once we have looked at all the elements exit the loop + if let Some(last) = loop_marker_id { + if new_id == last { + break; + } + } else { + loop_marker_id = Some(new_id); + } + + let current_level = rdom[new_id].state.focus.level; + let after_previous_focused = if forward { + current_level >= *focus_level + } else { + current_level <= *focus_level + }; + if after_previous_focused && current_level.focusable() && current_level == *focus_level + { + next_focus = Some(new_id); + break; + } + } + + if let Some(id) = next_focus { + rdom[id].state.focused = true; + if let Some(old) = self.last_focused_id.replace(id) { + rdom[old].state.focused = false; + } + // reset the position to the currently focused element + while self.focus_iter.next(rdom).id() != id {} + self.dirty = true; + return true; + } + + false + } + + pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &Dom) { + fn remove_children( + to_prune: &mut [&mut Option], + rdom: &Dom, + removed: ElementId, + ) { + for opt in to_prune.iter_mut() { + if let Some(id) = opt { + if *id == removed { + **opt = None; + } + } + } + if let NodeType::Element { children, .. } = &rdom[removed].node_type { + for child in children { + remove_children(to_prune, rdom, *child); + } + } + } + if self.focus_iter.prune(mutations, rdom) { + self.dirty = true; + } + for m in &mutations.edits { + match m { + dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children( + &mut [&mut self.last_focused_id], + rdom, + ElementId(*root as usize), + ), + dioxus_core::DomEdit::Remove { root } => remove_children( + &mut [&mut self.last_focused_id], + rdom, + ElementId(*root as usize), + ), + _ => (), + } + } + } + + pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: ElementId) { + if let Some(old) = self.last_focused_id.replace(id) { + rdom[old].state.focused = false; + } + let state = &mut rdom[id].state; + state.focused = true; + self.focus_level = state.focus.level; + // reset the position to the currently focused element + while self.focus_iter.next(rdom).id() != id {} + self.dirty = true; + } + + pub(crate) fn clean(&mut self) -> bool { + let old = self.dirty; + self.dirty = false; + old + } +} diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 3a1646fdf..b62cc256c 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -12,7 +12,7 @@ use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet}; use dioxus_html::{on::*, KeyCode}; use std::{ any::Any, - cell::RefCell, + cell::{RefCell, RefMut}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -20,6 +20,7 @@ use std::{ use taffy::geometry::{Point, Size}; use taffy::{prelude::Layout, Taffy}; +use crate::FocusState; use crate::{Dom, Node}; // a wrapper around the input state for easier access @@ -36,22 +37,22 @@ use crate::{Dom, Node}; // pub fn mouse(&self) -> Option { // let data = (**self.0).borrow(); -// data.mouse.as_ref().map(|m| m.clone()) +// mouse.as_ref().map(|m| m.clone()) // } // pub fn wheel(&self) -> Option { // let data = (**self.0).borrow(); -// data.wheel.as_ref().map(|w| w.clone()) +// wheel.as_ref().map(|w| w.clone()) // } // pub fn screen(&self) -> Option<(u16, u16)> { // let data = (**self.0).borrow(); -// data.screen.as_ref().map(|m| m.clone()) +// screen.as_ref().map(|m| m.clone()) // } // pub fn last_key_pressed(&self) -> Option { // let data = (**self.0).borrow(); -// data.last_key_pressed +// last_key_pressed // .as_ref() // .map(|k| &k.0.clone()) // } @@ -84,6 +85,7 @@ pub struct InnerInputState { wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, + pub(crate) focus_state: FocusState, // subscribers: Vec>, } @@ -95,6 +97,7 @@ impl InnerInputState { last_key_pressed: None, screen: None, // subscribers: Vec::new(), + focus_state: FocusState::default(), } } @@ -151,7 +154,7 @@ impl InnerInputState { fn update( &mut self, - evts: &mut [EventCore], + evts: &mut Vec, resolved_events: &mut Vec, layout: &Taffy, dom: &mut Dom, @@ -160,19 +163,57 @@ impl InnerInputState { self.wheel = None; + let old_focus = self.focus_state.last_focused_id; + + evts.retain(|e| match &e.1 { + EventData::Keyboard(k) => match k.key_code { + KeyCode::Tab => !self.focus_state.progress(dom, !k.shift_key), + _ => true, + }, + _ => true, + }); + for e in evts.iter_mut() { self.apply_event(e); } self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom); + if old_focus != self.focus_state.last_focused_id { + if let Some(id) = self.focus_state.last_focused_id { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focus", + element: Some(id), + data: Arc::new(FocusData {}), + }); + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focusin", + element: Some(id), + data: Arc::new(FocusData {}), + }); + } + if let Some(id) = old_focus { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focusout", + element: Some(id), + data: Arc::new(FocusData {}), + }); + } + } + // for s in &self.subscribers { // s(); // } } fn resolve_mouse_events( - &self, + &mut self, previous_mouse: Option, resolved_events: &mut Vec, layout: &Taffy, @@ -256,16 +297,16 @@ impl InnerInputState { if old_pos != Some(new_pos) { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousemove") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let node_layout = get_abs_layout(node, dom, layout); let previously_contained = old_pos - .filter(|pos| layout_contains_point(node_layout, *pos)) + .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains && previously_contained { try_create_event( "mousemove", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -280,11 +321,11 @@ impl InnerInputState { // mouseenter let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseenter") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let node_layout = get_abs_layout(node, dom, layout); let previously_contained = old_pos - .filter(|pos| layout_contains_point(node_layout, *pos)) + .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains && !previously_contained { try_create_event( @@ -303,16 +344,16 @@ impl InnerInputState { // mouseover let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseover") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let node_layout = get_abs_layout(node, dom, layout); let previously_contained = old_pos - .filter(|pos| layout_contains_point(node_layout, *pos)) + .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains && !previously_contained { try_create_event( "mouseover", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -326,13 +367,13 @@ impl InnerInputState { if was_pressed { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousedown") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let node_layout = get_abs_layout(node, dom, layout); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "mousedown", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -347,13 +388,13 @@ impl InnerInputState { if was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseup") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let node_layout = get_abs_layout(node, dom, layout); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "mouseup", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -369,13 +410,13 @@ impl InnerInputState { if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("click") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let node_layout = get_abs_layout(node, dom, layout); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "click", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -392,13 +433,13 @@ impl InnerInputState { { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("contextmenu") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let node_layout = get_abs_layout(node, dom, layout); + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( "contextmenu", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -415,9 +456,9 @@ impl InnerInputState { if wheel_delta != 0.0 { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("wheel") { - let node_layout = - layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let node_layout = get_abs_layout(node, dom, layout); + + let currently_contains = layout_contains_point(&node_layout, new_pos); if currently_contains { try_create_event( @@ -438,16 +479,16 @@ impl InnerInputState { // mouseleave let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseleave") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let node_layout = get_abs_layout(node, dom, layout); let previously_contained = old_pos - .filter(|pos| layout_contains_point(node_layout, *pos)) + .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let currently_contains = layout_contains_point(&node_layout, new_pos); if !currently_contains && previously_contained { try_create_event( "mouseleave", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -461,16 +502,16 @@ impl InnerInputState { // mouseout let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseout") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let node_layout = get_abs_layout(node, dom, layout); let previously_contained = old_pos - .filter(|pos| layout_contains_point(node_layout, *pos)) + .filter(|pos| layout_contains_point(&node_layout, *pos)) .is_some(); - let currently_contains = layout_contains_point(node_layout, new_pos); + let currently_contains = layout_contains_point(&node_layout, new_pos); if !currently_contains && previously_contained { try_create_event( "mouseout", - Arc::new(prepare_mouse_data(mouse_data, node_layout)), + Arc::new(prepare_mouse_data(mouse_data, &node_layout)), &mut will_bubble, resolved_events, node, @@ -479,6 +520,22 @@ impl InnerInputState { } } } + + // update focus + if was_released { + let mut focus_id = None; + dom.traverse_depth_first(|node| { + let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let currently_contains = layout_contains_point(node_layout, new_pos); + + if currently_contains && node.state.focus.level.focusable() { + focus_id = Some(node.id); + } + }); + if let Some(id) = focus_id { + self.focus_state.set_focus(dom, id); + } + } } } @@ -487,6 +544,22 @@ impl InnerInputState { // } } +fn get_abs_layout(node: &Node, dom: &Dom, taffy: &Taffy) -> Layout { + let mut node_layout = taffy + .layout(node.state.layout.node.unwrap()) + .unwrap() + .clone(); + let mut current = node; + while let Some(parent_id) = current.parent { + let parent = &dom[parent_id]; + current = parent; + let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap(); + node_layout.location.x += parent_layout.location.x; + node_layout.location.y += parent_layout.location.y; + } + node_layout +} + pub struct RinkInputHandler { state: Rc>, queued_events: Rc>>, @@ -523,6 +596,10 @@ impl RinkInputHandler { ) } + pub(crate) fn prune(&self, mutations: &dioxus_core::Mutations, rdom: &Dom) { + self.state.borrow_mut().focus_state.prune(mutations, rdom); + } + pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut Dom) -> Vec { let mut resolved_events = Vec::new(); @@ -556,7 +633,6 @@ impl RinkInputHandler { }) .map(|evt| (evt.0, evt.1.into_any())); - // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); for (event, data) in events { if let Some(v) = hm.get_mut(event) { @@ -568,19 +644,25 @@ impl RinkInputHandler { for (event, datas) in hm { for node in dom.get_listening_sorted(event) { for data in &datas { - resolved_events.push(UserEvent { - scope_id: None, - priority: EventPriority::Medium, - name: event, - element: Some(node.id), - data: data.clone(), - }); + if node.state.focused { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: event, + element: Some(node.id), + data: data.clone(), + }); + } } } } resolved_events } + + pub(crate) fn state(&self) -> RefMut { + self.state.borrow_mut() + } } // translate crossterm events into dioxus events diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 22d70524e..c8be33717 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,23 +7,23 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use dioxus_native_core::{real_dom::RealDom, state::*}; -use dioxus_native_core_macro::State; +use dioxus_native_core::real_dom::RealDom; +use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, pin_mut, StreamExt, }; -use layout::TaffyLayout; use std::cell::RefCell; use std::rc::Rc; use std::{io, time::Duration}; -use style_attributes::StyleModifier; use taffy::{geometry::Point, prelude::Size, Taffy}; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod focus; mod hooks; mod layout; +mod node; mod render; mod style; mod style_attributes; @@ -31,18 +31,7 @@ mod widget; pub use config::*; pub use hooks::*; - -type Dom = RealDom; -type Node = dioxus_native_core::real_dom::Node; - -#[derive(Debug, Clone, State, Default)] -struct NodeState { - #[child_dep_state(layout, RefCell)] - layout: TaffyLayout, - // depends on attributes, the C component of it's parent and a u8 context - #[parent_dep_state(style)] - style: StyleModifier, -} +pub(crate) use node::*; #[derive(Clone)] pub struct TuiContext { @@ -130,7 +119,7 @@ fn render_vdom( } let mut to_rerender: fxhash::FxHashSet = vec![0].into_iter().collect(); - let mut resized = true; + let mut updated = true; loop { /* @@ -144,8 +133,8 @@ fn render_vdom( todo: lazy re-rendering */ - if !to_rerender.is_empty() || resized { - resized = false; + if !to_rerender.is_empty() || updated { + updated = false; fn resize(dims: Rect, taffy: &mut Taffy, rdom: &Dom) { let width = dims.width; let height = dims.height; @@ -209,7 +198,7 @@ fn render_vdom( break; } } - TermEvent::Resize(_, _) => resized = true, + TermEvent::Resize(_, _) => updated = true, TermEvent::Mouse(_) => {} }, InputEvent::Close => break, @@ -224,10 +213,16 @@ fn render_vdom( { let evts = handler.get_events(&taffy.borrow(), &mut rdom); + { + updated |= handler.state().focus_state.clean(); + } for e in evts { vdom.handle_message(SchedulerMsg::Event(e)); } let mutations = vdom.work_with_deadline(|| false); + for m in &mutations { + handler.prune(m, &rdom); + } // updates the dom's nodes let to_update = rdom.apply_mutations(mutations); // update the style and layout diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs new file mode 100644 index 000000000..9cf4ba6d2 --- /dev/null +++ b/packages/tui/src/node.rs @@ -0,0 +1,94 @@ +use crate::focus::Focus; +use crate::layout::TaffyLayout; +use crate::style_attributes::StyleModifier; +use dioxus_native_core::{real_dom::RealDom, state::*}; +use dioxus_native_core_macro::{sorted_str_slice, State}; + +pub(crate) type Dom = RealDom; +pub(crate) type Node = dioxus_native_core::real_dom::Node; + +#[derive(Debug, Clone, State, Default)] +pub(crate) struct NodeState { + #[child_dep_state(layout, RefCell)] + pub layout: TaffyLayout, + #[parent_dep_state(style)] + pub style: StyleModifier, + #[node_dep_state()] + pub prevent_default: PreventDefault, + #[node_dep_state()] + pub focus: Focus, + pub focused: bool, +} + +#[derive(PartialEq, Debug, Clone)] +pub(crate) enum PreventDefault { + Focus, + KeyPress, + KeyRelease, + KeyDown, + KeyUp, + MouseDown, + Click, + MouseEnter, + MouseLeave, + MouseOut, + Unknown, + MouseOver, + ContextMenu, + Wheel, + MouseUp, +} + +impl Default for PreventDefault { + fn default() -> Self { + PreventDefault::Unknown + } +} + +impl NodeDepState for PreventDefault { + type Ctx = (); + + type DepState = (); + + const NODE_MASK: dioxus_native_core::node_ref::NodeMask = + dioxus_native_core::node_ref::NodeMask::new_with_attrs( + dioxus_native_core::node_ref::AttributeMask::Static(&sorted_str_slice!([ + "dioxus-prevent-default" + ])), + ); + + fn reduce( + &mut self, + node: dioxus_native_core::node_ref::NodeView, + _sibling: &Self::DepState, + _ctx: &Self::Ctx, + ) -> bool { + let new = match node + .attributes() + .find(|a| a.name == "dioxus-prevent-default") + .and_then(|a| a.value.as_text()) + { + Some("onfocus") => PreventDefault::Focus, + Some("onkeypress") => PreventDefault::KeyPress, + Some("onkeyrelease") => PreventDefault::KeyRelease, + Some("onkeydown") => PreventDefault::KeyDown, + Some("onkeyup") => PreventDefault::KeyUp, + Some("onclick") => PreventDefault::Click, + Some("onmousedown") => PreventDefault::MouseDown, + Some("onmouseup") => PreventDefault::MouseUp, + Some("onmouseenter") => PreventDefault::MouseEnter, + Some("onmouseover") => PreventDefault::MouseOver, + Some("onmouseleave") => PreventDefault::MouseLeave, + Some("onmouseout") => PreventDefault::MouseOut, + Some("onwheel") => PreventDefault::Wheel, + Some("oncontextmenu") => PreventDefault::ContextMenu, + _ => return false, + }; + if new == *self { + false + } else { + *self = new; + true + } + } +} diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs index f4d72cb26..00717029f 100644 --- a/packages/tui/src/render.rs +++ b/packages/tui/src/render.rs @@ -5,7 +5,7 @@ use taffy::{ prelude::{Layout, Size}, Taffy, }; -use tui::{backend::CrosstermBackend, layout::Rect}; +use tui::{backend::CrosstermBackend, layout::Rect, style::Color}; use crate::{ style::{RinkColor, RinkStyle}, @@ -272,6 +272,10 @@ impl RinkWidget for &Node { if let Some(c) = self.state.style.core.bg { new_cell.bg = c; } + if self.state.focused { + new_cell.bg.alpha = 100; + new_cell.bg.color = new_cell.bg.blend(Color::White); + } buf.set(x, y, new_cell); } } diff --git a/packages/tui/src/widget.rs b/packages/tui/src/widget.rs index b0c96d607..416538462 100644 --- a/packages/tui/src/widget.rs +++ b/packages/tui/src/widget.rs @@ -57,7 +57,7 @@ impl WidgetWithContext { impl Widget for WidgetWithContext { fn render(self, area: Rect, buf: &mut Buffer) { - self.widget.render(area, RinkBuffer::new(buf, self.config)) + self.widget.render(area, RinkBuffer::new(buf, self.config)); } }