rat_focus/lib.rs
1#![doc = include_str!("../readme.md")]
2
3pub mod doc {
4 #![doc = include_str!("../doc.md")]
5}
6mod builder;
7mod core;
8mod flag;
9mod focus;
10
11/// Reexport of types used by a macro.
12pub mod ratatui {
13 pub mod layout {
14 pub use ratatui_core::layout::Rect;
15 }
16}
17
18pub use crate::builder::FocusBuilder;
19pub use crate::flag::FocusFlag;
20pub use crate::focus::{Focus, handle_focus};
21
22/// Focus navigation for widgets.
23///
24/// The effects that hinder focus-change (`Reach*`, `Lock`) only work
25/// when navigation changes via next()/prev()/focus_at().
26///
27/// Programmatic focus changes are always possible.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum Navigation {
30 /// Widget is not reachable with normal keyboard or mouse navigation.
31 None,
32 /// Focus is locked to stay with this widget. No mouse or keyboard navigation
33 /// can change that.
34 Lock,
35 /// Widget is not reachable with keyboard navigation, but can be focused with the mouse.
36 Mouse,
37 /// Widget cannot be reached with normal keyboard navigation, but can be left.
38 /// (e.g. Tabs, Split-Divider)
39 Leave,
40 /// Widget can be reached with normal keyboard navigation, but not left.
41 /// (e.g. TextArea)
42 Reach,
43 /// Widget can be reached with normal keyboard navigation, but only be left with
44 /// backward navigation.
45 /// (e.g. some widget with internal structure)
46 ReachLeaveFront,
47 /// Widget can be reached with normal keyboard navigation, but only be left with
48 /// forward navigation.
49 /// (e.g. some widget with internal structure)
50 ReachLeaveBack,
51 /// Widget can be reached and left with normal keyboard and mouse navigation.
52 #[default]
53 Regular,
54}
55
56/// Trait for a widget that takes part of focus handling.
57///
58/// When used for a simple widget implement
59/// - build()
60/// - focus()
61/// - area()
62///
63/// and optionally
64///
65/// - area_z() and navigable()
66///
67/// ```rust no_run
68/// use rat_focus::ratatui::layout::Rect;
69/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
70///
71/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
72///
73/// impl HasFocus for MyWidgetState {
74/// fn build(&self, builder: &mut FocusBuilder) {
75/// builder.leaf_widget(self);
76/// }
77///
78/// fn focus(&self) -> FocusFlag {
79/// self.focus.clone()
80/// }
81///
82/// fn area(&self) -> Rect {
83/// self.area
84/// }
85/// }
86/// ```
87///
88///
89/// When used for a container widget implement
90/// - build()
91/// ```rust no_run
92/// use rat_focus::ratatui::layout::Rect;
93/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
94///
95/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
96/// # impl HasFocus for MyWidgetState {
97/// # fn build(&self, builder: &mut FocusBuilder) {
98/// # builder.leaf_widget(self);
99/// # }
100/// #
101/// # fn focus(&self) -> FocusFlag {
102/// # self.focus.clone()
103/// # }
104/// #
105/// # fn area(&self) -> Rect {
106/// # self.area
107/// # }
108/// # }
109/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
110///
111/// impl HasFocus for SomeWidgetState {
112/// fn build(&self, builder: &mut FocusBuilder) {
113/// let tag = builder.start(self);
114/// builder.widget(&self.component_a);
115/// builder.widget(&self.component_b);
116/// builder.end(tag);
117/// }
118///
119/// fn focus(&self) -> FocusFlag {
120/// self.focus.clone()
121/// }
122///
123/// fn area(&self) -> Rect {
124/// self.area
125/// }
126/// }
127/// ```
128/// Creates a container with an identity.
129///
130/// Or
131/// ```rust no_run
132/// use rat_focus::ratatui::layout::Rect;
133/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
134///
135/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
136/// # impl HasFocus for MyWidgetState {
137/// # fn build(&self, builder: &mut FocusBuilder) {
138/// # builder.leaf_widget(self);
139/// # }
140/// #
141/// # fn focus(&self) -> FocusFlag {
142/// # self.focus.clone()
143/// # }
144/// #
145/// # fn area(&self) -> Rect {
146/// # self.area
147/// # }
148/// # }
149/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
150///
151/// impl HasFocus for SomeWidgetState {
152/// fn build(&self, builder: &mut FocusBuilder) {
153/// builder.widget(&self.component_a);
154/// builder.widget(&self.component_b);
155/// }
156///
157/// fn focus(&self) -> FocusFlag {
158/// unimplemented!("not in use")
159/// }
160///
161/// fn area(&self) -> Rect {
162/// unimplemented!("not in use")
163/// }
164/// }
165/// ```
166/// for an anonymous container.
167///
168/// focus(), area() and area_z() are only used for the first case.
169/// navigable() is ignored for containers, leave it at the default.
170///
171pub trait HasFocus {
172 /// Build the focus-structure for the container/widget.
173 fn build(&self, builder: &mut FocusBuilder);
174
175 /// Build the focus-structure for the container/widget.
176 ///
177 /// This function is called when the default navigation is
178 /// overridden by calling [FocusBuilder::widget_navigate].
179 /// You only need to implement this function, if you have
180 /// a container-widget, that wants to react to an
181 /// alternate navigation.
182 ///
183 /// For regular widgets this will be called too, but
184 /// the overridden flag will be used by Focus, regardless
185 /// of what you do. It's only useful to get a notification
186 /// of an alternate navigation.
187 ///
188 /// It defaults to calling build. If you don't have very
189 /// specific requirements, you need not concern with this;
190 /// just implement [HasFocus::build].
191 #[allow(unused_variables)]
192 fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
193 self.build(builder);
194 }
195
196 /// Access to the focus flag.
197 fn focus(&self) -> FocusFlag;
198
199 /// Provide a unique id for the widget.
200 fn id(&self) -> usize {
201 self.focus().widget_id()
202 }
203
204 /// Area for mouse focus.
205 ///
206 /// Generally, this area shouldn't overlap with other areas.
207 /// If it does, you can use `area_z()` to give an extra z-value
208 /// for mouse interactions. Default is 0, higher values mean
209 /// `above`.
210 /// If two areas with the same z overlap, the last one will
211 /// be used.
212 fn area(&self) -> ratatui::layout::Rect;
213
214 /// Z-value for the area.
215 ///
216 /// When testing for mouse interactions the z-value is taken into account.
217 fn area_z(&self) -> u16 {
218 0
219 }
220
221 /// Declares how the widget interacts with focus.
222 ///
223 /// Default is [Navigation::Regular].
224 fn navigable(&self) -> Navigation {
225 Navigation::Regular
226 }
227
228 /// Does this widget have the focus or if the flag is
229 /// used for a container, does any of widgets inside
230 /// the container have the focus.
231 ///
232 /// This flag is set by `Focus::handle`.
233 ///
234 /// See [match_focus!].
235 fn is_focused(&self) -> bool {
236 self.focus().get()
237 }
238
239 /// This widget just lost the focus.
240 ///
241 /// This flag is set by `Focus::handle` if there is a focus transfer,
242 /// and will be reset by the next call to `Focus::handle`.
243 ///
244 /// See [on_lost!].
245 fn lost_focus(&self) -> bool {
246 self.focus().lost()
247 }
248
249 /// This widget just gained the focus.
250 ///
251 /// This flag is set by `Focus::handle` if there is a focus transfer,
252 /// and will be reset by the next call to `Focus::handle`.
253 ///
254 /// See [on_gained!].
255 fn gained_focus(&self) -> bool {
256 self.focus().gained()
257 }
258
259 /// This widget is not beneath the mouse-cursor.
260 ///
261 /// Focus runs a hit-test with the current mouse position
262 /// for all the widgets. A widget is not obscured
263 /// if the widget area matches and there is no other
264 /// matching widget/container with a higher z-index.
265 ///
266 /// This flag is set when handling a mouse-event.
267 fn is_obscured(&self) -> bool {
268 self.focus().obscured()
269 }
270}
271
272/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
273/// the block is executed. This requires that `widget_state` implements [HasFocus],
274/// but that's the basic requirement for this whole crate.
275///
276/// ```rust ignore
277/// use rat_focus::on_lost;
278///
279/// on_lost!(
280/// state.field1 => {
281/// // do checks
282/// },
283/// state.field2 => {
284/// // do checks
285/// }
286/// );
287/// ```
288#[macro_export]
289macro_rules! on_lost {
290 ($($field:expr => $validate:expr),*) => {{
291 use $crate::HasFocus;
292 $(if $field.lost_focus() { _ = $validate })*
293 }};
294}
295
296/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
297/// the block is executed. This requires that `widget_state` implements [HasFocus],
298/// but that's the basic requirement for this whole crate.
299///
300/// ```rust ignore
301/// use rat_focus::on_gained;
302///
303/// on_gained!(
304/// state.field1 => {
305/// // do prep
306/// },
307/// state.field2 => {
308/// // do prep
309/// }
310/// );
311/// ```
312#[macro_export]
313macro_rules! on_gained {
314 ($($field:expr => $validate:expr),*) => {{
315 use $crate::HasFocus;
316 $(if $field.gained_focus() { _ = $validate })*
317 }};
318}
319
320/// Does a match on several fields and can return a result.
321/// Does a `widget_state.is_focused()` for each field and returns
322/// the first that is true. There is an `else` branch too.
323///
324/// This requires that `widget_state` implements [HasFocus],
325/// but that's the basic requirement for this whole crate.
326///
327/// ```rust ignore
328/// use rat_focus::match_focus;
329///
330/// let res = match_focus!(
331/// state.field1 => {
332/// // do this
333/// true
334/// },
335/// state.field2 => {
336/// // do that
337/// true
338/// },
339/// else => {
340/// false
341/// }
342/// );
343///
344/// if res {
345/// // react
346/// }
347/// ```
348///
349#[macro_export]
350macro_rules! match_focus {
351 ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
352 use $crate::HasFocus;
353 if false {
354 unreachable!();
355 }
356 $(else if $field.is_focused() { $block })*
357 $(else { $final })?
358 }};
359}
360
361/// Create the implementation of HasFocus for the
362/// given list of struct members.
363///
364/// Create a container with no identity.
365/// ```
366/// # use rat_focus::{impl_has_focus, FocusFlag};
367/// # struct MyState { field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
368/// impl_has_focus!(field1, field2, field3 for MyState);
369/// ```
370///
371/// Create a container with an identity.
372/// ```
373/// # use rat_focus::{impl_has_focus, FocusFlag};
374/// # struct MyState { container: FocusFlag, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
375/// impl_has_focus!(container: field1, field2, field3 for MyState);
376/// ```
377///
378/// Create a container with an identity and an area that will react to mouse clicks.
379/// ```
380/// # use rat_focus::ratatui::layout::Rect;
381/// # use rat_focus::{impl_has_focus, FocusFlag};
382/// # struct MyState { container: FocusFlag, area: Rect, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
383/// impl_has_focus!(container:area: field1, field2, field3 for MyState);
384/// ```
385#[macro_export]
386macro_rules! impl_has_focus {
387 ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
388 impl $crate::HasFocus for $ty {
389 fn build(&self, builder: &mut $crate::FocusBuilder) {
390 let tag = builder.start(self);
391 $(builder.widget(&self.$n);)*
392 builder.end(tag);
393 }
394
395 fn focus(&self) -> $crate::FocusFlag {
396 self.$cc.clone()
397 }
398
399 fn area(&self) -> $crate::ratatui::layout::Rect {
400 self.$area
401 }
402 }
403 };
404 ($cc:ident: $($n:ident),* for $ty:ty) => {
405 impl $crate::HasFocus for $ty {
406 fn build(&self, builder: &mut $crate::FocusBuilder) {
407 let tag = builder.start(self);
408 $(builder.widget(&self.$n);)*
409 builder.end(tag);
410 }
411
412 fn focus(&self) -> $crate::FocusFlag {
413 self.$cc.clone()
414 }
415
416 fn area(&self) -> $crate::ratatui::layout::Rect {
417 $crate::ratatui::layout::Rect::default()
418 }
419 }
420 };
421 ($($n:ident),* for $ty:ty) => {
422 impl $crate::HasFocus for $ty {
423 fn build(&self, builder: &mut $crate::FocusBuilder) {
424 $(builder.widget(&self.$n);)*
425 }
426
427 fn focus(&self) -> $crate::FocusFlag {
428 unimplemented!("not defined")
429 }
430
431 fn area(&self) -> $crate::ratatui::layout::Rect {
432 unimplemented!("not defined")
433 }
434 }
435 };
436}