Skip to main content

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.
229    /// Or, if the flag is used for a container, does any of
230    /// widget inside the container have the focus.
231    ///
232    /// This flag is set by [Focus::handle].
233    fn is_focused(&self) -> bool {
234        self.focus().get()
235    }
236
237    /// This widget just lost the focus. This flag is set by [Focus::handle]
238    /// if there is a focus transfer, and will be reset by the next
239    /// call to [Focus::handle].
240    ///
241    /// See [on_lost!](crate::on_lost!)
242    fn lost_focus(&self) -> bool {
243        self.focus().lost()
244    }
245
246    /// This widget just gained the focus. This flag is set by [Focus::handle]
247    /// if there is a focus transfer, and will be reset by the next
248    /// call to [Focus::handle].
249    ///
250    /// See [on_gained!](crate::on_gained!)
251    fn gained_focus(&self) -> bool {
252        self.focus().gained()
253    }
254
255    /// This flag is set by [Focus::handle], if a mouse-event
256    /// matches one of the areas associated with a widget.
257    ///
258    /// > It searches all containers for an area-match. All
259    /// matching areas will have the flag set.
260    /// If an area with a higher z is found, all previously
261    /// found areas are discarded.
262    ///
263    /// > The z value for the last container is taken as a baseline.
264    /// Only widgets with a z greater or equal are considered.
265    /// If multiple widget areas are matching, the last one
266    /// will get the flag set.
267    ///
268    /// This rules enable popup-windows with complex ui's.
269    /// The popup-container starts with a z=1 and all widgets
270    /// within also get the same z. With the given rules, all
271    /// widgets underneath the popup are ignored.
272    ///
273    /// * This flag starts with a default `true`. This allows
274    ///   widgets to work, even if Focus is not used.
275    /// * Mouse drag events are not bound to any area.
276    ///   Instead, they set the mouse-focus to true for all
277    ///   widgets and containers.
278    fn has_mouse_focus(&self) -> bool {
279        self.focus().mouse_focus()
280    }
281}
282
283/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
284/// the block is executed. This requires that `widget_state` implements [HasFocus],
285/// but that's the basic requirement for this whole crate.
286///
287/// ```rust ignore
288/// use rat_focus::on_lost;
289///
290/// on_lost!(
291///     state.field1 => {
292///         // do checks
293///     },
294///     state.field2 => {
295///         // do checks
296///     }
297/// );
298/// ```
299#[macro_export]
300macro_rules! on_lost {
301    ($($field:expr => $validate:expr),*) => {{
302        use $crate::HasFocus;
303        $(if $field.lost_focus() { _ = $validate })*
304    }};
305}
306
307/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
308/// the block is executed. This requires that `widget_state` implements [HasFocus],
309/// but that's the basic requirement for this whole crate.
310///
311/// ```rust ignore
312/// use rat_focus::on_gained;
313///
314/// on_gained!(
315///     state.field1 => {
316///         // do prep
317///     },
318///     state.field2 => {
319///         // do prep
320///     }
321/// );
322/// ```
323#[macro_export]
324macro_rules! on_gained {
325    ($($field:expr => $validate:expr),*) => {{
326        use $crate::HasFocus;
327        $(if $field.gained_focus() { _ = $validate })*
328    }};
329}
330
331/// Does a match on several fields and can return a result.
332/// Does a `widget_state.is_focused()` for each field and returns
333/// the first that is true. There is an `else` branch too.
334///
335/// This requires that `widget_state` implements [HasFocus],
336/// but that's the basic requirement for this whole crate.
337///
338/// ```rust ignore
339/// use rat_focus::match_focus;
340///
341/// let res = match_focus!(
342///     state.field1 => {
343///         // do this
344///         true
345///     },
346///     state.field2 => {
347///         // do that
348///         true
349///     },
350///     else => {
351///         false
352///     }
353/// );
354///
355/// if res {
356///     // react
357/// }
358/// ```
359///
360#[macro_export]
361macro_rules! match_focus {
362    ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
363        use $crate::HasFocus;
364        if false {
365            unreachable!();
366        }
367        $(else if $field.is_focused() { $block })*
368        $(else { $final })?
369    }};
370}
371
372/// Create the implementation of HasFocus for the
373/// given list of struct members.
374///
375/// Create a container with no identity.
376/// ```
377/// # use rat_focus::{impl_has_focus, FocusFlag};
378/// # struct MyState { field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
379/// impl_has_focus!(field1, field2, field3 for MyState);
380/// ```
381///
382/// Create a container with an identity.
383/// ```
384/// # use rat_focus::{impl_has_focus, FocusFlag};
385/// # struct MyState { container: FocusFlag, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
386/// impl_has_focus!(container: field1, field2, field3 for MyState);
387/// ```
388///
389/// Create a container with an identity and an area that will react to mouse clicks.
390/// ```
391/// # use rat_focus::ratatui::layout::Rect;
392/// # use rat_focus::{impl_has_focus, FocusFlag};
393/// # struct MyState { container: FocusFlag, area: Rect, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
394/// impl_has_focus!(container:area: field1, field2, field3 for MyState);
395/// ```
396#[macro_export]
397macro_rules! impl_has_focus {
398    ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
399        impl $crate::HasFocus for $ty {
400            fn build(&self, builder: &mut $crate::FocusBuilder) {
401                let tag = builder.start(self);
402                $(builder.widget(&self.$n);)*
403                builder.end(tag);
404            }
405
406            fn focus(&self) -> $crate::FocusFlag {
407                self.$cc.clone()
408            }
409
410            fn area(&self) -> $crate::ratatui::layout::Rect {
411                self.$area
412            }
413        }
414    };
415    ($cc:ident: $($n:ident),* for $ty:ty) => {
416        impl $crate::HasFocus for $ty {
417            fn build(&self, builder: &mut $crate::FocusBuilder) {
418                let tag = builder.start(self);
419                $(builder.widget(&self.$n);)*
420                builder.end(tag);
421            }
422
423            fn focus(&self) -> $crate::FocusFlag {
424                self.$cc.clone()
425            }
426
427            fn area(&self) -> $crate::ratatui::layout::Rect {
428                $crate::ratatui::layout::Rect::default()
429            }
430        }
431    };
432    ($($n:ident),* for $ty:ty) => {
433        impl $crate::HasFocus for $ty {
434            fn build(&self, builder: &mut $crate::FocusBuilder) {
435                $(builder.widget(&self.$n);)*
436            }
437
438            fn focus(&self) -> $crate::FocusFlag {
439                unimplemented!("not defined")
440            }
441
442            fn area(&self) -> $crate::ratatui::layout::Rect {
443                unimplemented!("not defined")
444            }
445        }
446    };
447}