Skip to main content

cranpose_ui/modifier/
focus.rs

1//! Focus modifier nodes for Cranpose.
2//!
3//! This module implements focus management that mirrors Jetpack Compose's
4//! focus system. Focus nodes participate in focus traversal, track focus state,
5//! and integrate with the modifier chain lifecycle.
6
7use std::{
8    cell::Cell,
9    hash::{Hash, Hasher},
10    rc::Rc,
11};
12
13use cranpose_foundation::{
14    DelegatableNode, FocusNode, FocusState, ModifierNode, ModifierNodeContext, ModifierNodeElement,
15    NodeCapabilities, NodeState, impl_focus_node,
16};
17
18/// Focus direction for navigation.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum FocusDirection {
21    /// Enter focus from outside.
22    Enter,
23    /// Exit focus to outside.
24    Exit,
25    /// Move to next focusable.
26    Next,
27    /// Move to previous focusable.
28    Previous,
29    /// Move up (2D navigation).
30    Up,
31    /// Move down (2D navigation).
32    Down,
33    /// Move left (2D navigation).
34    Left,
35    /// Move right (2D navigation).
36    Right,
37}
38
39/// A focus target node that can receive focus.
40///
41/// This is the core building block for focusable components. Each focus target
42/// tracks its own focus state and participates in the focus traversal system.
43pub struct FocusTargetNode {
44    state: NodeState,
45    focus_state: Cell<FocusState>,
46    on_focus_changed: Option<Rc<dyn Fn(FocusState)>>,
47}
48
49impl FocusTargetNode {
50    pub fn new() -> Self {
51        Self {
52            state: NodeState::new(),
53            focus_state: Cell::new(FocusState::Inactive),
54            on_focus_changed: None,
55        }
56    }
57
58    pub fn with_callback<F>(callback: F) -> Self
59    where
60        F: Fn(FocusState) + 'static,
61    {
62        Self {
63            state: NodeState::new(),
64            focus_state: Cell::new(FocusState::Inactive),
65            on_focus_changed: Some(Rc::new(callback)),
66        }
67    }
68
69    /// Sets the focus state for this node.
70    pub fn set_focus_state(&self, state: FocusState) {
71        let old_state = self.focus_state.get();
72        if old_state != state {
73            self.focus_state.set(state);
74            if let Some(callback) = &self.on_focus_changed {
75                callback(state);
76            }
77        }
78    }
79
80    /// Clears focus from this node.
81    pub fn clear_focus(&self) {
82        self.set_focus_state(FocusState::Inactive);
83    }
84}
85
86impl Default for FocusTargetNode {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl DelegatableNode for FocusTargetNode {
93    fn node_state(&self) -> &NodeState {
94        &self.state
95    }
96}
97
98impl ModifierNode for FocusTargetNode {
99    fn on_attach(&mut self, _context: &mut dyn ModifierNodeContext) {
100        self.state.set_attached(true);
101    }
102
103    fn on_detach(&mut self) {
104        self.state.set_attached(false);
105        self.clear_focus();
106    }
107
108    // Capability-driven implementation using helper macro
109    impl_focus_node!();
110}
111
112impl FocusNode for FocusTargetNode {
113    fn focus_state(&self) -> FocusState {
114        self.focus_state.get()
115    }
116
117    fn on_focus_changed(&mut self, _context: &mut dyn ModifierNodeContext, state: FocusState) {
118        self.set_focus_state(state);
119    }
120}
121
122/// Modifier element for focus targets.
123///
124/// Creates a focusable modifier that can receive and track focus.
125#[derive(Clone)]
126pub struct FocusTargetElement {
127    on_focus_changed: Option<Rc<dyn Fn(FocusState)>>,
128}
129
130impl FocusTargetElement {
131    pub fn new() -> Self {
132        Self {
133            on_focus_changed: None,
134        }
135    }
136
137    pub fn with_callback<F>(callback: F) -> Self
138    where
139        F: Fn(FocusState) + 'static,
140    {
141        Self {
142            on_focus_changed: Some(Rc::new(callback)),
143        }
144    }
145}
146
147impl Default for FocusTargetElement {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl std::fmt::Debug for FocusTargetElement {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.debug_struct("FocusTargetElement")
156            .field("has_callback", &self.on_focus_changed.is_some())
157            .finish()
158    }
159}
160
161impl PartialEq for FocusTargetElement {
162    fn eq(&self, other: &Self) -> bool {
163        // Type-based matching: compare only presence of callback, not pointer
164        // Nodes are updated via update() method, preserving behavior
165        self.on_focus_changed.is_some() == other.on_focus_changed.is_some()
166    }
167}
168
169impl Hash for FocusTargetElement {
170    fn hash<H: Hasher>(&self, state: &mut H) {
171        // Consistent hash based on callback presence only
172        "focus_target".hash(state);
173        self.on_focus_changed.is_some().hash(state);
174    }
175}
176
177impl ModifierNodeElement for FocusTargetElement {
178    type Node = FocusTargetNode;
179
180    fn create(&self) -> Self::Node {
181        if let Some(callback) = &self.on_focus_changed {
182            FocusTargetNode::with_callback({
183                let callback = callback.clone();
184                move |state| callback(state)
185            })
186        } else {
187            FocusTargetNode::new()
188        }
189    }
190
191    fn update(&self, node: &mut Self::Node) {
192        node.on_focus_changed = self.on_focus_changed.clone();
193    }
194
195    fn inspector_name(&self) -> &'static str {
196        "focusTarget"
197    }
198
199    fn capabilities(&self) -> NodeCapabilities {
200        NodeCapabilities::FOCUS
201    }
202
203    fn always_update(&self) -> bool {
204        // Always update to capture new closure if it changed
205        true
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use cranpose_foundation::{BasicModifierNodeContext, ModifierNodeChain};
212
213    use super::*;
214
215    #[test]
216    fn focus_target_node_lifecycle() {
217        let mut node = FocusTargetNode::new();
218        let mut context = BasicModifierNodeContext::new();
219
220        assert_eq!(node.focus_state(), FocusState::Inactive);
221        assert!(!node.node_state().is_attached());
222
223        node.on_attach(&mut context);
224        assert!(node.node_state().is_attached());
225
226        node.set_focus_state(FocusState::Active);
227        assert_eq!(node.focus_state(), FocusState::Active);
228        assert!(node.focus_state().is_focused());
229
230        node.on_detach();
231        assert!(!node.node_state().is_attached());
232        assert_eq!(node.focus_state(), FocusState::Inactive);
233    }
234
235    #[test]
236    fn focus_target_callback_invoked() {
237        use std::cell::RefCell;
238        let states = Rc::new(RefCell::new(Vec::new()));
239        let states_clone = states.clone();
240
241        let node = FocusTargetNode::with_callback(move |state| {
242            states_clone.borrow_mut().push(state);
243        });
244
245        node.set_focus_state(FocusState::Active);
246        node.set_focus_state(FocusState::ActiveParent);
247        node.set_focus_state(FocusState::Inactive);
248
249        let recorded = states.borrow();
250        assert_eq!(recorded.len(), 3);
251        assert_eq!(recorded[0], FocusState::Active);
252        assert_eq!(recorded[1], FocusState::ActiveParent);
253        assert_eq!(recorded[2], FocusState::Inactive);
254    }
255
256    #[test]
257    fn focus_element_creates_node() {
258        let element = FocusTargetElement::new();
259        let node = element.create();
260        assert_eq!(node.focus_state(), FocusState::Inactive);
261    }
262
263    #[test]
264    fn focus_chain_integration() {
265        let element = FocusTargetElement::new();
266        let dyn_element = cranpose_foundation::modifier_element(element);
267
268        let mut chain = ModifierNodeChain::new();
269        let mut context = BasicModifierNodeContext::new();
270
271        chain.update(vec![dyn_element], &mut context);
272
273        assert_eq!(chain.len(), 1);
274        assert!(chain.has_capability(NodeCapabilities::FOCUS));
275    }
276
277    #[test]
278    fn focus_state_predicates() {
279        assert!(FocusState::Active.is_focused());
280        assert!(FocusState::Captured.is_focused());
281        assert!(!FocusState::Inactive.is_focused());
282        assert!(!FocusState::ActiveParent.is_focused());
283
284        assert!(FocusState::Active.has_focus());
285        assert!(FocusState::ActiveParent.has_focus());
286        assert!(FocusState::Captured.has_focus());
287        assert!(!FocusState::Inactive.has_focus());
288
289        assert!(FocusState::Captured.is_captured());
290        assert!(!FocusState::Active.is_captured());
291    }
292}