rat_focus/
lib.rs

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