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 super::Modifier;
14use crate::{
15    focus_dispatch,
16    focus_within::{FocusWithinChain, current_focus_within},
17    render_state::AppContextId,
18};
19
20/// Focus direction for navigation.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum FocusDirection {
23    /// Enter focus from outside.
24    Enter,
25    /// Exit focus to outside.
26    Exit,
27    /// Move to next focusable.
28    Next,
29    /// Move to previous focusable.
30    Previous,
31    /// Move up (2D navigation).
32    Up,
33    /// Move down (2D navigation).
34    Down,
35    /// Move left (2D navigation).
36    Left,
37    /// Move right (2D navigation).
38    Right,
39}
40
41type FocusChangedCallback = Rc<dyn Fn(FocusState)>;
42
43struct FocusTargetShared {
44    focus_state: Cell<FocusState>,
45    on_focus_changed: RefCell<Option<FocusChangedCallback>>,
46    /// The focus observers composed around this target.
47    observers: RefCell<Option<FocusWithinChain>>,
48}
49
50impl FocusTargetShared {
51    fn new(on_focus_changed: Option<FocusChangedCallback>) -> Self {
52        Self {
53            focus_state: Cell::new(FocusState::Inactive),
54            on_focus_changed: RefCell::new(on_focus_changed),
55            observers: RefCell::new(None),
56        }
57    }
58
59    fn set_focus_state(&self, state: FocusState) {
60        let old_state = self.focus_state.get();
61        if old_state != state {
62            self.focus_state.set(state);
63            if old_state.is_focused() != state.is_focused()
64                && let Some(observers) = self.observers.borrow().clone()
65            {
66                if state.is_focused() {
67                    observers.enter();
68                } else {
69                    observers.leave();
70                }
71            }
72            let callback = self.on_focus_changed.borrow().clone();
73            if let Some(callback) = callback {
74                callback(state);
75            }
76        }
77    }
78
79    /// Moves a focused target's focus from the old observers to the new.
80    fn set_observers(&self, observers: Option<FocusWithinChain>) {
81        let previous = self.observers.replace(observers.clone());
82        if previous == observers || !self.focus_state.get().is_focused() {
83            return;
84        }
85        if let Some(previous) = previous {
86            previous.leave();
87        }
88        if let Some(observers) = observers {
89            observers.enter();
90        }
91    }
92}
93
94impl focus_dispatch::FocusTargetHandle for FocusTargetShared {
95    fn set_focus_state(&self, state: FocusState) {
96        Self::set_focus_state(self, state);
97    }
98}
99
100pub(super) struct FocusTargetNode {
101    state: NodeState,
102    shared: Rc<FocusTargetShared>,
103    handle: Rc<dyn focus_dispatch::FocusTargetHandle>,
104    registered_node_id: Cell<Option<NodeId>>,
105}
106
107impl FocusTargetNode {
108    pub(super) fn new() -> Self {
109        Self::from_callback(None)
110    }
111
112    fn from_callback(on_focus_changed: Option<FocusChangedCallback>) -> Self {
113        let shared = Rc::new(FocusTargetShared::new(on_focus_changed));
114        let handle: Rc<dyn focus_dispatch::FocusTargetHandle> =
115            Rc::<FocusTargetShared>::clone(&shared);
116        Self {
117            state: NodeState::new(),
118            shared,
119            handle,
120            registered_node_id: Cell::new(None),
121        }
122    }
123
124    pub(super) fn set_focus_state(&self, state: FocusState) {
125        self.shared.set_focus_state(state);
126    }
127
128    pub(super) fn clear_focus(&self) {
129        self.set_focus_state(FocusState::Inactive);
130    }
131
132    fn set_callback(&self, callback: Option<FocusChangedCallback>) {
133        *self.shared.on_focus_changed.borrow_mut() = callback;
134    }
135}
136
137impl Default for FocusTargetNode {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl DelegatableNode for FocusTargetNode {
144    fn node_state(&self) -> &NodeState {
145        &self.state
146    }
147}
148
149impl ModifierNode for FocusTargetNode {
150    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
151        if let Some(node_id) = context.node_id() {
152            self.registered_node_id.set(Some(node_id));
153            focus_dispatch::register_focus_target(node_id, Rc::clone(&self.handle));
154        }
155    }
156
157    fn on_detach(&mut self) {
158        if let Some(node_id) = self.registered_node_id.take() {
159            focus_dispatch::unregister_focus_target(node_id, &self.handle);
160        }
161        self.clear_focus();
162    }
163
164    impl_focus_node!();
165}
166
167impl FocusNode for FocusTargetNode {
168    fn focus_state(&self) -> FocusState {
169        self.shared.focus_state.get()
170    }
171
172    fn on_focus_changed(&mut self, _context: &mut dyn ModifierNodeContext, state: FocusState) {
173        self.set_focus_state(state);
174    }
175}
176
177#[derive(Clone)]
178pub(super) struct FocusTargetElement {
179    on_focus_changed: Option<FocusChangedCallback>,
180    observers: Option<FocusWithinChain>,
181}
182
183impl FocusTargetElement {
184    pub(super) fn new() -> Self {
185        Self {
186            on_focus_changed: None,
187            observers: current_focus_within(),
188        }
189    }
190
191    pub(super) fn with_callback<F>(callback: F) -> Self
192    where
193        F: Fn(FocusState) + 'static,
194    {
195        Self {
196            on_focus_changed: Some(Rc::new(callback)),
197            observers: current_focus_within(),
198        }
199    }
200}
201
202impl Default for FocusTargetElement {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208impl std::fmt::Debug for FocusTargetElement {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        f.debug_struct("FocusTargetElement")
211            .field("has_callback", &self.on_focus_changed.is_some())
212            .finish()
213    }
214}
215
216impl PartialEq for FocusTargetElement {
217    fn eq(&self, other: &Self) -> bool {
218        self.on_focus_changed.is_some() == other.on_focus_changed.is_some()
219            && self.observers == other.observers
220    }
221}
222
223impl Hash for FocusTargetElement {
224    fn hash<H: Hasher>(&self, state: &mut H) {
225        "focus_target".hash(state);
226        self.on_focus_changed.is_some().hash(state);
227    }
228}
229
230impl ModifierNodeElement for FocusTargetElement {
231    type Node = FocusTargetNode;
232
233    fn create(&self) -> Self::Node {
234        let node = FocusTargetNode::from_callback(self.on_focus_changed.clone());
235        node.shared.set_observers(self.observers.clone());
236        node
237    }
238
239    fn update(&self, node: &mut Self::Node) {
240        node.set_callback(self.on_focus_changed.clone());
241        node.shared.set_observers(self.observers.clone());
242    }
243
244    fn inspector_name(&self) -> &'static str {
245        "focusTarget"
246    }
247
248    fn capabilities(&self) -> NodeCapabilities {
249        NodeCapabilities::FOCUS
250    }
251
252    fn always_update(&self) -> bool {
253        true
254    }
255}
256
257/// Why [`FocusRequester::request_focus`] could not move focus.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub enum FocusRequestError {
260    /// The requester has never been attached to a node via
261    /// [`Modifier::focus_requester`](super::Modifier::focus_requester), or the
262    /// node it was attached to has since left composition.
263    NotAttached,
264    /// The requester's node is attached, but nothing on it — no
265    /// [`Modifier::focus_target`](super::Modifier::focus_target), no
266    /// [`Modifier::on_focus_changed`](super::Modifier::on_focus_changed), no
267    /// text field — is registered to receive focus.
268    NoFocusTarget,
269}
270
271impl std::fmt::Display for FocusRequestError {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            Self::NotAttached => {
275                write!(f, "FocusRequester is not attached to a node in composition")
276            }
277            Self::NoFocusTarget => write!(
278                f,
279                "FocusRequester's node has no focus target to move focus onto"
280            ),
281        }
282    }
283}
284
285impl std::error::Error for FocusRequestError {}
286
287#[derive(Clone, Copy)]
288struct FocusRequesterBinding {
289    app_context: AppContextId,
290    node_id: NodeId,
291}
292
293/// A handle an app holds to move focus onto a node imperatively.
294///
295/// `remember` one, hang it on a node with
296/// [`Modifier::focus_requester`](super::Modifier::focus_requester) next to a
297/// [`Modifier::focus_target`](super::Modifier::focus_target) (or
298/// [`Modifier::on_focus_changed`](super::Modifier::on_focus_changed)), and
299/// call [`request_focus`](Self::request_focus) — from a click handler, an
300/// effect that runs once a screen appears, wherever the app decides focus
301/// should move.
302///
303/// ```ignore
304/// let requester = remember(FocusRequester::new).with(Clone::clone);
305/// // ... in the composition:
306/// Modifier::empty().focus_requester(&requester).focus_target()
307/// // ... later, imperatively:
308/// requester.request_focus().ok();
309/// ```
310#[derive(Clone, Default)]
311pub struct FocusRequester {
312    binding: Rc<Cell<Option<FocusRequesterBinding>>>,
313}
314
315impl FocusRequester {
316    /// Creates a requester that no node uses yet. [`Self::request_focus`] fails with
317    /// `FocusRequestError::NotAttached` until a `focus_requester` modifier attaches it.
318    pub fn new() -> Self {
319        Self::default()
320    }
321
322    /// Moves focus onto the node this requester is attached to.
323    ///
324    /// See [`FocusRequestError`] for the two predictable ways this can fail
325    /// instead of moving focus.
326    pub fn request_focus(&self) -> Result<(), FocusRequestError> {
327        let Some(binding) = self.binding.get() else {
328            return Err(FocusRequestError::NotAttached);
329        };
330        match focus_dispatch::request_focus_for(binding.app_context, binding.node_id) {
331            Some(true) => Ok(()),
332            Some(false) => Err(FocusRequestError::NoFocusTarget),
333            None => Err(FocusRequestError::NotAttached),
334        }
335    }
336
337    /// The node this requester is attached to, if attached.
338    pub fn node_id(&self) -> Option<NodeId> {
339        self.binding.get().map(|binding| binding.node_id)
340    }
341
342    fn bind(&self, binding: Option<FocusRequesterBinding>) {
343        self.binding.set(binding);
344    }
345
346    fn binding_here(node_id: Option<NodeId>) -> Option<FocusRequesterBinding> {
347        Some(FocusRequesterBinding {
348            app_context: crate::render_state::current_app_context_id_opt()?,
349            node_id: node_id?,
350        })
351    }
352}
353
354impl std::fmt::Debug for FocusRequester {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        f.debug_struct("FocusRequester")
357            .field("node_id", &self.node_id())
358            .finish()
359    }
360}
361
362pub(crate) struct FocusRequesterNode {
363    state: NodeState,
364    requester: FocusRequester,
365}
366
367impl FocusRequesterNode {
368    pub(crate) fn new(requester: FocusRequester) -> Self {
369        Self {
370            state: NodeState::new(),
371            requester,
372        }
373    }
374}
375
376impl DelegatableNode for FocusRequesterNode {
377    fn node_state(&self) -> &NodeState {
378        &self.state
379    }
380}
381
382impl ModifierNode for FocusRequesterNode {
383    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
384        self.requester
385            .bind(FocusRequester::binding_here(context.node_id()));
386    }
387
388    fn on_detach(&mut self) {
389        self.requester.bind(None);
390    }
391}
392
393#[derive(Clone)]
394pub(crate) struct FocusRequesterElement {
395    requester: FocusRequester,
396}
397
398impl FocusRequesterElement {
399    pub(crate) fn new(requester: FocusRequester) -> Self {
400        Self { requester }
401    }
402}
403
404impl std::fmt::Debug for FocusRequesterElement {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        f.write_str("FocusRequesterElement")
407    }
408}
409
410impl PartialEq for FocusRequesterElement {
411    fn eq(&self, other: &Self) -> bool {
412        Rc::ptr_eq(&self.requester.binding, &other.requester.binding)
413    }
414}
415
416impl Eq for FocusRequesterElement {}
417
418impl Hash for FocusRequesterElement {
419    fn hash<H: Hasher>(&self, state: &mut H) {
420        Rc::as_ptr(&self.requester.binding).hash(state);
421    }
422}
423
424impl ModifierNodeElement for FocusRequesterElement {
425    type Node = FocusRequesterNode;
426
427    fn create(&self) -> Self::Node {
428        FocusRequesterNode::new(self.requester.clone())
429    }
430
431    fn update(&self, node: &mut Self::Node) {
432        if !Rc::ptr_eq(&node.requester.binding, &self.requester.binding) {
433            let bound = node.requester.binding.get();
434            node.requester.bind(None);
435            node.requester = self.requester.clone();
436            node.requester.bind(bound);
437        }
438    }
439
440    fn inspector_name(&self) -> &'static str {
441        "focusRequester"
442    }
443
444    fn capabilities(&self) -> NodeCapabilities {
445        NodeCapabilities::NONE
446    }
447}
448
449impl Modifier {
450    /// Makes this component focusable.
451    ///
452    /// This adds a focus target node that can receive focus and participate
453    /// in focus traversal. The component will be included in tab order and
454    /// can be focused programmatically.
455    pub fn focus_target(self) -> Self {
456        let element = FocusTargetElement::new();
457        let modifier = Self::from_dyn_element(super::modifier_element(element));
458        self.then(modifier)
459    }
460
461    /// Makes this component focusable with a callback for focus changes.
462    ///
463    /// The callback is invoked whenever the focus state changes, allowing
464    /// components to react to gaining or losing focus.
465    pub fn on_focus_changed<F>(self, callback: F) -> Self
466    where
467        F: Fn(FocusState) + 'static,
468    {
469        let element = FocusTargetElement::with_callback(callback);
470        let modifier = Self::from_dyn_element(super::modifier_element(element));
471        self.then(modifier)
472    }
473
474    /// Binds a [`FocusRequester`] to this node, so an app can move focus onto
475    /// it imperatively with [`FocusRequester::request_focus`].
476    ///
477    /// Pair it with [`focus_target`](Self::focus_target) (or
478    /// [`on_focus_changed`](Self::on_focus_changed)) on the same node —
479    /// `request_focus` moves whichever focus targets are attached there.
480    pub fn focus_requester(self, requester: &FocusRequester) -> Self {
481        let element = FocusRequesterElement::new(requester.clone());
482        let modifier = Self::from_dyn_element(super::modifier_element(element));
483        self.then(modifier)
484    }
485}
486
487// What a group remembers so that entering it again lands where focus left it.
488pub(crate) struct FocusRestorer {
489    fallback: RefCell<Option<FocusRequester>>,
490    saved: Cell<Option<NodeId>>,
491}
492
493impl FocusRestorer {
494    pub(crate) fn save(&self, child: NodeId) {
495        self.saved.set(Some(child));
496    }
497
498    // The child that had focus when it last left the group, else the
499    // fallback's node.
500    pub(crate) fn restored(&self) -> impl Iterator<Item = NodeId> {
501        let fallback = self
502            .fallback
503            .borrow()
504            .as_ref()
505            .and_then(FocusRequester::node_id);
506        self.saved.get().into_iter().chain(fallback)
507    }
508}
509
510pub(crate) struct FocusGroupNode {
511    state: NodeState,
512    restorer: Option<Rc<FocusRestorer>>,
513    registered_node_id: Option<NodeId>,
514}
515
516impl DelegatableNode for FocusGroupNode {
517    fn node_state(&self) -> &NodeState {
518        &self.state
519    }
520}
521
522impl ModifierNode for FocusGroupNode {
523    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
524        if let Some(node_id) = context.node_id() {
525            self.registered_node_id = Some(node_id);
526            focus_dispatch::register_focus_group(node_id, self.restorer.clone());
527        }
528    }
529
530    fn on_detach(&mut self) {
531        if let Some(node_id) = self.registered_node_id.take() {
532            focus_dispatch::unregister_focus_group(node_id);
533        }
534    }
535}
536
537#[derive(Clone)]
538enum GroupKind {
539    Plain,
540    // A restoring group, with the requester whose node takes focus when no
541    // child had it yet.
542    Restorer(Option<FocusRequester>),
543}
544
545#[derive(Clone)]
546pub(super) struct FocusGroupElement {
547    kind: GroupKind,
548}
549
550impl FocusGroupElement {
551    fn restores(&self) -> bool {
552        matches!(self.kind, GroupKind::Restorer(_))
553    }
554}
555
556impl std::fmt::Debug for FocusGroupElement {
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        f.debug_struct("FocusGroupElement")
559            .field("restores", &self.restores())
560            .finish()
561    }
562}
563
564impl PartialEq for FocusGroupElement {
565    fn eq(&self, other: &Self) -> bool {
566        self.restores() == other.restores()
567    }
568}
569
570impl Hash for FocusGroupElement {
571    fn hash<H: Hasher>(&self, state: &mut H) {
572        "focus_group".hash(state);
573        self.restores().hash(state);
574    }
575}
576
577impl ModifierNodeElement for FocusGroupElement {
578    type Node = FocusGroupNode;
579
580    fn create(&self) -> Self::Node {
581        FocusGroupNode {
582            state: NodeState::new(),
583            restorer: match &self.kind {
584                GroupKind::Plain => None,
585                GroupKind::Restorer(fallback) => Some(Rc::new(FocusRestorer {
586                    fallback: RefCell::new(fallback.clone()),
587                    saved: Cell::new(None),
588                })),
589            },
590            registered_node_id: None,
591        }
592    }
593
594    fn update(&self, node: &mut Self::Node) {
595        if let (Some(restorer), GroupKind::Restorer(fallback)) = (&node.restorer, &self.kind) {
596            restorer.fallback.replace(fallback.clone());
597        }
598    }
599
600    fn inspector_name(&self) -> &'static str {
601        if self.restores() {
602            "focusRestorer"
603        } else {
604            "focusGroup"
605        }
606    }
607
608    fn capabilities(&self) -> NodeCapabilities {
609        NodeCapabilities::NONE
610    }
611
612    fn always_update(&self) -> bool {
613        true
614    }
615}
616
617impl Modifier {
618    /// Makes this node a focus group, as Compose's `Modifier.focusGroup()`:
619    /// an arrow key from a target inside the group moves to the nearest
620    /// target inside it, and leaves the group only when there is none that
621    /// way.
622    pub fn focus_group(self) -> Self {
623        self.then(Self::from_dyn_element(super::modifier_element(
624            FocusGroupElement {
625                kind: GroupKind::Plain,
626            },
627        )))
628    }
629
630    /// Makes this node a [`focus_group`](Self::focus_group) that gives focus
631    /// back to the child that had it last when an arrow key enters the group
632    /// again, as Compose's `Modifier.focusRestorer(fallback)`. A row of
633    /// cards keeps its place this way while focus visits other rows.
634    ///
635    /// The first time, or when that child is gone, focus goes to the node
636    /// `fallback` is attached to; with `None`, to the target the key itself
637    /// reaches.
638    pub fn focus_restorer(self, fallback: Option<FocusRequester>) -> Self {
639        self.then(Self::from_dyn_element(super::modifier_element(
640            FocusGroupElement {
641                kind: GroupKind::Restorer(fallback),
642            },
643        )))
644    }
645}
646
647#[cfg(test)]
648#[path = "tests/focus_tests.rs"]
649mod tests;