Skip to content

Implement keyboard focus system #3

@philippeitis

Description

@philippeitis

I'm taking most of my inspiration from this document: https://docs.flutter.dev/development/ui/advanced/focus

Essentially, they describe a system of focus handling as follows:

  • Focus Tree: Tracks focus nodes and their relationships
  • Focus Node: A single node in the focus tree which can receive focus
  • Focus Chain: the focused nodes, starting from the root and going to the primary focus node
  • Primary Focus Node:
    • The deepest focused node
    • Events go first to this node, and propagate up through the focus chain
  • Focus Traversal:
    • the process of changing focus nodes on TAB
    • Focus Scope: a scope that groups nodes for traversal

To implement this functionality, two key requirements stand out:

  1. We need to build a focus tree. This can be done in two ways:
  • Use a hook-based system that detects the creation of focus nodes, and adds them to hidden global state. By using callbacks on drop, we can detect different of the tree
  • Add a function to the widget trait which returns Option<FocusTree> - and recursively builds the focus tree
  1. We need widgets to be able to bubble input upwards
  • We can track the current focus node, and drill down to it
  • Or, we can start from the current focus node, and bubble up to the parents - this requires passing a reference to the focus node parent to it's child

We can either use Rc / Weak style references, or track IDs which correspond to each focus node:

struct FocusNodeId(i64);
// Needs to be regenerated on each render call
static FOCUS_TREE: Mutex<BTreeMap<FocusNodeId, Vec<FocusNodeId>>> = Mutex::new(...);
static FOCUS_NODES: Mutex<HashMap<FocusNodeId, FocusNode>> = Mutex::new(...);

I think that tracking IDs is the best/simplest solution, though it requires some consideration W.R.T. concurrency.

Focus hook system:

mod focus_hook {
    static ROOT: Mutex<Option<Vec<FocusNode>>> = Mutex::new(None);

    /// Returns the parent of the current focus node;
    fn parent() -> Weak<FocusNode>;

    /// Add new focus node as child
    fn push_child(FocusNode);

    /// Move up one level in the tree. Returns all focus nodes that would be children
    /// `push_child` will now add to the parent of the previously pushed focus node
    fn bubble() -> Option<Vec<Rc<FocusNode>>>;

     /// Assign focus to the current node
     fn grab_focus(fn: &mut FocusNode);
}

struct FocusNode {
    inner: Option<Widget>,
    parent: Option<Weak<FocusNode>>,
    focus_children: Option<Vec<Rc<FocusNode>>>
}

impl FocusNode {
    fn new(component: _) -> _ {
         let mut self_ = Rc::new(FocusNode { parent: None, inner: None, focus_children: None });
         self_.parent = Some(focus_hook::parent());
         focus_hook::push_child(self_.clone());
         /// Render call will populate children
         self_.inner = component.render();
         self_.focus_children = focus_hook::bubble();
         self_
    }
}

Trait based focus tree:

pub trait Element {
  fn draw(&self, _rect: Rect, _frame: &mut Frame) {}
  fn on_key(&self, _event: KeyEvent) {}
  fn on_mouse(&self, _rect: Rect, _event: MouseEvent) {}
  fn children(&self) -> Vec<Box<dyn Element>>;
  fn focus_tree(&self) -> Vec<Rc<FocusNode>> {
    // Produce list of focus trees for each child
    // Place self at top of focus tree, with child focus trees as children
    // eg.
    //         f0
    //         / \
    //        o  f1 _
    //       / \     \
    //      o  f2    f3
    //     / \   \   |  \
    //    f4 f5  f6  o  f7
    // Focus tree:
    //       _ f0 __
    //      /  / \  \
    //     /   /  \  \
    //    f4  f5  f2 f1
    //             |  |
    //            f6 f3
    //                |
    //               f7
    let mut focus_trees = vec![];
    for child in self.children() {
        focus_trees.extend(child.focus_tree());
    }
    if let Some(s) = self.as_focus_node() {
        vec![{ s.focus_children = Some(focus_trees); s}])
    } else {
        focus_trees
    }
  }

  fn as_focus_node(&self) -> Option<Rc<FocusNode>> { None }
}

struct FocusNode {
    inner: Option<Widget>,
    parent: Option<Weak<FocusNode>>,
    focus_children: Option<Vec<Rc<FocusNode>>>
}

impl FocusNode {
    fn new(component: _) -> _ {
         let mut self_ = Rc::new(FocusNode { parent: None, inner: None, focus_children: None });
         self_.parent = Some(focus_hook::parent());
         focus_hook::push_child(self_.clone());
         /// Render call will populate children
         self_.inner = component.render();
         self_.focus_children = self.focus_tree();
         self_
    }
}

Bubbling approach to handling key events:

mod focus {
    static ACTIVE_FOCUS_NODE: Mutex<Option<Rc<FocusNode>>>> = Mutex::new(None);
    fn take_focus(focus_node: &FocusNode);
    
    fn handle_key(event: KeyEvent) {
       if let Some(node) = ACTIVE_FOCUS_NODE.lock().unwrap() {
           node.on_key(event);
       }
    }
}

impl Element for FocusNode {
    fn on_key(&self, event: KeyEvent) {
        if self.inner.on_key(event) == Propagate::Next {
            self.parent.unwrap().on_key(event);
        }
    }
}

The snippets I've outlined allow describing the focus tree (explicit) / focus chain (implicit), active focus node (explicit).

The focus scope is relatively easy to implement, as a plain focus node.
Focus traversal could be done using the focus tree and visiting parents in order of increasing ID - again, certain implementation details would need to be figured out, but this should be doable.

Again - I'd be happy to implement this, but I'd like to get an idea for what overall design you think is best before I go ahead and implement this functionality. I should be able to implement an MVP relatively quickly - likely under an experimental_components::focus module.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions