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]>,
}
impl FocusPath {
fn of(tree: &Tree, id: WidgetId) -> Self {
let mut chain = SmallVec::new();
let mut current = Some(id);
while let Some(node) = current {
chain.push(node);
current = tree.get_parent(node);
}
Self { chain }
}
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 fn is_empty(&self) -> bool {
self.chain.is_empty()
}
}
static FOCUS: GlobalSignal<FocusPath> = GlobalSignal::new(FocusPath::default);
fn focus() -> RwSignal<FocusPath> {
FOCUS.get()
}
pub fn focus_path() -> FocusPath {
focus().get()
}
pub fn request_focus(tree: &Tree, id: WidgetId) {
let old = focus().get_untracked();
if old.widget() == Some(id) {
return;
}
if let Some(old_id) = old.widget() {
request_job(old_id, JobRequest::Paint);
}
focus().set(FocusPath::of(tree, id));
request_job(id, JobRequest::Paint);
}
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) {
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)) {
clear_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() {
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 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"
);
}
}