Focus Mechanism

Flor's focus is an explicit mechanism. Views won't automatically enter the focus system just because they can be clicked, can display text, or bound keyboard events. A view only enters the focus table when explicitly registered, then it will be accessed by Tab, will become the dispatch target for view-level on_key_*, and will have on_focus and on_blur triggered by the focus manager.

First look at a minimal example:

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let name = label("Name")
    // Add to focus table. 0 is sort value, means this is first accessed focus item in current scope.
    .focus_index(0)
    .on_focus(|view_id, virtual_index| {
        println!("focus {view_id}, virtual index: {virtual_index}");
    });

Views without focus_index won't enter the focus table. They won't be selected by Tab; when clicking them, the framework can't set focus to that view; their view-level on_focus / on_blur won't be triggered by the focus manager; their view-level on_key_* won't be triggered because "this view got focus".

For specific builder writing see Focus Builder. For API to manipulate focus at runtime through ViewId see ViewId.

Keyboard Events Depend on Focus

Keyboard events are dispatched to the currently focused view. To let a view handle view-level keyboard events, it needs to enter the focus table and become the current focus.

use flor::base::platform::{HandleResult, KeyCode};
use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let editor = label("Press Ctrl+S to save")
    .focus_index(0)
    .on_key_down(|_, code, is_alt, is_ctrl, is_shift| {
        if !is_alt && is_ctrl && !is_shift && code == KeyCode::S {
            println!("save");
            HandleResult::Handled
        } else {
            HandleResult::Default
        }
    });

When there's no current focus view, key_down / key_up will go through window-level processing; a normal view that didn't get focus won't receive its own view-level on_key_*. Text input and IME input also depend on the current focus view.

Click and Focus

Mouse events go through hit test, not the same dispatch path as the focus table. So views without focus_index can still receive on_click, on_button_down, on_mouse_move and similar mouse events.

When click ends, the framework will try to set focus to the clicked view. This action is still limited by the focus table: if the target view doesn't have focus_index, the focus manager can't find a corresponding entry, and the view won't become the current focus.

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let item = label("Get focus after click")
    .focus_index(0)
    .on_click(|| {
        println!("clicked");
    });

Virtual Focus

Entries in focus table are not just ViewId, but:

(focus_index, ViewId, virtual_index)

Most views only have one virtual focus point, number is 0. So on_focus and on_blur's second parameter common value is 0.

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor_lys::label::label;

let view = label("Single focus view")
    .focus_index(0)
    .on_focus(|_, virtual_index| {
        assert_eq!(virtual_index, 0);
    });

Compound views can expose multiple virtual focus points. For example, an editor view can design text area, completion panel, line number area as different focus positions within the same view. Terminal user only needs to read virtual_index, doesn't need to apply for virtual focus count; applying for count belongs to the view developer's View::on_focus_count capability.

Sort Segmentation

Complex pages often need to split focus order into several areas. Flor provides focus_scope(u32) as sort offset: it will affect final sort value of focus_index in the current view subtree.

use flor::view::builder::FocusIndexBuilder;
use flor::views;
use flor_lys::div::div;
use flor_lys::label::label;

let toolbar = div(views![
    label("Tool 1").focus_index(0),
    label("Tool 2").focus_index(1),
])
.focus_scope(100);

let content = div(views![
    label("Content 1").focus_index(0),
    label("Content 2").focus_index(1),
])
.focus_scope(200);

Final sort values for this set of focus are:

ViewLocal WritingFinal Sort Value
Tool 1100 + 0100
Tool 2100 + 1101
Content 1200 + 0200
Content 2200 + 1201

focus_scope(u32) is sort segmentation, not focus isolation in popup. It won't let Tab only stay in this area.

Runtime Scope

Modal, Popup, Sidebar and similar interfaces need another capability: when opened, let Tab only cycle inside popup layer; after closed, restore previous focus position.

Flor uses runtime focus scope to handle this scenario. When popup layer opens, push popup layer root view into focus scope; when closing, pop this scope.

use flor::view::builder::{EventBuilder, FocusIndexBuilder};
use flor::views;
use flor_lys::div::div;
use flor_lys::label::label;

let dialog = div(views![
    label("Name").focus_index(0),
    label("Confirm").focus_index(1),
])
.on_create(|dialog_root_id| {
    dialog_root_id.push_focus_scope();
});

When closing popup layer call pop_focus_scope. For complete ViewId method description see ViewId.

Runtime scope only filters views already in focus table. If no view inside popup layer sets focus_index, Tab has no target in this scope.

Mechanism Explanation

When window creates, Flor initializes focus table:

  1. Traverse view tree starting from window root node.
  2. When encountering focus_scope(u32), add it to current cumulative offset.
  3. When encountering focus_index(u32), use "cumulative offset + local index" to generate sort value.
  4. Query this view's virtual focus count, default is 1.
  5. Generate one (focus_index, ViewId, virtual_index) for each virtual focus.
  6. Sort all entries and hand to FocusManager.

After Tab key enters event bus, FocusManager::next() will move backward in current focus table; Shift+Tab will call prev() to move forward. Each focus switch, old entry triggers blur, new entry triggers focus.

If there's runtime focus scope, FocusManager will first filter focus table to subtree of stack top scope root view, then execute next/prev.

Keyboard events query current focus's ViewId, then dispatch to this view. When no current focus, view-level keyboard handler won't be called.