소스 검색

add focus state

Evan Almloff 3 년 전
부모
커밋
ce5ade9fa6
2개의 변경된 파일279개의 추가작업 그리고 35개의 파일을 삭제
  1. 268 0
      packages/tui/src/focus.rs
  2. 11 35
      packages/tui/src/lib.rs

+ 268 - 0
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<std::cmp::Ordering> {
+        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::<i32>() {
+                    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<ElementId>,
+    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<ElementId>],
+            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),
+                ),
+                _ => (),
+            }
+        }
+    }
+}

+ 11 - 35
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<usize> = 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);