From ce5ade9fa6a584178256a4b4d05e33adf85c229e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 15:06:07 -0500 Subject: [PATCH] add focus state --- packages/tui/src/focus.rs | 268 ++++++++++++++++++++++++++++++++++++++ packages/tui/src/lib.rs | 46 ++----- 2 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 packages/tui/src/focus.rs diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs new file mode 100644 index 000000000..cc89e9660 --- /dev/null +++ b/packages/tui/src/focus.rs @@ -0,0 +1,268 @@ +use crate::Dom; + +use dioxus_core::ElementId; +use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; + +use std::num::NonZeroU16; + +use dioxus_native_core::{ + node_ref::{AttributeMask, NodeMask, NodeView}, + real_dom::NodeType, + state::NodeDepState, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Ord)] +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 Default for FocusLevel { + fn default() -> Self { + FocusLevel::Unfocusable + } +} + +#[derive(Clone, PartialEq, Debug, Default)] +pub(crate) struct Focus { + pub pass_focus: bool, + 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 { + pass_focus: !node + .attributes() + .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "true"), + level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { + if let Ok(index) = a.value.parse::() { + if index < 0 { + FocusLevel::Unfocusable + } else if index == 0 { + FocusLevel::Focusable + } else { + 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 + } + } +} + +// must be sorted +const FOCUS_EVENTS: &[&str] = &["keydown", "keypress", "keyup"]; +const FOCUS_ATTRIBUTES: &[&str] = &["dioxus-prevent-default", "tabindex"]; + +#[derive(Default)] +pub(crate) struct FocusState { + pub(crate) focus_iter: PersistantElementIter, + pub(crate) last_focused_id: Option, + pub(crate) focus_level: FocusLevel, +} + +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.focus.pass_focus { + return false; + } + } + let mut loop_marker_id = self.last_focused_id; + let focus_level = &mut self.focus_level; + let mut next_focus = None; + let starting_focus_level = *focus_level; + + loop { + let new = if forward { + self.focus_iter.next(&rdom) + } else { + self.focus_iter.prev(&rdom) + }; + let new_id = new.id(); + let current_level = rdom[new_id].state.focus.level; + 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 current_level = n.state.focus.level; + if current_level != *focus_level { + if current_level > *focus_level { + if let Some(level) = &mut closest_level { + if current_level < *level { + *level = current_level; + } + } else { + closest_level = Some(current_level); + } + } + } + }); + } else { + // find the closest focusable element before the current level + rdom.traverse_depth_first(|n| { + let current_level = n.state.focus.level; + if current_level != *focus_level { + if current_level < *focus_level { + if let Some(level) = &mut closest_level { + if current_level > *level { + *level = current_level; + } + } else { + closest_level = Some(current_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; + } + } + + // if the focus level looped, we are done + if *focus_level == starting_focus_level { + break; + } + } + + // 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 after_previous_focused = if forward { + current_level >= *focus_level + } else { + current_level <= *focus_level + }; + if after_previous_focused && current_level.focusable() { + if current_level == *focus_level { + next_focus = Some((new_id, current_level)); + break; + } + } + } + + if let Some((id, order)) = next_focus { + if order.focusable() { + 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 if forward { + self.focus_iter.next(&rdom).id() + } else { + self.focus_iter.prev(&rdom).id() + } != id + {} + 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); + } + } + } + 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), + ), + _ => (), + } + } + } +} diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 033eb1d8d..d8dec50e9 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,10 +7,11 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use dioxus_native_core::real_dom::{NodeType, RealDom}; +use dioxus_native_core::real_dom::RealDom; use dioxus_native_core::state::*; -use dioxus_native_core::utils::PersistantElementIter; use dioxus_native_core_macro::State; +use focus::Focus; +use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, pin_mut, StreamExt, @@ -24,6 +25,7 @@ use style_attributes::StyleModifier; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod focus; mod hooks; mod layout; mod render; @@ -44,6 +46,8 @@ struct NodeState { // depends on attributes, the C component of it's parent and a u8 context #[parent_dep_state(style)] style: StyleModifier, + #[node_dep_state()] + focus: Focus, focused: bool, } @@ -135,8 +139,7 @@ fn render_vdom( let to_rerender: fxhash::FxHashSet = vec![0].into_iter().collect(); let mut updated = true; - let mut focus_iter = PersistantElementIter::new(); - let mut focus_id = ElementId(0); + let mut focus_state = FocusState::default(); loop { /* @@ -210,38 +213,11 @@ fn render_vdom( break; } if let KeyCode::BackTab = key.code { - let mut new_focused_id; - loop { - new_focused_id = focus_iter.prev(&rdom).id(); - if let NodeType::Element { .. } = - &rdom[new_focused_id].node_type - { - break; - } - } - rdom[focus_id].state.focused = false; - focus_id = new_focused_id; - rdom[focus_id].state.focused = true; - evt_intersepted = true; - updated = true; - // println!("{:?}", focus_id); + evt_intersepted = + focus_state.progress(&mut rdom, false); } if let KeyCode::Tab = key.code { - let mut new_focused_id; - loop { - new_focused_id = focus_iter.next(&rdom).id(); - if let NodeType::Element { .. } = - &rdom[new_focused_id].node_type - { - break; - } - } - rdom[focus_id].state.focused = false; - focus_id = new_focused_id; - rdom[focus_id].state.focused = true; - evt_intersepted = true; - updated = true; - // println!("{:?}", focus_id); + evt_intersepted = focus_state.progress(&mut rdom, true); } } TermEvent::Resize(_, _) => updated = true, @@ -266,7 +242,7 @@ fn render_vdom( } let mutations = vdom.work_with_deadline(|| false); for m in &mutations { - focus_iter.prune(m, &rdom); + focus_state.prune(m, &rdom); } // updates the dom's nodes let to_update = rdom.apply_mutations(mutations);