use smallvec::SmallVec;
use crate::app_state::with_app_state;
use crate::jobs::{JobRequest, request_job};
use crate::reactive::global::GlobalSignal;
use crate::reactive::signal::RwSignal;
use crate::tree::{Tree, WidgetId};
use crate::widget_ref::Attachment;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FocusPath {
chain: SmallVec<[WidgetId; 8]>,
by_keyboard: bool,
}
impl FocusPath {
fn of(tree: &Tree, id: WidgetId, by_keyboard: bool) -> Self {
Self {
chain: tree.ancestors(id).collect(),
by_keyboard,
}
}
pub fn by_keyboard(&self) -> bool {
self.by_keyboard
}
pub fn widget(&self) -> Option<WidgetId> {
self.chain.first().copied()
}
pub fn contains(&self, id: WidgetId) -> bool {
self.chain.contains(&id)
}
pub(crate) fn root(&self) -> Option<WidgetId> {
self.chain.last().copied()
}
pub(crate) fn chain(&self) -> &[WidgetId] {
&self.chain
}
pub fn is_empty(&self) -> bool {
self.chain.is_empty()
}
}
static FOCUS: GlobalSignal<FocusPath> = GlobalSignal::new(FocusPath::default);
fn focus() -> RwSignal<FocusPath> {
FOCUS.get()
}
pub(crate) fn focus_path_untracked() -> FocusPath {
focus().get_untracked()
}
pub fn focus_path() -> FocusPath {
focus().get()
}
pub fn request_focus(tree: &Tree, id: WidgetId) {
move_focus(tree, id, false);
}
pub(crate) fn request_focus_by_keyboard(tree: &Tree, id: WidgetId) {
move_focus(tree, id, true);
}
fn move_focus(tree: &Tree, id: WidgetId, by_keyboard: bool) {
if super::invalidation::is_detached(id) {
return;
}
let old = focus().get_untracked();
if old.widget() == Some(id) && old.by_keyboard == by_keyboard {
return;
}
if let Some(old_id) = old.widget() {
request_job(old_id, JobRequest::Paint);
}
let new = FocusPath::of(tree, id, by_keyboard);
if old.root() != new.root() {
with_app_state(|app| {
let mut kept = app.kept_focus.borrow_mut();
if let (Some(root), Some(widget)) = (old.root(), old.widget()) {
kept.insert(root, (widget, old.by_keyboard));
}
if let Some(root) = new.root() {
kept.remove(&root);
}
});
}
focus().set(new);
request_job(id, JobRequest::Paint);
}
pub(crate) fn give_back_kept_focus(tree: &Tree, root: WidgetId) {
let Some((id, by_keyboard)) = with_app_state(|app| app.kept_focus.borrow_mut().remove(&root))
else {
return;
};
if tree.surface_root_of(id) == Some(root) {
move_focus(tree, id, by_keyboard);
}
}
pub(crate) fn offer_autofocus(tree: &Tree, id: WidgetId) {
let Some(root) = tree.surface_root_of(id) else {
return;
};
match focus().with_untracked(FocusPath::root) {
None => request_focus(tree, id),
Some(held) if held == root => {}
Some(_) if tree.keyboard_is_on(root) => request_focus(tree, id),
Some(_) => with_app_state(|app| {
app.kept_focus
.borrow_mut()
.entry(root)
.or_insert((id, false));
}),
}
}
pub(crate) fn forget_surface(root: WidgetId) {
with_app_state(|app| app.kept_focus.borrow_mut().remove(&root));
}
pub(crate) fn has_focus_with_keyboard(tree: &Tree, id: WidgetId) -> bool {
focus().with_untracked(|path| {
path.widget() == Some(id) && path.root().is_some_and(|root| tree.has_keyboard(root))
})
}
pub(crate) fn request_focus_deferred(widget_ref: crate::widget_ref::WidgetRef) {
with_app_state(|app| *app.pending_focus.borrow_mut() = Some(widget_ref));
crate::jobs::wake_loop();
}
pub fn apply_pending_focus(tree: &Tree) {
let Some(widget_ref) = with_app_state(|app| *app.pending_focus.borrow()) else {
return;
};
match widget_ref.attachment() {
Attachment::Gone => with_app_state(|app| *app.pending_focus.borrow_mut() = None),
Attachment::Unattached => {}
Attachment::To(id) => {
with_app_state(|app| *app.pending_focus.borrow_mut() = None);
request_focus(tree, id);
}
}
}
pub(crate) fn drop_pending_focus_for(widget_ref: crate::widget_ref::WidgetRef) {
with_app_state(|app| {
let mut pending = app.pending_focus.borrow_mut();
if *pending == Some(widget_ref) {
*pending = None;
}
});
}
pub fn release_focus(id: WidgetId) {
with_app_state(|app| {
app.kept_focus
.borrow_mut()
.retain(|_, (kept, _)| *kept != id)
});
if focus().get_untracked().widget() == Some(id) {
request_job(id, JobRequest::Paint);
focus().set(FocusPath::default());
}
}
pub(crate) fn release_focus_if_within(id: WidgetId) {
if focus_within(id) {
focus().set(FocusPath::default());
}
}
pub(crate) fn focus_within(id: WidgetId) -> bool {
focus().with_untracked(|path| path.contains(id))
}
pub(crate) fn release_focus_under(root: WidgetId) {
if focus().with_untracked(|path| path.root() == Some(root)) {
drop_focus();
}
}
pub fn has_focus(id: WidgetId) -> bool {
focus().get_untracked().widget() == Some(id)
}
pub fn focused_widget() -> Option<WidgetId> {
focus().get_untracked().widget()
}
pub fn clear_focus() {
with_app_state(|app| app.kept_focus.borrow_mut().clear());
drop_focus();
}
fn drop_focus() {
let old = focus().get_untracked();
if let Some(old_id) = old.widget() {
request_job(old_id, JobRequest::Paint);
focus().set(FocusPath::default());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::owner::{create_root_owner, dispose_owner_now, with_owner};
#[test]
fn the_keyboard_entering_another_surface_leaves_this_one_without_it() {
use crate::layout::Constraints;
use crate::widgets::{Event, container, text_input};
create_root_owner();
clear_focus();
let mut tree = Tree::new();
let field = tree.register(Box::new(text_input(crate::reactive::create_signal(
String::new(),
))));
let other = tree.register(Box::new(container()));
for root in [field, other] {
tree.layout_widget(root, Constraints::new(0.0, 0.0, 200.0, 40.0));
}
request_focus(&tree, field);
assert!(
has_focus_with_keyboard(&tree, field),
"nothing said otherwise yet"
);
tree.dispatch(other, &Event::FocusIn);
assert!(has_focus(field), "the focus stays");
assert!(!has_focus_with_keyboard(&tree, field), "the keyboard went");
tree.dispatch(field, &Event::FocusOut);
tree.dispatch(field, &Event::FocusIn);
assert!(has_focus_with_keyboard(&tree, field), "and came back");
tree.dispatch(other, &Event::FocusOut);
assert!(
has_focus_with_keyboard(&tree, field),
"a late leave from the other surface says nothing about this one"
);
}
#[test]
fn a_kept_focus_that_was_released_is_not_given_back() {
use crate::layout::Constraints;
use crate::widgets::{Event, text_input};
create_root_owner();
clear_focus();
let mut tree = Tree::new();
let field = |tree: &mut Tree| {
let id = tree.register(Box::new(text_input(crate::reactive::create_signal(
String::new(),
))));
tree.layout_widget(id, Constraints::new(0.0, 0.0, 200.0, 40.0));
id
};
let search = field(&mut tree);
let in_popup = field(&mut tree);
request_focus(&tree, search);
request_focus(&tree, in_popup);
release_focus(search);
tree.dispatch(search, &Event::FocusIn);
assert!(!has_focus(search), "blurred, and it stays blurred");
assert!(has_focus(in_popup), "the focus is where it was");
}
fn two_fields(tree: &mut Tree) -> (WidgetId, WidgetId, WidgetId) {
use crate::layout::Constraints;
use crate::widgets::{container, text_input};
let field = || text_input(crate::reactive::create_signal(String::new()));
let root = tree.register(Box::new(container().child(field()).child(field())));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
tree.layout_widget(root, Constraints::new(0.0, 0.0, 200.0, 80.0));
let children = tree.get_children(root).to_vec();
(root, children[0], children[1])
}
#[test]
fn autofocus_leaves_the_focus_on_its_own_surface_alone() {
use crate::widgets::Event;
create_root_owner();
clear_focus();
let mut tree = Tree::new();
let (root, typed_into, autofocusing) = two_fields(&mut tree);
tree.dispatch(root, &Event::FocusIn);
request_focus(&tree, typed_into);
offer_autofocus(&tree, autofocusing);
assert!(has_focus(typed_into));
}
#[test]
fn autofocus_waits_behind_a_widget_its_surface_already_keeps() {
use crate::widgets::Event;
create_root_owner();
clear_focus();
let mut tree = Tree::new();
let (popup, kept, autofocusing) = two_fields(&mut tree);
let (bar, on_bar, _) = two_fields(&mut tree);
tree.dispatch(bar, &Event::FocusIn);
request_focus(&tree, kept);
request_focus(&tree, on_bar);
offer_autofocus(&tree, autofocusing);
assert!(has_focus(on_bar), "the keyboard is on the bar");
tree.dispatch(popup, &Event::FocusIn);
assert!(has_focus(kept));
}
#[test]
fn a_request_dies_with_the_widget_it_named() {
use crate::layout::Constraints;
use crate::widgets::{container, text_input};
create_root_owner();
let field = crate::widget_ref::create_widget_ref();
let mut tree = Tree::new();
let root = tree.register(Box::new(container().child(
text_input(crate::reactive::create_signal(String::new())).widget_ref(field),
)));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
tree.layout_widget(root, Constraints::new(0.0, 0.0, 200.0, 40.0));
crate::widget_ref::update_widget_refs(&tree);
assert!(field.widget().is_some(), "laid out, so it names a widget");
request_focus_deferred(field);
crate::jobs::teardown_widget_subtree(&mut tree, root);
crate::widget_ref::update_widget_refs(&tree);
assert!(
with_app_state(|app| app.pending_focus.borrow().is_none()),
"a request for a widget that has left does not wait for its ref to be reused"
);
}
#[test]
fn a_parked_request_whose_ref_died_is_dropped_rather_than_read_forever() {
create_root_owner();
let tree = Tree::new();
let ((), popup_scope) = with_owner(|| {
request_focus_deferred(crate::widget_ref::create_widget_ref());
});
dispose_owner_now(popup_scope);
apply_pending_focus(&tree);
apply_pending_focus(&tree);
assert!(
with_app_state(|app| app.pending_focus.borrow().is_none()),
"a request that can never be honoured does not stay parked"
);
}
}