///|
/// Focus Management for Tab/Shift+Tab Navigation
/// Implements standard HTML focus navigation order
// =============================================================================
// Types
// =============================================================================
///|
/// Represents a focusable item in the tab order
pub(all) struct FocusableItem {
// Reference to the accessibility node
node : AccessibilityNode
// Document order index (for sorting)
document_order : Int
// Effective tabindex for sorting (0 for inherently focusable, actual value otherwise)
effective_tabindex : Int
}
///|
/// Focus manager for handling Tab/Shift+Tab navigation
pub(all) struct FocusManager {
// The accessibility tree
tree : AccessibilityTree
// Computed tab order (cached)
tab_order : Array[FocusableItem]
// Current focus index in tab_order (-1 if no focus)
mut current_index : Int
}
// =============================================================================
// Tab Order Computation
// =============================================================================
///|
/// Minimum size in pixels for an element to be considered tabbable
/// Elements smaller than this are likely invisible or too small to interact with
let min_focusable_size : Double = 1.0
///|
/// Check if bounds are valid for focus (not zero-sized or too small)
fn is_valid_bounds_for_focus(bounds : Bounds?) -> Bool {
match bounds {
Some(b) => b.width >= min_focusable_size && b.height >= min_focusable_size
// If no bounds info, allow focus (conservative approach)
None => true
}
}
///|
/// Compute the tab order for an accessibility tree
/// Returns focusable items sorted by tab order:
/// 1. Elements with tabindex > 0, sorted by tabindex value (ascending)
/// 2. Elements with tabindex = 0 or implicit focusable, in document order
/// 3. Elements with tabindex < 0 are excluded (not tabbable)
/// 4. Elements with zero-size or missing bounds are excluded
pub fn compute_tab_order(tree : AccessibilityTree) -> Array[FocusableItem] {
let items : Array[FocusableItem] = []
// Collect all focusable items in document order
let _ = collect_focusable_items(tree.root, items, 0)
// Filter out items with tabindex < 0 (not tabbable) or invalid bounds
let tabbable : Array[FocusableItem] = []
for item in items {
// tabindex < 0 means not tabbable (programmatic focus only)
if item.effective_tabindex >= 0 {
// Also check if element has valid bounds (not zero-sized)
if is_valid_bounds_for_focus(item.node.bounds) {
tabbable.push(item)
}
}
}
// Sort by tab order:
// 1. Items with tabindex > 0 come first, sorted by tabindex value
// 2. Items with tabindex = 0 (or implicit) come after, in document order
sort_tab_order(tabbable)
tabbable
}
///|
/// Collect focusable items recursively in document order
fn collect_focusable_items(
node : AccessibilityNode,
items : Array[FocusableItem],
doc_order : Int,
) -> Int {
let mut current_order = doc_order
// Check if this node is focusable
if node.focusable {
// Determine effective tabindex
let effective_tabindex = match node.tabindex {
Some(idx) => idx
// No explicit tabindex but focusable = implicit tabindex 0
None => 0
}
items.push({ node, document_order: current_order, effective_tabindex })
current_order += 1
}
// Recurse into children
for child in node.children {
current_order = collect_focusable_items(child, items, current_order)
}
current_order
}
///|
/// Sort items by tab order (stable sort)
/// Items with tabindex > 0 come first (sorted by value)
/// Items with tabindex = 0 come after (in document order)
fn sort_tab_order(items : Array[FocusableItem]) -> Unit {
// Simple insertion sort (stable)
for i = 1; i < items.length(); i = i + 1 {
let current = items[i]
let mut j = i - 1
while j >= 0 && should_come_after(items[j], current) {
items[j + 1] = items[j]
j -= 1
}
items[j + 1] = current
}
}
///|
/// Compare function for tab order
/// Returns true if 'a' should come after 'b' in tab order
fn should_come_after(a : FocusableItem, b : FocusableItem) -> Bool {
let a_positive = a.effective_tabindex > 0
let b_positive = b.effective_tabindex > 0
if a_positive && b_positive {
// Both positive: lower tabindex comes first
// If equal, earlier document order comes first
if a.effective_tabindex == b.effective_tabindex {
a.document_order > b.document_order
} else {
a.effective_tabindex > b.effective_tabindex
}
} else if a_positive && !b_positive {
// Positive tabindex comes before zero tabindex
false
} else if !a_positive && b_positive {
// Zero tabindex comes after positive tabindex
true
} else {
// Both zero: document order
a.document_order > b.document_order
}
}
// =============================================================================
// Focus Manager
// =============================================================================
///|
/// Create a new FocusManager for an accessibility tree
pub fn FocusManager::new(tree : AccessibilityTree) -> FocusManager {
let tab_order = compute_tab_order(tree)
{ tree, tab_order, current_index: -1 }
}
///|
/// Get the currently focused node, if any
pub fn FocusManager::current(self : FocusManager) -> AccessibilityNode? {
if self.current_index >= 0 && self.current_index < self.tab_order.length() {
Some(self.tab_order[self.current_index].node)
} else {
None
}
}
///|
/// Get the currently focused node id, if any
pub fn FocusManager::current_id(self : FocusManager) -> String? {
match self.current() {
Some(node) => Some(node.id)
None => None
}
}
///|
/// Move focus to the next focusable element (Tab)
/// Returns the newly focused node, or None if no focusable elements
pub fn FocusManager::focus_next(self : FocusManager) -> AccessibilityNode? {
if self.tab_order.is_empty() {
return None
}
// Move to next, wrap around
self.current_index = (self.current_index + 1) % self.tab_order.length()
Some(self.tab_order[self.current_index].node)
}
///|
/// Move focus to the previous focusable element (Shift+Tab)
/// Returns the newly focused node, or None if no focusable elements
pub fn FocusManager::focus_prev(self : FocusManager) -> AccessibilityNode? {
if self.tab_order.is_empty() {
return None
}
// Move to previous, wrap around
if self.current_index <= 0 {
self.current_index = self.tab_order.length() - 1
} else {
self.current_index -= 1
}
Some(self.tab_order[self.current_index].node)
}
///|
/// Focus a specific node by id
/// Returns true if focus was set, false if node not found or not focusable
pub fn FocusManager::focus_by_id(self : FocusManager, id : String) -> Bool {
for i, item in self.tab_order {
if item.node.id == id {
self.current_index = i
return true
}
}
false
}
///|
/// Focus a specific node by source element id
/// Returns true if focus was set, false if not found
pub fn FocusManager::focus_by_source_id(
self : FocusManager,
source_id : String,
) -> Bool {
for i, item in self.tab_order {
match item.node.source_id {
Some(sid) if sid == source_id => {
self.current_index = i
return true
}
_ => continue
}
}
false
}
///|
/// Clear focus
pub fn FocusManager::blur(self : FocusManager) -> Unit {
self.current_index = -1
}
///|
/// Check if there is currently a focused element
pub fn FocusManager::has_focus(self : FocusManager) -> Bool {
self.current_index >= 0 && self.current_index < self.tab_order.length()
}
///|
/// Get the number of focusable elements
pub fn FocusManager::focusable_count(self : FocusManager) -> Int {
self.tab_order.length()
}
///|
/// Get all focusable items in tab order
pub fn FocusManager::get_tab_order(self : FocusManager) -> Array[FocusableItem] {
self.tab_order
}
///|
/// Reset focus to the first focusable element
/// Returns the focused node, or None if no focusable elements
pub fn FocusManager::focus_first(self : FocusManager) -> AccessibilityNode? {
if self.tab_order.is_empty() {
self.current_index = -1
return None
}
self.current_index = 0
Some(self.tab_order[0].node)
}
///|
/// Focus the last focusable element
/// Returns the focused node, or None if no focusable elements
pub fn FocusManager::focus_last(self : FocusManager) -> AccessibilityNode? {
if self.tab_order.is_empty() {
self.current_index = -1
return None
}
self.current_index = self.tab_order.length() - 1
Some(self.tab_order[self.current_index].node)
}
// =============================================================================
// Convenience Functions
// =============================================================================
///|
/// Get tab order node IDs as a simple array of strings
/// Useful for debugging and testing
pub fn get_tab_order_ids(tree : AccessibilityTree) -> Array[String] {
let order = compute_tab_order(tree)
let ids : Array[String] = []
for item in order {
ids.push(item.node.id)
}
ids
}
///|
/// Get tab order with names for debugging
pub fn get_tab_order_info(
tree : AccessibilityTree,
) -> Array[(String, String?, Role)] {
let order = compute_tab_order(tree)
let info : Array[(String, String?, Role)] = []
for item in order {
info.push((item.node.id, item.node.name, item.node.role))
}
info
}