enricozb / intuitive

A library for building declarative text-based user interfaces
213 stars 3 forks source link

Implement keyboard focus system #3

Open philippeitis opened 2 years ago

philippeitis commented 2 years ago

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:

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
  2. 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.

enricozb commented 2 years ago

Thank you for the thorough post. I'm actually rewriting a few things right now that might make focusing easier to do than it currently is. Specifically I'm rewriting the hooks system, and how reactive rendering works. Right now any state change will trigger a full re-render, even if component properties haven't changed. I'm trying to make it such that only the component whose state has changed is re-rendered. It's probably going to take me at least a few weeks, and so I probably won't be able to go through your post in detail until then.

philippeitis commented 2 years ago

Sure, that's fine with me. To support partial re-rendering, it seems like you'd need to be able to pass messages from the modified widget to the containing widget and neighboring widgets, since changes to the widget content might affect the layout for flex widgets.

If you implement support for child elements sending messages to parent widgets, that should be a big part of what the focus system needs to work, since it would allow a child widget to send key-events (and focus-switching events) to the parent widget.