compose_rust/ui/focus.rs
1//! Moving keyboard focus from Rust: a handle a widget is bound to, and a request that
2//! puts focus on that widget's node.
3
4use std::cell::Cell;
5use std::fmt;
6use std::rc::Rc;
7
8use crate::runtime::with_composer_if_idle;
9
10/// What a handle and the composition share: the node the handle is bound to, 0 while it
11/// is bound to none, and whether a request for it is waiting for the next batch.
12#[derive(Default)]
13pub(crate) struct FocusTarget {
14 pub(crate) node: Cell<u32>,
15 pub(crate) requested: Cell<bool>,
16}
17
18/// A way to put keyboard focus on one widget. Compose's `FocusRequester`.
19///
20/// Make one with [`use_focus_handle`], give it to a widget with `.focus_handle(&handle)`,
21/// and call [`FocusHandle::request`] from a handler or an effect. Cloning is cheap: every
22/// clone is the same handle. The handle follows its widget's node across re-renders, and
23/// when the widget is replaced by a new node, it follows the new one.
24///
25/// A drawer that takes focus when it opens and gives it back to the control that opened
26/// it when Escape closes it:
27///
28/// ```ignore
29/// use compose_rust::runtime::*;
30/// use compose_rust::ui::*;
31///
32/// #[Composable]
33/// fn Screen() {
34/// let open = remember(|| mutable_state_of(false));
35/// let opener = use_focus_handle();
36/// let drawer = use_focus_handle();
37/// Column().content(|| {
38/// Button("Menu")
39/// .focus_handle(&opener)
40/// .on_click(move || open.set(true));
41/// if open.get() {
42/// let back = opener.clone();
43/// Column()
44/// .focus_handle(&drawer)
45/// .on_key_down(move |key| {
46/// if key.text() == "Escape" {
47/// key.consume();
48/// open.set(false);
49/// back.request();
50/// }
51/// })
52/// .content(|| {
53/// Text("Drawer");
54/// });
55/// }
56/// });
57/// // The drawer's node exists once the composition that opened it is applied, so
58/// // the request runs as an effect of that composition rather than in the click.
59/// let take = drawer.clone();
60/// let is_open = open.get();
61/// launched_effect(is_open, move || async move {
62/// if is_open {
63/// take.request();
64/// }
65/// });
66/// }
67/// ```
68///
69/// A request made while the handle's node is not on screen does nothing and is not kept
70/// for later, which is why the drawer above asks from an effect.
71#[derive(Clone, Default)]
72pub struct FocusHandle {
73 target: Rc<FocusTarget>,
74}
75
76impl FocusHandle {
77 /// A handle bound to nothing yet.
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 /// Asks for keyboard focus on the node this handle is bound to. The next batch
83 /// carries one `RequestFocus` for it, however many times this is called before then.
84 ///
85 /// A handle that is bound to no node on screen, because its widget has not been
86 /// composed yet or has left, asks for nothing.
87 pub fn request(&self) {
88 if self.target.requested.get() {
89 return;
90 }
91 let target = Rc::clone(&self.target);
92 with_composer_if_idle(|composer| composer.request_focus(target));
93 }
94
95 /// Whether the handle is bound to a node. A node that has since left the screen still
96 /// counts until another widget takes the handle.
97 pub fn is_bound(&self) -> bool {
98 self.target.node.get() != 0
99 }
100
101 pub(crate) fn target(&self) -> Rc<FocusTarget> {
102 Rc::clone(&self.target)
103 }
104}
105
106impl fmt::Debug for FocusHandle {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 f.debug_struct("FocusHandle")
109 .field("node", &self.target.node.get())
110 .field("requested", &self.target.requested.get())
111 .finish()
112 }
113}
114
115impl PartialEq for FocusHandle {
116 fn eq(&self, other: &Self) -> bool {
117 Rc::ptr_eq(&self.target, &other.target)
118 }
119}
120
121/// A focus handle this call site keeps across compositions. Compose's
122/// `remember { FocusRequester() }`.
123#[track_caller]
124pub fn use_focus_handle() -> FocusHandle {
125 crate::runtime::remember(FocusHandle::new)
126}
127
128/// Binds `handle` to the node the widget being composed is about to create or update.
129/// Called right before the widget's `emit`.
130pub(crate) fn bind_focus_handle(handle: Option<FocusHandle>) {
131 if let Some(handle) = handle {
132 with_composer_if_idle(|composer| composer.bind_focus_to_next_node(handle.target()));
133 }
134}