// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use crate::types::Point; use crate::widget::{ LaidOutWidget, WidgetHandlers }; /// Hit test `pos` against the laid-out focusable widgets. Returns the /// `flat_idx` of the topmost widget under the point, or `None` if the point /// hits nothing focusable. Topmost-wins because layout pushes parents before /// children — the reverse iteration order makes the visually-on-top widget /// (drawn last) hit-tested first. pub fn find_widget_at( widget_rects: &[ LaidOutWidget ], pos: Point, ) -> Option { for w in widget_rects.iter().rev() { if w.rect.contains( pos ) { return Some( w.flat_idx ); } } None } /// O(N) lookup of a single widget by `flat_idx`. N is the number of /// *focusable leaves*, not the size of the [`crate::widget::Element`] tree. /// If the hot path ever needs O(1), swap the per-surface `widget_rects` /// slice for a `Vec` + a `HashMap` index. pub fn find_widget<'a, Msg: Clone>( widget_rects: &'a [ LaidOutWidget ], flat_idx: usize, ) -> Option<&'a LaidOutWidget> { widget_rects.iter().find( |w| w.flat_idx == flat_idx ) } /// Convenience wrapper around [`find_widget`] that returns just the handler /// snapshot — what most input dispatch sites actually want. pub fn find_handlers<'a, Msg: Clone>( widget_rects: &'a [ LaidOutWidget ], flat_idx: usize, ) -> Option<&'a WidgetHandlers> { find_widget( widget_rects, flat_idx ).map( |w| &w.handlers ) } /// Compute the next focusable widget for Tab / Shift+Tab navigation. /// Returns the `flat_idx` of the next keyboard-focusable entry after the one /// matching `current` (or wrapping around). `reverse` flips the direction /// (Shift+Tab). Returns `None` when no entry has /// [`LaidOutWidget::keyboard_focusable`] set — typical for a surface that only /// hosts hit-testable chrome (e.g. a row of `WindowButton`s). /// /// When `current` is `None` (no widget currently focused) the result is the /// first or last keyboard-focusable widget depending on direction — matching /// how desktop toolkits behave when Tab is pressed in an unfocused window. /// /// Non-focusable interactive entries (chrome such as `WindowButton`) live in /// the same `widget_rects` slice so pointer/touch hit testing finds them, but /// are skipped here so they don't steal Tab focus from window content. pub fn next_focusable_index( widget_rects: &[ LaidOutWidget ], current: Option, reverse: bool, ) -> Option { // Project the slice down to just the keyboard-focusable entries. Tab // cycles over this projection only — pointer-only chrome sits in // `widget_rects` for hit testing but never receives keyboard focus. let focusables: Vec = widget_rects .iter() .filter( |w| w.keyboard_focusable ) .map( |w| w.flat_idx ) .collect(); let n = focusables.len(); if n == 0 { return None; } let current_pos = current.and_then( |fi| focusables.iter().position( |&i| i == fi ) ); let next = if reverse { current_pos .map( |pos| focusables[ ( pos + n - 1 ) % n ] ) .unwrap_or( focusables[ n - 1 ] ) } else { current_pos .map( |pos| focusables[ ( pos + 1 ) % n ] ) .unwrap_or( focusables[ 0 ] ) }; Some( next ) }