Skip to main content

cranpose_ui/modifier/
focus.rs

1use std::{
2    cell::{Cell, RefCell},
3    hash::{Hash, Hasher},
4    rc::Rc,
5};
6
7use cranpose_core::NodeId;
8use cranpose_foundation::{
9    DelegatableNode, FocusNode, FocusState, ModifierNode, ModifierNodeContext, ModifierNodeElement,
10    NodeCapabilities, NodeState, impl_focus_node,
11};
12
13use crate::{focus_dispatch, render_state::AppContextId};
14
15/// Focus direction for navigation.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum FocusDirection {
18    /// Enter focus from outside.
19    Enter,
20    /// Exit focus to outside.
21    Exit,
22    /// Move to next focusable.
23    Next,
24    /// Move to previous focusable.
25    Previous,
26    /// Move up (2D navigation).
27    Up,
28    /// Move down (2D navigation).
29    Down,
30    /// Move left (2D navigation).
31    Left,
32    /// Move right (2D navigation).
33    Right,
34}
35
36type FocusChangedCallback = Rc<dyn Fn(FocusState)>;
37
38struct FocusTargetShared {
39    focus_state: Cell<FocusState>,
40    on_focus_changed: RefCell<Option<FocusChangedCallback>>,
41}
42
43impl FocusTargetShared {
44    fn new(on_focus_changed: Option<FocusChangedCallback>) -> Self {
45        Self {
46            focus_state: Cell::new(FocusState::Inactive),
47            on_focus_changed: RefCell::new(on_focus_changed),
48        }
49    }
50
51    fn set_focus_state(&self, state: FocusState) {
52        let old_state = self.focus_state.get();
53        if old_state != state {
54            self.focus_state.set(state);
55            let callback = self.on_focus_changed.borrow().clone();
56            if let Some(callback) = callback {
57                callback(state);
58            }
59        }
60    }
61}
62
63impl focus_dispatch::FocusTargetHandle for FocusTargetShared {
64    fn set_focus_state(&self, state: FocusState) {
65        FocusTargetShared::set_focus_state(self, state);
66    }
67}
68
69pub struct FocusTargetNode {
70    state: NodeState,
71    shared: Rc<FocusTargetShared>,
72    handle: Rc<dyn focus_dispatch::FocusTargetHandle>,
73    registered_node_id: Cell<Option<NodeId>>,
74}
75
76impl FocusTargetNode {
77    pub fn new() -> Self {
78        Self::from_callback(None)
79    }
80
81    pub fn with_callback<F>(callback: F) -> Self
82    where
83        F: Fn(FocusState) + 'static,
84    {
85        Self::from_callback(Some(Rc::new(callback) as FocusChangedCallback))
86    }
87
88    fn from_callback(on_focus_changed: Option<FocusChangedCallback>) -> Self {
89        let shared = Rc::new(FocusTargetShared::new(on_focus_changed));
90        let handle: Rc<dyn focus_dispatch::FocusTargetHandle> = shared.clone();
91        Self {
92            state: NodeState::new(),
93            shared,
94            handle,
95            registered_node_id: Cell::new(None),
96        }
97    }
98
99    pub fn set_focus_state(&self, state: FocusState) {
100        self.shared.set_focus_state(state);
101    }
102
103    pub fn clear_focus(&self) {
104        self.set_focus_state(FocusState::Inactive);
105    }
106
107    fn set_callback(&self, callback: Option<FocusChangedCallback>) {
108        *self.shared.on_focus_changed.borrow_mut() = callback;
109    }
110}
111
112impl Default for FocusTargetNode {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl DelegatableNode for FocusTargetNode {
119    fn node_state(&self) -> &NodeState {
120        &self.state
121    }
122}
123
124impl ModifierNode for FocusTargetNode {
125    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
126        self.state.set_attached(true);
127        if let Some(node_id) = context.node_id() {
128            self.registered_node_id.set(Some(node_id));
129            focus_dispatch::register_focus_target(node_id, Rc::clone(&self.handle));
130        }
131    }
132
133    fn on_detach(&mut self) {
134        self.state.set_attached(false);
135        if let Some(node_id) = self.registered_node_id.take() {
136            focus_dispatch::unregister_focus_target(node_id, &self.handle);
137        }
138        self.clear_focus();
139    }
140
141    impl_focus_node!();
142}
143
144impl FocusNode for FocusTargetNode {
145    fn focus_state(&self) -> FocusState {
146        self.shared.focus_state.get()
147    }
148
149    fn on_focus_changed(&mut self, _context: &mut dyn ModifierNodeContext, state: FocusState) {
150        self.set_focus_state(state);
151    }
152}
153
154#[derive(Clone)]
155pub struct FocusTargetElement {
156    on_focus_changed: Option<FocusChangedCallback>,
157}
158
159impl FocusTargetElement {
160    pub fn new() -> Self {
161        Self {
162            on_focus_changed: None,
163        }
164    }
165
166    pub fn with_callback<F>(callback: F) -> Self
167    where
168        F: Fn(FocusState) + 'static,
169    {
170        Self {
171            on_focus_changed: Some(Rc::new(callback)),
172        }
173    }
174}
175
176impl Default for FocusTargetElement {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl std::fmt::Debug for FocusTargetElement {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("FocusTargetElement")
185            .field("has_callback", &self.on_focus_changed.is_some())
186            .finish()
187    }
188}
189
190impl PartialEq for FocusTargetElement {
191    fn eq(&self, other: &Self) -> bool {
192        self.on_focus_changed.is_some() == other.on_focus_changed.is_some()
193    }
194}
195
196impl Hash for FocusTargetElement {
197    fn hash<H: Hasher>(&self, state: &mut H) {
198        "focus_target".hash(state);
199        self.on_focus_changed.is_some().hash(state);
200    }
201}
202
203impl ModifierNodeElement for FocusTargetElement {
204    type Node = FocusTargetNode;
205
206    fn create(&self) -> Self::Node {
207        if let Some(callback) = &self.on_focus_changed {
208            FocusTargetNode::with_callback({
209                let callback = callback.clone();
210                move |state| callback(state)
211            })
212        } else {
213            FocusTargetNode::new()
214        }
215    }
216
217    fn update(&self, node: &mut Self::Node) {
218        node.set_callback(self.on_focus_changed.clone());
219    }
220
221    fn inspector_name(&self) -> &'static str {
222        "focusTarget"
223    }
224
225    fn capabilities(&self) -> NodeCapabilities {
226        NodeCapabilities::FOCUS
227    }
228
229    fn always_update(&self) -> bool {
230        true
231    }
232}
233
234/// Why [`FocusRequester::request_focus`] could not move focus.
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub enum FocusRequestError {
237    /// The requester has never been attached to a node via
238    /// [`Modifier::focus_requester`](super::Modifier::focus_requester), or the
239    /// node it was attached to has since left composition.
240    NotAttached,
241    /// The requester's node is attached, but nothing on it — no
242    /// [`Modifier::focus_target`](super::Modifier::focus_target), no
243    /// [`Modifier::on_focus_changed`](super::Modifier::on_focus_changed), no
244    /// text field — is registered to receive focus.
245    NoFocusTarget,
246}
247
248impl std::fmt::Display for FocusRequestError {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        match self {
251            FocusRequestError::NotAttached => {
252                write!(f, "FocusRequester is not attached to a node in composition")
253            }
254            FocusRequestError::NoFocusTarget => write!(
255                f,
256                "FocusRequester's node has no focus target to move focus onto"
257            ),
258        }
259    }
260}
261
262impl std::error::Error for FocusRequestError {}
263
264#[derive(Clone, Copy)]
265struct FocusRequesterBinding {
266    app_context: AppContextId,
267    node_id: NodeId,
268}
269
270/// A handle an app holds to move focus onto a node imperatively.
271///
272/// `remember` one, hang it on a node with
273/// [`Modifier::focus_requester`](super::Modifier::focus_requester) next to a
274/// [`Modifier::focus_target`](super::Modifier::focus_target) (or
275/// [`Modifier::on_focus_changed`](super::Modifier::on_focus_changed)), and
276/// call [`request_focus`](Self::request_focus) — from a click handler, an
277/// effect that runs once a screen appears, wherever the app decides focus
278/// should move.
279///
280/// ```ignore
281/// let requester = remember(FocusRequester::new).with(Clone::clone);
282/// // ... in the composition:
283/// Modifier::empty().focus_requester(&requester).focus_target()
284/// // ... later, imperatively:
285/// requester.request_focus().ok();
286/// ```
287#[derive(Clone, Default)]
288pub struct FocusRequester {
289    binding: Rc<Cell<Option<FocusRequesterBinding>>>,
290}
291
292impl FocusRequester {
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    /// Moves focus onto the node this requester is attached to.
298    ///
299    /// See [`FocusRequestError`] for the two predictable ways this can fail
300    /// instead of moving focus.
301    pub fn request_focus(&self) -> Result<(), FocusRequestError> {
302        let Some(binding) = self.binding.get() else {
303            return Err(FocusRequestError::NotAttached);
304        };
305        match focus_dispatch::request_focus_for(binding.app_context, binding.node_id) {
306            Some(true) => Ok(()),
307            Some(false) => Err(FocusRequestError::NoFocusTarget),
308            None => Err(FocusRequestError::NotAttached),
309        }
310    }
311
312    /// The node this requester is attached to, if attached.
313    pub fn node_id(&self) -> Option<NodeId> {
314        self.binding.get().map(|binding| binding.node_id)
315    }
316
317    fn bind(&self, binding: Option<FocusRequesterBinding>) {
318        self.binding.set(binding);
319    }
320
321    fn binding_here(node_id: Option<NodeId>) -> Option<FocusRequesterBinding> {
322        Some(FocusRequesterBinding {
323            app_context: crate::render_state::current_app_context_id_opt()?,
324            node_id: node_id?,
325        })
326    }
327}
328
329impl std::fmt::Debug for FocusRequester {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        f.debug_struct("FocusRequester")
332            .field("node_id", &self.node_id())
333            .finish()
334    }
335}
336
337pub struct FocusRequesterNode {
338    state: NodeState,
339    requester: FocusRequester,
340}
341
342impl FocusRequesterNode {
343    pub(crate) fn new(requester: FocusRequester) -> Self {
344        Self {
345            state: NodeState::new(),
346            requester,
347        }
348    }
349}
350
351impl DelegatableNode for FocusRequesterNode {
352    fn node_state(&self) -> &NodeState {
353        &self.state
354    }
355}
356
357impl ModifierNode for FocusRequesterNode {
358    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
359        self.state.set_attached(true);
360        self.requester
361            .bind(FocusRequester::binding_here(context.node_id()));
362    }
363
364    fn on_detach(&mut self) {
365        self.state.set_attached(false);
366        self.requester.bind(None);
367    }
368}
369
370/// Modifier element for [`FocusRequester`].
371#[derive(Clone)]
372pub struct FocusRequesterElement {
373    requester: FocusRequester,
374}
375
376impl FocusRequesterElement {
377    pub(crate) fn new(requester: FocusRequester) -> Self {
378        Self { requester }
379    }
380}
381
382impl std::fmt::Debug for FocusRequesterElement {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        f.write_str("FocusRequesterElement")
385    }
386}
387
388impl PartialEq for FocusRequesterElement {
389    fn eq(&self, other: &Self) -> bool {
390        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
391    }
392}
393
394impl Eq for FocusRequesterElement {}
395
396impl Hash for FocusRequesterElement {
397    fn hash<H: Hasher>(&self, state: &mut H) {
398        Rc::as_ptr(&self.requester.binding).hash(state);
399    }
400}
401
402impl ModifierNodeElement for FocusRequesterElement {
403    type Node = FocusRequesterNode;
404
405    fn create(&self) -> Self::Node {
406        FocusRequesterNode::new(self.requester.clone())
407    }
408
409    fn update(&self, node: &mut Self::Node) {
410        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
411            let bound = node.requester.binding.get();
412            node.requester.bind(None);
413            node.requester = self.requester.clone();
414            node.requester.bind(bound);
415        }
416    }
417
418    fn inspector_name(&self) -> &'static str {
419        "focusRequester"
420    }
421
422    fn capabilities(&self) -> NodeCapabilities {
423        NodeCapabilities::NONE
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
430
431    use super::*;
432
433    #[test]
434    fn focus_target_node_lifecycle() {
435        let mut node = FocusTargetNode::new();
436        let mut context = BasicModifierNodeContext::new();
437
438        assert_eq!(node.focus_state(), FocusState::Inactive);
439        assert!(!node.node_state().is_attached());
440
441        node.on_attach(&mut context);
442        assert!(node.node_state().is_attached());
443
444        node.set_focus_state(FocusState::Active);
445        assert_eq!(node.focus_state(), FocusState::Active);
446        assert!(node.focus_state().is_focused());
447
448        node.on_detach();
449        assert!(!node.node_state().is_attached());
450        assert_eq!(node.focus_state(), FocusState::Inactive);
451    }
452
453    #[test]
454    fn focus_target_callback_invoked() {
455        use std::cell::RefCell;
456        let states = Rc::new(RefCell::new(Vec::new()));
457        let states_clone = states.clone();
458
459        let node = FocusTargetNode::with_callback(move |state| {
460            states_clone.borrow_mut().push(state);
461        });
462
463        node.set_focus_state(FocusState::Active);
464        node.set_focus_state(FocusState::ActiveParent);
465        node.set_focus_state(FocusState::Inactive);
466
467        let recorded = states.borrow();
468        assert_eq!(recorded.len(), 3);
469        assert_eq!(recorded[0], FocusState::Active);
470        assert_eq!(recorded[1], FocusState::ActiveParent);
471        assert_eq!(recorded[2], FocusState::Inactive);
472    }
473
474    #[test]
475    fn focus_element_creates_node() {
476        let element = FocusTargetElement::new();
477        let node = element.create();
478        assert_eq!(node.focus_state(), FocusState::Inactive);
479    }
480
481    #[test]
482    fn focus_chain_integration() {
483        let element = FocusTargetElement::new();
484        let dyn_element = cranpose_foundation::modifier_element(element);
485
486        let mut chain = ModifierNodeChain::new();
487        let mut context = BasicModifierNodeContext::new();
488
489        chain.update(vec![dyn_element], &mut context);
490
491        assert_eq!(chain.len(), 1);
492        assert!(chain.has_capability(NodeCapabilities::FOCUS));
493    }
494
495    #[test]
496    fn focus_state_predicates() {
497        assert!(FocusState::Active.is_focused());
498        assert!(FocusState::Captured.is_focused());
499        assert!(!FocusState::Inactive.is_focused());
500        assert!(!FocusState::ActiveParent.is_focused());
501
502        assert!(FocusState::Active.has_focus());
503        assert!(FocusState::ActiveParent.has_focus());
504        assert!(FocusState::Captured.has_focus());
505        assert!(!FocusState::Inactive.has_focus());
506
507        assert!(FocusState::Captured.is_captured());
508        assert!(!FocusState::Active.is_captured());
509    }
510
511    fn attach_at(
512        node_id: NodeId,
513        elements: Vec<cranpose_foundation::DynModifierElement>,
514    ) -> ModifierNodeChain {
515        let mut context = BasicModifierNodeContext::new();
516        context.set_node_id(Some(node_id));
517        let mut chain = ModifierNodeChain::new();
518        chain.update(elements, &mut context);
519        chain
520    }
521
522    #[test]
523    fn request_focus_on_a_never_attached_requester_fails_predictably() {
524        let requester = FocusRequester::new();
525        assert_eq!(
526            requester.request_focus(),
527            Err(FocusRequestError::NotAttached)
528        );
529    }
530
531    #[test]
532    fn request_focus_on_a_requester_with_no_focus_target_fails_predictably() {
533        let _app_context = crate::render_state::app_context_test_scope();
534        let requester = FocusRequester::new();
535
536        let _chain = attach_at(
537            1,
538            vec![cranpose_foundation::modifier_element(
539                FocusRequesterElement::new(requester.clone()),
540            )],
541        );
542
543        assert_eq!(requester.node_id(), Some(1));
544        assert_eq!(
545            requester.request_focus(),
546            Err(FocusRequestError::NoFocusTarget)
547        );
548    }
549
550    #[test]
551    fn a_focus_requester_moves_focus_onto_its_paired_focus_target() {
552        let _app_context = crate::render_state::app_context_test_scope();
553        let requester = FocusRequester::new();
554
555        let chain = attach_at(
556            2,
557            vec![
558                cranpose_foundation::modifier_element(FocusRequesterElement::new(
559                    requester.clone(),
560                )),
561                cranpose_foundation::modifier_element(FocusTargetElement::new()),
562            ],
563        );
564
565        assert_eq!(requester.request_focus(), Ok(()));
566
567        let target = chain.node::<FocusTargetNode>(1).expect("focus target node");
568        assert_eq!(target.focus_state(), FocusState::Active);
569        assert_eq!(focus_dispatch::active_focus_target(), Some(2));
570    }
571
572    #[test]
573    fn request_focus_after_the_node_leaves_composition_fails_predictably() {
574        let _app_context = crate::render_state::app_context_test_scope();
575        let requester = FocusRequester::new();
576
577        let mut context = BasicModifierNodeContext::new();
578        context.set_node_id(Some(3));
579        let mut chain = ModifierNodeChain::new();
580        chain.update(
581            vec![
582                cranpose_foundation::modifier_element(FocusRequesterElement::new(
583                    requester.clone(),
584                )),
585                cranpose_foundation::modifier_element(FocusTargetElement::new()),
586            ],
587            &mut context,
588        );
589        assert!(requester.request_focus().is_ok());
590
591        chain.update(Vec::new(), &mut context);
592
593        assert_eq!(
594            requester.request_focus(),
595            Err(FocusRequestError::NotAttached)
596        );
597    }
598
599    #[test]
600    fn focus_survives_the_requested_node_being_recomposed() {
601        let _app_context = crate::render_state::app_context_test_scope();
602        let requester = FocusRequester::new();
603
604        let mut context = BasicModifierNodeContext::new();
605        context.set_node_id(Some(4));
606        let mut chain = ModifierNodeChain::new();
607        let make_elements = || {
608            vec![
609                cranpose_foundation::modifier_element(FocusRequesterElement::new(
610                    requester.clone(),
611                )),
612                cranpose_foundation::modifier_element(FocusTargetElement::new()),
613            ]
614        };
615        chain.update(make_elements(), &mut context);
616        assert_eq!(requester.request_focus(), Ok(()));
617
618        let node_ptr_before = {
619            let node = chain.node::<FocusTargetNode>(1).unwrap();
620            &*node as *const FocusTargetNode
621        };
622
623        chain.update(make_elements(), &mut context);
624
625        let target = chain.node::<FocusTargetNode>(1).unwrap();
626        let node_ptr_after = &*target as *const FocusTargetNode;
627        assert_eq!(
628            node_ptr_before, node_ptr_after,
629            "recomposition with a structurally equal modifier must reuse the node"
630        );
631        assert_eq!(
632            target.focus_state(),
633            FocusState::Active,
634            "recomposing the focused node must not reset its focus state"
635        );
636        assert_eq!(focus_dispatch::active_focus_target(), Some(4));
637    }
638
639    #[test]
640    fn requesting_focus_from_inside_on_focus_changed_does_not_double_borrow_or_recurse() {
641        let _app_context = crate::render_state::app_context_test_scope();
642
643        let requester_a = FocusRequester::new();
644        let requester_b = FocusRequester::new();
645        let requester_b_for_callback = requester_b.clone();
646        let bounced = Rc::new(Cell::new(false));
647        let bounced_for_callback = bounced.clone();
648
649        let chain_a = attach_at(
650            10,
651            vec![
652                cranpose_foundation::modifier_element(FocusRequesterElement::new(
653                    requester_a.clone(),
654                )),
655                cranpose_foundation::modifier_element(FocusTargetElement::with_callback(
656                    move |state| {
657                        if state == FocusState::Active && !bounced_for_callback.get() {
658                            bounced_for_callback.set(true);
659                            requester_b_for_callback.request_focus().expect(
660                                "a reentrant request_focus must succeed, not double-borrow",
661                            );
662                        }
663                    },
664                )),
665            ],
666        );
667        let chain_b = attach_at(
668            20,
669            vec![
670                cranpose_foundation::modifier_element(FocusRequesterElement::new(
671                    requester_b.clone(),
672                )),
673                cranpose_foundation::modifier_element(FocusTargetElement::new()),
674            ],
675        );
676
677        let result =
678            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| requester_a.request_focus()));
679        assert!(result.is_ok(), "a reentrant focus request must not panic");
680        assert_eq!(result.unwrap(), Ok(()));
681        assert!(bounced.get(), "the reentrant callback never ran");
682
683        let target_a = chain_a.node::<FocusTargetNode>(1).unwrap();
684        let target_b = chain_b.node::<FocusTargetNode>(1).unwrap();
685        assert_eq!(target_a.focus_state(), FocusState::Inactive);
686        assert_eq!(target_b.focus_state(), FocusState::Active);
687        assert_eq!(focus_dispatch::active_focus_target(), Some(20));
688    }
689}