rosace_widgets/tree/focus_api.rs
1use rosace_a11y::FocusNode;
2use rosace_render::{Color, DrawCommand};
3use super::{Widget, PaintCtx};
4
5// ── WithFocus wrapper ─────────────────────────────────────────────────────────
6
7/// Wraps a widget with a [`FocusNode`], enabling explicit focus graph wiring
8/// and reactive focus-ring rendering.
9///
10/// Created by the [`FocusApi`] builder methods. `focused()` is a reactive
11/// `Atom<bool>` — set it to `true` (via `FocusNode::request()`) to draw the
12/// focus ring around this widget.
13pub struct WithFocus<W: Widget> {
14 inner: W,
15 node: FocusNode,
16}
17
18impl<W: Widget + 'static> WithFocus<W> {
19 pub fn new(inner: W, node: FocusNode) -> Self {
20 Self { inner, node }
21 }
22
23 /// Wire an explicit Tab-forward neighbor.
24 pub fn focus_next_node(self, next: FocusNode) -> Self {
25 self.node.set_next(next);
26 self
27 }
28
29 /// Wire an explicit Shift+Tab / reverse neighbor.
30 pub fn focus_prev_node(self, prev: FocusNode) -> Self {
31 self.node.set_prev(prev);
32 self
33 }
34
35 /// The focus node attached to this widget (cloned — cheap Arc clone).
36 pub fn node(&self) -> FocusNode { self.node.clone() }
37}
38
39impl<W: Widget + Send + Sync + 'static> Widget for WithFocus<W> {
40 fn children(&self) -> super::Children<'_> {
41 super::Children::One(&self.inner)
42 }
43
44 fn paint(&self, ctx: &mut PaintCtx) {
45 // Register in DFS order so FocusManager can build the Tab cycle.
46 ctx.register_focus(self.node.clone());
47
48 self.inner.paint(ctx);
49
50 // Draw a 2px focus ring when focused.
51 if self.node.is_focused() {
52 let rect = ctx.rect;
53 ctx.recorder.push(DrawCommand::StrokeRect {
54 rect,
55 color: Color::rgba(100, 160, 255, 220),
56 width: 2.0,
57 });
58 }
59 }
60 // layout, flex_factor: protocol defaults delegate to the child.
61}
62
63// ── FocusApi trait — blanket impl for all widgets ─────────────────────────────
64
65/// Builder methods that attach a [`FocusNode`] to any widget.
66///
67/// ```rust,ignore
68/// let email = FocusNode::new();
69/// let pass = FocusNode::new();
70/// let submit = FocusNode::new();
71///
72/// TextInput::new("Email").focus_node(email.clone())
73/// .focus_next_node(pass.clone())
74///
75/// TextInput::new("Password").focus_node(pass.clone())
76/// .focus_next_node(submit.clone())
77/// .focus_prev_node(email.clone())
78///
79/// Button::new("Login").focus_node(submit.clone())
80/// .focus_prev_node(pass.clone())
81/// ```
82pub trait FocusApi: Widget + Sized + Send + Sync + 'static {
83 /// Attach a focus node. This enables focus-ring rendering and explicit
84 /// neighbor wiring.
85 fn focus_node(self, node: FocusNode) -> WithFocus<Self> {
86 WithFocus::new(self, node)
87 }
88}
89
90impl<W: Widget + Send + Sync + 'static> FocusApi for W {}