use std::rc::Rc;
use layout_core::NodeId;
use platform_core::{Key, ModifiersState, NamedKey, NumericValue};
use reactive_core::{RwSignal, signal};
use rustc_hash::FxHashSet;
pub type FocusId = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FocusKind {
Widget,
TextEntry,
}
pub use platform_core::Role;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FocusHandle(FocusId);
impl FocusHandle {
pub fn request(self) {
request(self.0);
}
pub fn release(self) {
release(self.0);
}
pub fn is_focused(self) -> bool {
is_focused(self.0)
}
}
pub fn handle(id: FocusId) -> FocusHandle {
FocusHandle(id)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ScopeId(u64);
struct Scope {
id: ScopeId,
node: NodeId,
showing: Rc<dyn Fn() -> bool>,
traps: bool,
reason: ScopeReason,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ScopeReason {
NotShowing,
Disabled,
}
struct Entry {
id: FocusId,
node: Option<NodeId>,
role: Role,
tabbable: bool,
toggled: Option<Rc<dyn Fn() -> bool>>,
value: Option<Rc<dyn Fn() -> NumericValue>>,
}
struct FocusState {
next_id: FocusId,
next_scope: u64,
focused: RwSignal<Option<FocusId>>,
pointer_focus: RwSignal<bool>,
order: Vec<Entry>,
scopes: Vec<Scope>,
text_entries: FxHashSet<FocusId>,
}
impl FocusState {
fn new() -> Self {
Self {
next_id: 1,
next_scope: 1,
focused: signal(None),
pointer_focus: signal(false),
order: Vec::new(),
scopes: Vec::new(),
text_entries: FxHashSet::default(),
}
}
}
reactive_core::surface_local! {
slot FOCUS: FocusState = FocusState::new();
access with_focus, with_focus_ref;
context FocusContext, FocusGuard;
}
fn focused_signal() -> RwSignal<Option<FocusId>> {
with_focus_ref(|s| s.focused.clone())
}
pub fn next_id() -> FocusId {
with_focus(|s| {
let id = s.next_id;
s.next_id += 1;
id
})
}
pub fn current() -> Option<FocusId> {
focused_signal().get()
}
pub fn is_focused(id: FocusId) -> bool {
current() == Some(id)
}
pub fn request(id: FocusId) {
set_pointer_focus(false);
let focused = focused_signal();
if focused.peek() != Some(id) {
focused.set(Some(id));
}
}
pub fn request_from_pointer(id: FocusId) {
set_pointer_focus(true);
let focused = focused_signal();
if focused.peek() != Some(id) {
focused.set(Some(id));
}
}
pub fn is_focus_visible(id: FocusId) -> bool {
is_focused(id) && !pointer_focus_signal().get()
}
fn pointer_focus_signal() -> RwSignal<bool> {
with_focus_ref(|s| s.pointer_focus.clone())
}
fn set_pointer_focus(from_pointer: bool) {
let flag = pointer_focus_signal();
if flag.peek() != from_pointer {
flag.set(from_pointer);
}
}
pub fn release(id: FocusId) {
let focused = focused_signal();
if focused.peek() == Some(id) {
focused.set(None);
}
}
pub fn clear() {
let focused = focused_signal();
if focused.peek().is_some() {
focused.set(None);
}
}
pub fn register_as(id: FocusId, kind: FocusKind) {
register_node(id, kind, None, default_role(kind), true);
}
pub fn register_at(id: FocusId, kind: FocusKind, node: NodeId) {
register_node(id, kind, Some(node), default_role(kind), true);
}
pub fn register_with_role(id: FocusId, kind: FocusKind, node: NodeId, role: Role) {
register_node(id, kind, Some(node), role, true);
}
pub fn register_presented(id: FocusId, node: NodeId, role: Role) {
register_node(id, FocusKind::Widget, Some(node), role, false);
}
fn default_role(kind: FocusKind) -> Role {
match kind {
FocusKind::Widget => Role::Button,
FocusKind::TextEntry => Role::TextInput,
}
}
fn register_node(id: FocusId, kind: FocusKind, node: Option<NodeId>, role: Role, tabbable: bool) {
with_focus(|s| {
match s.order.iter_mut().find(|e| e.id == id) {
Some(existing) => {
existing.node = existing.node.or(node);
if role != Role::default() {
existing.role = role;
}
existing.tabbable &= tabbable;
}
None => s.order.push(Entry {
id,
node,
role,
tabbable,
toggled: None,
value: None,
}),
}
if kind == FocusKind::TextEntry {
s.text_entries.insert(id);
}
});
}
pub fn register_scope(node: NodeId, showing: impl Fn() -> bool + 'static, traps: bool) -> ScopeId {
register_scope_because(node, showing, traps, ScopeReason::NotShowing)
}
pub fn register_scope_because(
node: NodeId,
showing: impl Fn() -> bool + 'static,
traps: bool,
reason: ScopeReason,
) -> ScopeId {
with_focus(|s| {
let id = ScopeId(s.next_scope);
s.next_scope += 1;
s.scopes.push(Scope {
id,
node,
showing: Rc::new(showing),
traps,
reason,
});
id
})
}
pub fn unregister_scope(id: ScopeId) {
with_focus(|s| s.scopes.retain(|scope| scope.id != id));
}
pub fn unregister(id: FocusId) {
with_focus(|s| {
s.order.retain(|e| e.id != id);
s.text_entries.remove(&id);
});
release(id);
}
pub fn text_entry_focused() -> bool {
match current() {
Some(id) => with_focus_ref(|s| s.text_entries.contains(&id)),
None => false,
}
}
pub fn text_entry_takes_key(key: &Key, modifiers: ModifiersState) -> bool {
text_entry_focused() && edits_text(key, modifiers)
}
fn edits_text(key: &Key, modifiers: ModifiersState) -> bool {
match key {
Key::Char(_) if modifiers.is_ctrl || modifiers.is_meta => false,
Key::Char(c) => !c.is_control(),
Key::Named(named) => matches!(
named,
NamedKey::Space
| NamedKey::Backspace
| NamedKey::Delete
| NamedKey::ArrowLeft
| NamedKey::ArrowRight
| NamedKey::ArrowUp
| NamedKey::ArrowDown
| NamedKey::Home
| NamedKey::End
| NamedKey::Enter
| NamedKey::Escape
| NamedKey::Tab
),
}
}
pub fn focus_next() {
step(1);
}
pub fn focus_prev() {
step(-1);
}
type ScopeView = (NodeId, Rc<dyn Fn() -> bool>, bool);
fn reachable(node: Option<NodeId>, scopes: &[ScopeView]) -> bool {
let Some(node) = node else { return true };
if layout_reactive::is_hidden(node) {
return false;
}
if scopes
.iter()
.any(|(scope, showing, _)| !showing() && layout_reactive::is_descendant_of(node, *scope))
{
return false;
}
match scopes
.iter()
.rev()
.find(|(_, showing, traps)| *traps && showing())
{
Some((scope, _, _)) => layout_reactive::is_descendant_of(node, *scope),
None => true,
}
}
fn snapshot() -> (Vec<(FocusId, Option<NodeId>)>, Vec<ScopeView>) {
with_focus_ref(|s| {
let order: Vec<(FocusId, Option<NodeId>)> = s
.order
.iter()
.filter(|e| e.tabbable)
.map(|e| (e.id, e.node))
.collect();
let scopes: Vec<ScopeView> = s
.scopes
.iter()
.map(|sc| (sc.node, sc.showing.clone(), sc.traps))
.collect();
(order, scopes)
})
}
pub fn set_toggled(id: FocusId, state: impl Fn() -> bool + 'static) {
let state: Rc<dyn Fn() -> bool> = Rc::new(state);
with_focus(|s| {
if let Some(entry) = s.order.iter_mut().find(|e| e.id == id) {
entry.toggled = Some(state);
}
});
}
pub fn set_value(id: FocusId, read: impl Fn() -> NumericValue + 'static) {
let read: Rc<dyn Fn() -> NumericValue> = Rc::new(read);
with_focus(|s| {
if let Some(entry) = s.order.iter_mut().find(|e| e.id == id) {
entry.value = Some(read);
}
});
}
pub struct Exposed {
pub id: FocusId,
pub node: NodeId,
pub role: Role,
pub enabled: bool,
pub toggled: Option<bool>,
pub value: Option<NumericValue>,
}
pub fn exposed() -> Vec<Exposed> {
let (order, scopes) = with_focus_ref(|s| {
type Row = (
FocusId,
Option<NodeId>,
Role,
Option<Rc<dyn Fn() -> bool>>,
Option<Rc<dyn Fn() -> NumericValue>>,
);
let order: Vec<Row> = s
.order
.iter()
.map(|e| (e.id, e.node, e.role, e.toggled.clone(), e.value.clone()))
.collect();
let scopes: Vec<(NodeId, Rc<dyn Fn() -> bool>, bool, ScopeReason)> = s
.scopes
.iter()
.map(|sc| (sc.node, sc.showing.clone(), sc.traps, sc.reason))
.collect();
(order, scopes)
});
let hiding: Vec<ScopeView> = scopes
.iter()
.filter(|(_, _, _, reason)| *reason == ScopeReason::NotShowing)
.map(|(node, showing, traps, _)| (*node, showing.clone(), *traps))
.collect();
order
.into_iter()
.filter_map(|(id, node, role, toggled, value)| {
let node = node?;
reachable(Some(node), &hiding).then(|| Exposed {
id,
node,
role,
enabled: !scopes.iter().any(|(scope, showing, _, reason)| {
*reason == ScopeReason::Disabled
&& !showing()
&& layout_reactive::is_descendant_of(node, *scope)
}),
toggled: toggled.as_ref().map(|read| read()),
value: value.as_ref().map(|read| read()),
})
})
.collect()
}
pub fn focus_first_in(node: NodeId) -> bool {
let (order, scopes) = snapshot();
let found = order.into_iter().find(|(_, widget)| {
widget.is_some_and(|widget| layout_reactive::is_descendant_of(widget, node))
&& reachable(*widget, &scopes)
});
match found {
Some((id, _)) => {
request(id);
true
}
None => false,
}
}
pub fn is_registered(id: FocusId) -> bool {
with_focus_ref(|s| s.order.iter().any(|e| e.id == id))
}
fn step(dir: isize) {
let (order, scopes) = snapshot();
let order: Vec<FocusId> = order
.into_iter()
.filter(|(_, node)| reachable(*node, &scopes))
.map(|(id, _)| id)
.collect();
if order.is_empty() {
return;
}
let n = order.len() as isize;
let next = match current().and_then(|c| order.iter().position(|&x| x == c)) {
Some(i) => order[((i as isize + dir).rem_euclid(n)) as usize],
None => {
if dir > 0 {
order[0]
} else {
order[order.len() - 1]
}
}
};
request(next);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_release_and_ids_are_unique() {
clear();
let a = next_id();
let b = next_id();
assert_ne!(a, b, "ids must be unique");
assert!(!is_focused(a));
request(a);
assert!(is_focused(a) && current() == Some(a));
request(b);
assert!(is_focused(b) && !is_focused(a));
release(a);
assert!(is_focused(b));
release(b);
assert!(current().is_none());
}
#[test]
fn tab_skips_a_disabled_box() {
use crate::context::{compute_layout, reset_layout_runtime};
use crate::{LayoutItem, StyledContainer};
use layout_core::{AvailableSpace, LayoutStyle};
reset_layout_runtime();
let base = next_id();
register_as(base, FocusKind::Widget);
let below = next_id();
let off = StyledContainer::new(
LayoutStyle::new().width(50.0).height(20.0),
|_r| renderer_core::RectStyle::default(),
vec![],
)
.unwrap()
.on_focus(|_| {})
.disabled(|| true);
let above = next_id();
compute_layout(
off.layout_node(),
AvailableSpace::Definite(50.0),
AvailableSpace::Definite(20.0),
)
.unwrap();
request(base);
focus_next();
let landed = current().expect("something took focus");
assert!(
!(landed > below && landed < above),
"Tab landed on a box the application had disabled"
);
}
#[test]
fn tab_skips_a_focusable_taken_out_of_layout_flow() {
use crate::context::{compute_layout, reset_layout_runtime, set_display};
use crate::{LayoutItem, StyledContainer};
use layout_core::{AvailableSpace, LayoutStyle};
reset_layout_runtime();
let base = next_id();
register_as(base, FocusKind::Widget);
let below = next_id();
let hidden = StyledContainer::new(
LayoutStyle::new().width(50.0).height(20.0),
|_r| renderer_core::RectStyle::default(),
vec![],
)
.unwrap()
.on_focus(|_| {});
let node = hidden.layout_node();
let root = StyledContainer::new(
LayoutStyle::new().width(100.0).height(100.0),
|_r| renderer_core::RectStyle::default(),
vec![Box::new(hidden)],
)
.unwrap();
let above = next_id();
set_display(node, false);
compute_layout(
root.layout_node(),
AvailableSpace::Definite(100.0),
AvailableSpace::Definite(100.0),
)
.unwrap();
request(base);
focus_next();
let landed = current().expect("something took focus");
assert!(
!(landed > below && landed < above),
"Tab landed on a focusable that is out of layout flow"
);
}
#[test]
fn tab_order_steps_forward_and_back() {
let (a, b, c) = (next_id(), next_id(), next_id());
register_as(a, FocusKind::Widget);
register_as(b, FocusKind::Widget);
register_as(c, FocusKind::Widget);
request(a);
focus_next();
assert_eq!(current(), Some(b));
focus_next();
assert_eq!(current(), Some(c));
focus_prev();
assert_eq!(current(), Some(b));
unregister(b);
assert!(current().is_none());
unregister(a);
unregister(c);
}
}