rat_focus/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3mod focus;
4
5use ratatui::layout::Rect;
6use std::cell::Cell;
7use std::fmt::{Debug, Display, Formatter};
8use std::hash::{Hash, Hasher};
9use std::ptr;
10use std::rc::Rc;
11
12pub use crate::focus::{Focus, FocusBuilder, handle_focus};
13
14pub mod event {
15    pub use rat_event::*;
16
17    /// Special focus-traversal.
18    ///
19    /// There are some widgets that have special keys that
20    /// interact with focus. This marks an event-handler for
21    /// such a case and provides it with the valid Focus instance
22    /// for the application.
23    ///
24    /// eg:
25    /// - Leaving a textarea with some navigation key other than tab.
26    pub struct FocusTraversal<'a>(pub &'a crate::Focus);
27}
28
29/// Holds the flags for the focus.
30///
31/// Add this to the widget state.
32///
33/// This struct is intended to be cloned and uses a Rc internally
34/// to share the state.
35///
36/// __Attention__
37/// Equality for FocusFlag means pointer-equality of the underlying
38/// Rc using Rc::ptr_eq.
39///
40/// __See__
41/// [HasFocus], [on_gained!](crate::on_gained!) and
42/// [on_lost!](crate::on_lost!).
43///
44#[derive(Clone, Default)]
45pub struct FocusFlag(Rc<FocusFlagCore>);
46
47/// Equality for FocusFlag means pointer equality of the underlying
48/// Rc using Rc::ptr_eq.
49impl PartialEq for FocusFlag {
50    fn eq(&self, other: &Self) -> bool {
51        Rc::ptr_eq(&self.0, &other.0)
52    }
53}
54
55impl Eq for FocusFlag {}
56
57impl Hash for FocusFlag {
58    fn hash<H: Hasher>(&self, state: &mut H) {
59        ptr::hash(Rc::as_ptr(&self.0), state);
60    }
61}
62
63impl Display for FocusFlag {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        write!(f, "|{}|", self.0.name)
66    }
67}
68
69impl HasFocus for FocusFlag {
70    fn build(&self, builder: &mut FocusBuilder) {
71        builder.leaf_widget(self);
72    }
73
74    fn focus(&self) -> FocusFlag {
75        self.clone()
76    }
77
78    fn area(&self) -> Rect {
79        Rect::default()
80    }
81
82    fn area_z(&self) -> u16 {
83        0
84    }
85
86    fn navigable(&self) -> Navigation {
87        Navigation::Regular
88    }
89}
90
91// not Clone, always Rc<>
92#[derive(Default)]
93struct FocusFlagCore {
94    /// Field name for debugging purposes.
95    name: Box<str>,
96    /// Focus.
97    focus: Cell<bool>,
98    /// This widget just gained the focus. This flag is set by [Focus::handle]
99    /// if there is a focus transfer, and will be reset by the next
100    /// call to [Focus::handle].
101    ///
102    /// See [on_gained!](crate::on_gained!)
103    gained: Cell<bool>,
104    /// This widget just lost the focus. This flag is set by [Focus::handle]
105    /// if there is a focus transfer, and will be reset by the next
106    /// call to [Focus::handle].
107    ///
108    /// See [on_lost!](crate::on_lost!)
109    lost: Cell<bool>,
110}
111
112/// Focus navigation for widgets.
113///
114/// The effects that hinder focus-change (`Reach*`, `Lock`) only work
115/// when navigation changes via next()/prev()/focus_at().
116///
117/// Programmatic focus changes are always possible.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub enum Navigation {
120    /// Widget is not reachable with normal keyboard or mouse navigation.
121    None,
122    /// Focus is locked to stay with this widget. No mouse or keyboard navigation
123    /// can change that.
124    Lock,
125    /// Widget is not reachable with keyboard navigation, but can be focused with the mouse.
126    Mouse,
127    /// Widget cannot be reached with normal keyboard navigation, but can be left.
128    /// (e.g. Tabs, Split-Divider)
129    Leave,
130    /// Widget can be reached with normal keyboard navigation, but not left.
131    /// (e.g. TextArea)
132    Reach,
133    /// Widget can be reached with normal keyboard navigation, but only be left with
134    /// backward navigation.
135    /// (e.g. some widget with internal structure)
136    ReachLeaveFront,
137    /// Widget can be reached with normal keyboard navigation, but only be left with
138    /// forward navigation.
139    /// (e.g. some widget with internal structure)
140    ReachLeaveBack,
141    /// Widget can be reached and left with normal keyboard navigation.
142    #[default]
143    Regular,
144}
145
146/// Trait for a widget that takes part of focus handling.
147///
148/// When used for a simple widget implement
149/// - build()
150/// - focus()
151/// - area()
152///
153/// and optionally
154///
155/// - area_z() and navigable()
156///
157/// ```rust no_run
158/// use ratatui::layout::Rect;
159/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
160///
161/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
162///
163/// impl HasFocus for MyWidgetState {
164///     fn build(&self, builder: &mut FocusBuilder) {
165///         builder.leaf_widget(self);
166///     }
167///
168///     fn focus(&self) -> FocusFlag {
169///         self.focus.clone()
170///     }
171///
172///     fn area(&self) -> Rect {
173///         self.area
174///     }
175/// }
176/// ```
177///
178///
179/// When used for a container widget implement
180/// - build()
181/// ```rust no_run
182/// use ratatui::layout::Rect;
183/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
184///
185/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
186/// # impl HasFocus for MyWidgetState {
187/// #     fn build(&self, builder: &mut FocusBuilder) {
188/// #         builder.leaf_widget(self);
189/// #     }
190/// #
191/// #     fn focus(&self) -> FocusFlag {
192/// #         self.focus.clone()
193/// #     }
194/// #
195/// #     fn area(&self) -> Rect {
196/// #         self.area
197/// #     }
198/// # }
199/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
200///
201/// impl HasFocus for SomeWidgetState {
202///     fn build(&self, builder: &mut FocusBuilder) {
203///         let tag = builder.start(self);
204///         builder.widget(&self.component_a);
205///         builder.widget(&self.component_b);
206///         builder.end(tag);
207///     }
208///
209///     fn focus(&self) -> FocusFlag {
210///         self.focus.clone()
211///     }
212///
213///     fn area(&self) -> Rect {
214///         self.area
215///     }
216/// }
217/// ```
218/// Creates a container with an identity.
219///
220/// Or
221/// ```rust no_run
222/// use ratatui::layout::Rect;
223/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
224///
225/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
226/// # impl HasFocus for MyWidgetState {
227/// #     fn build(&self, builder: &mut FocusBuilder) {
228/// #         builder.leaf_widget(self);
229/// #     }
230/// #
231/// #     fn focus(&self) -> FocusFlag {
232/// #         self.focus.clone()
233/// #     }
234/// #
235/// #     fn area(&self) -> Rect {
236/// #         self.area
237/// #     }
238/// # }
239/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
240///
241/// impl HasFocus for SomeWidgetState {
242///     fn build(&self, builder: &mut FocusBuilder) {
243///         builder.widget(&self.component_a);
244///         builder.widget(&self.component_b);
245///     }
246///
247///     fn focus(&self) -> FocusFlag {
248///         unimplemented!("not in use")
249///     }
250///
251///     fn area(&self) -> Rect {
252///         unimplemented!("not in use")
253///     }
254/// }
255/// ```
256/// for an anonymous container.
257///
258/// focus(), area() and area_z() are only used for the first case.
259/// navigable() is ignored for containers, leave it at the default.
260///
261pub trait HasFocus {
262    /// Build the focus-structure for the container.
263    fn build(&self, builder: &mut FocusBuilder);
264
265    /// Access to the flag for the rest.
266    fn focus(&self) -> FocusFlag;
267
268    /// Provide a unique id for the widget.
269    fn id(&self) -> usize {
270        self.focus().widget_id()
271    }
272
273    /// Area for mouse focus.
274    ///
275    /// This area shouldn't overlap with areas returned by other widgets.
276    /// If it does, the widget should use `area_z()` for clarification.
277    /// Otherwise, the areas are searched in order of addition.
278    fn area(&self) -> Rect;
279
280    /// Z value for the area.
281    ///
282    /// When testing for mouse interactions the z-value is taken into
283    /// account too.
284    fn area_z(&self) -> u16 {
285        0
286    }
287
288    /// Declares how the widget interacts with focus.
289    ///
290    /// Default is [Navigation::Regular].
291    fn navigable(&self) -> Navigation {
292        Navigation::Regular
293    }
294
295    /// Focused?
296    fn is_focused(&self) -> bool {
297        self.focus().get()
298    }
299
300    /// Just lost focus.
301    fn lost_focus(&self) -> bool {
302        self.focus().lost()
303    }
304
305    /// Just gained focus.
306    fn gained_focus(&self) -> bool {
307        self.focus().gained()
308    }
309}
310
311impl Debug for FocusFlag {
312    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct("FocusFlag")
314            .field("name", &self.name())
315            .field("focus", &self.get())
316            .field("gained", &self.gained())
317            .field("lost", &self.lost())
318            .finish()
319    }
320}
321
322impl FocusFlag {
323    /// Create a default flag.
324    pub fn new() -> Self {
325        Self::default()
326    }
327
328    /// Return an identity value.
329    ///
330    /// This uses the memory address of the backing Rc so it will
331    /// be unique during the runtime but will be different for each
332    /// run.
333    pub fn widget_id(&self) -> usize {
334        Rc::as_ptr(&self.0) as usize
335    }
336
337    /// Create a named flag.
338    ///
339    /// The name is only used for debugging.
340    pub fn named(name: &str) -> Self {
341        Self(Rc::new(FocusFlagCore::named(name)))
342    }
343
344    /// Has the focus.
345    #[inline]
346    pub fn get(&self) -> bool {
347        self.0.focus.get()
348    }
349
350    /// Set the focus.
351    #[inline]
352    pub fn set(&self, focus: bool) {
353        self.0.focus.set(focus);
354    }
355
356    /// Get the field-name.
357    #[inline]
358    pub fn name(&self) -> &str {
359        self.0.name.as_ref()
360    }
361
362    /// Just lost the focus.
363    #[inline]
364    pub fn lost(&self) -> bool {
365        self.0.lost.get()
366    }
367
368    #[inline]
369    pub fn set_lost(&self, lost: bool) {
370        self.0.lost.set(lost);
371    }
372
373    /// Just gained the focus.
374    #[inline]
375    pub fn gained(&self) -> bool {
376        self.0.gained.get()
377    }
378
379    #[inline]
380    pub fn set_gained(&self, gained: bool) {
381        self.0.gained.set(gained);
382    }
383
384    /// Reset all flags to false.
385    #[inline]
386    pub fn clear(&self) {
387        self.0.focus.set(false);
388        self.0.lost.set(false);
389        self.0.gained.set(false);
390    }
391}
392
393impl FocusFlagCore {
394    pub(crate) fn named(name: &str) -> Self {
395        Self {
396            name: name.into(),
397            focus: Cell::new(false),
398            gained: Cell::new(false),
399            lost: Cell::new(false),
400        }
401    }
402}
403
404/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
405/// the block is executed. This requires that `widget_state` implements [HasFocus],
406/// but that's the basic requirement for this whole crate.
407///
408/// ```rust ignore
409/// use rat_focus::on_lost;
410///
411/// on_lost!(
412///     state.field1 => {
413///         // do checks
414///     },
415///     state.field2 => {
416///         // do checks
417///     }
418/// );
419/// ```
420#[macro_export]
421macro_rules! on_lost {
422    ($($field:expr => $validate:expr),*) => {{
423        use $crate::HasFocus;
424        $(if $field.lost_focus() { _ = $validate })*
425    }};
426}
427
428/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
429/// the block is executed. This requires that `widget_state` implements [HasFocus],
430/// but that's the basic requirement for this whole crate.
431///
432/// ```rust ignore
433/// use rat_focus::on_gained;
434///
435/// on_gained!(
436///     state.field1 => {
437///         // do prep
438///     },
439///     state.field2 => {
440///         // do prep
441///     }
442/// );
443/// ```
444#[macro_export]
445macro_rules! on_gained {
446    ($($field:expr => $validate:expr),*) => {{
447        use $crate::HasFocus;
448        $(if $field.gained_focus() { _ = $validate })*
449    }};
450}
451
452/// Does a match on the state struct of a widget. If
453/// `widget_state.is_focused()` is true the block is executed.
454/// There is a `_` branch too, that is evaluated if none of the
455/// given widget-states has the focus.
456///
457/// This requires that `widget_state` implements [HasFocus],
458/// but that's the basic requirement for this whole crate.
459///
460/// ```rust ignore
461/// use rat_focus::match_focus;
462///
463/// let res = match_focus!(
464///     state.field1 => {
465///         // do this
466///         true
467///     },
468///     state.field2 => {
469///         // do that
470///         true
471///     },
472///     _ => {
473///         false
474///     }
475/// );
476///
477/// if res {
478///     // react
479/// }
480/// ```
481///
482#[macro_export]
483macro_rules! match_focus {
484    ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
485        use $crate::HasFocus;
486        if false {
487            unreachable!();
488        }
489        $(else if $field.is_focused() { $block })*
490        $(else { $final })?
491    }};
492}
493
494/// Create the implementation of HasFocus for the
495/// given list of struct members.
496#[macro_export]
497macro_rules! impl_has_focus {
498    ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
499        impl $crate::HasFocus for $ty {
500            fn build(&self, builder: &mut $crate::FocusBuilder) {
501                let tag = builder.start(self);
502                $(builder.widget(&self.$n);)*
503                builder.end(tag);
504            }
505
506            fn focus(&self) -> $crate::FocusFlag {
507                self.$cc.clone()
508            }
509
510            fn area(&self) -> Rect {
511                self.$area
512            }
513        }
514    };
515    ($cc:ident: $($n:ident),* for $ty:ty) => {
516        impl $crate::HasFocus for $ty {
517            fn build(&self, builder: &mut $crate::FocusBuilder) {
518                let tag = builder.start(self);
519                $(builder.widget(&self.$n);)*
520                builder.end(tag);
521            }
522
523            fn focus(&self) -> $crate::FocusFlag {
524                self.$cc.clone()
525            }
526
527            fn area(&self) -> Rect {
528                Rect::default()
529            }
530        }
531    };
532    ($($n:ident),* for $ty:ty) => {
533        impl $crate::HasFocus for $ty {
534            fn build(&self, builder: &mut $crate::FocusBuilder) {
535                $(builder.widget(&self.$n);)*
536            }
537
538            fn focus(&self) -> $crate::FocusFlag {
539                unimplemented!("not defined")
540            }
541
542            fn area(&self) -> Rect {
543                unimplemented!("not defined")
544            }
545        }
546    };
547}