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