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/widget.
259    fn build(&self, builder: &mut FocusBuilder);
260
261    /// Build the focus-structure for the container/widget.
262    /// This is called when the default navigation will be
263    /// overridden by the builder.
264    ///
265    /// It defaults to calling build and ignoring the navigable flag.
266    ///
267    /// You still have to implement build() for the baseline functionality.
268    /// This is just an extra.
269    #[allow(unused_variables)]
270    fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
271        self.build(builder);
272    }
273
274    /// Access to the flag for the rest.
275    fn focus(&self) -> FocusFlag;
276
277    /// Provide a unique id for the widget.
278    fn id(&self) -> usize {
279        self.focus().widget_id()
280    }
281
282    /// Area for mouse focus.
283    ///
284    /// This area shouldn't overlap with areas returned by other widgets.
285    /// If it does, the widget should use `area_z()` for clarification.
286    /// Otherwise, the areas are searched in order of addition.
287    fn area(&self) -> Rect;
288
289    /// Z value for the area.
290    ///
291    /// When testing for mouse interactions the z-value is taken into
292    /// account too.
293    fn area_z(&self) -> u16 {
294        0
295    }
296
297    /// Declares how the widget interacts with focus.
298    ///
299    /// Default is [Navigation::Regular].
300    fn navigable(&self) -> Navigation {
301        Navigation::Regular
302    }
303
304    /// Focused?
305    fn is_focused(&self) -> bool {
306        self.focus().get()
307    }
308
309    /// Just lost focus.
310    fn lost_focus(&self) -> bool {
311        self.focus().lost()
312    }
313
314    /// Just gained focus.
315    fn gained_focus(&self) -> bool {
316        self.focus().gained()
317    }
318}
319
320impl Debug for FocusFlag {
321    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
322        f.debug_struct("FocusFlag")
323            .field("name", &self.0.name)
324            .field("focus", &self.0.focus.get())
325            .field("widget_id", &self.widget_id())
326            .field("gained", &self.0.gained.get())
327            .field("on_gained", &self.0.on_gained.borrow().is_some())
328            .field("lost", &self.0.lost.get())
329            .field("on_lost", &self.0.on_lost.borrow().is_some())
330            .finish()
331    }
332}
333
334impl FocusFlag {
335    /// Create a default flag.
336    pub fn new() -> Self {
337        Self::default()
338    }
339
340    /// Create a deep copy of the FocusFlag.
341    ///
342    /// Caution
343    ///
344    /// It will lose the on_gained() and on_lost() callbacks.
345    /// Those can not be replicated/cloned as they will
346    /// most probably hold some Rc's to somewhere.
347    ///
348    /// You will need to set them anew.
349    pub fn new_instance(&self) -> Self {
350        Self(Rc::new(self.0.fake_clone()))
351    }
352
353    /// Return an identity value.
354    ///
355    /// This uses the memory address of the backing Rc so it will
356    /// be unique during the runtime but will be different for each
357    /// run.
358    pub fn widget_id(&self) -> usize {
359        Rc::as_ptr(&self.0) as usize
360    }
361
362    /// Create a named flag.
363    ///
364    /// The name is only used for debugging.
365    #[deprecated(
366        since = "1.4.0",
367        note = "to dangerous, use FocusFlag::new().with_name(..) or FocusFlag::fake_clone(..) for a clone."
368    )]
369    pub fn named(name: impl AsRef<str>) -> Self {
370        Self(Rc::new(FocusFlagCore::default().named(name.as_ref())))
371    }
372
373    /// Set a name for a FocusFlag.
374    pub fn with_name(self, name: &str) -> Self {
375        self.set_name(name);
376        self
377    }
378
379    /// Has the focus.
380    #[inline]
381    pub fn get(&self) -> bool {
382        self.0.focus.get()
383    }
384
385    /// Set the focus.
386    #[inline]
387    pub fn set(&self, focus: bool) {
388        self.0.focus.set(focus);
389    }
390
391    /// Get the field-name.
392    #[inline]
393    pub fn name(&self) -> Box<str> {
394        self.0.name.borrow().clone().unwrap_or_default()
395    }
396
397    /// Set the field-name.
398    #[inline]
399    pub fn set_name(&self, name: &str) {
400        *self.0.name.borrow_mut() = Some(Box::from(name))
401    }
402
403    /// Just lost the focus.
404    #[inline]
405    pub fn lost(&self) -> bool {
406        self.0.lost.get()
407    }
408
409    /// Set the lost-flag.
410    ///
411    /// This doesn't call the on_lost callback.
412    #[inline]
413    pub fn set_lost(&self, lost: bool) {
414        self.0.lost.set(lost);
415    }
416
417    /// Set an on_lost callback. The intention is that widget-creators
418    /// can use this to get guaranteed notifications on focus-changes.
419    ///
420    /// This is not an api for widget *users.
421    #[inline]
422    pub fn on_lost(&self, on_lost: impl Fn() + 'static) {
423        *(self.0.on_lost.borrow_mut()) = Some(Box::new(on_lost));
424    }
425
426    /// Notify an on_lost() tragedy.
427    #[inline]
428    pub fn call_on_lost(&self) {
429        let borrow = self.0.on_lost.borrow();
430        if let Some(f) = borrow.as_ref() {
431            f();
432        }
433    }
434
435    /// Just gained the focus.
436    #[inline]
437    pub fn gained(&self) -> bool {
438        self.0.gained.get()
439    }
440
441    /// Set the gained-flag.
442    ///
443    /// This doesn't call the on_gained callback.
444    #[inline]
445    pub fn set_gained(&self, gained: bool) {
446        self.0.gained.set(gained);
447    }
448
449    /// Set an on_gained callback. The intention is that widget-creators
450    /// can use this to get guaranteed notifications on focus-changes.
451    ///
452    /// This is not an api for widget *users.
453    #[inline]
454    pub fn on_gained(&self, on_gained: impl Fn() + 'static) {
455        *(self.0.on_gained.borrow_mut()) = Some(Box::new(on_gained));
456    }
457
458    /// Notify an on_gained() comedy.
459    #[inline]
460    pub fn call_on_gained(&self) {
461        let borrow = self.0.on_gained.borrow();
462        if let Some(f) = borrow.as_ref() {
463            f();
464        }
465    }
466
467    /// Reset all flags to false.
468    #[inline]
469    pub fn clear(&self) {
470        self.0.focus.set(false);
471        self.0.lost.set(false);
472        self.0.gained.set(false);
473    }
474}
475
476impl FocusFlagCore {
477    #[inline(always)]
478    pub(crate) fn named(self, name: &str) -> Self {
479        *self.name.borrow_mut() = Some(Box::from(name));
480        self
481    }
482
483    pub(crate) fn fake_clone(&self) -> Self {
484        Self {
485            name: self.name.clone(),
486            focus: Cell::new(self.focus.get()),
487            gained: Cell::new(self.gained.get()),
488            on_gained: RefCell::new(None),
489            lost: Cell::new(self.lost.get()),
490            on_lost: RefCell::new(None),
491        }
492    }
493}
494
495/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
496/// the block is executed. This requires that `widget_state` implements [HasFocus],
497/// but that's the basic requirement for this whole crate.
498///
499/// ```rust ignore
500/// use rat_focus::on_lost;
501///
502/// on_lost!(
503///     state.field1 => {
504///         // do checks
505///     },
506///     state.field2 => {
507///         // do checks
508///     }
509/// );
510/// ```
511#[macro_export]
512macro_rules! on_lost {
513    ($($field:expr => $validate:expr),*) => {{
514        use $crate::HasFocus;
515        $(if $field.lost_focus() { _ = $validate })*
516    }};
517}
518
519/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
520/// the block is executed. This requires that `widget_state` implements [HasFocus],
521/// but that's the basic requirement for this whole crate.
522///
523/// ```rust ignore
524/// use rat_focus::on_gained;
525///
526/// on_gained!(
527///     state.field1 => {
528///         // do prep
529///     },
530///     state.field2 => {
531///         // do prep
532///     }
533/// );
534/// ```
535#[macro_export]
536macro_rules! on_gained {
537    ($($field:expr => $validate:expr),*) => {{
538        use $crate::HasFocus;
539        $(if $field.gained_focus() { _ = $validate })*
540    }};
541}
542
543/// Does a match on several fields and can return a result.
544/// Does a `widget_state.is_focused()` for each field and returns
545/// the first that is true. There is an `else` branch too.
546///
547/// This requires that `widget_state` implements [HasFocus],
548/// but that's the basic requirement for this whole crate.
549///
550/// ```rust ignore
551/// use rat_focus::match_focus;
552///
553/// let res = match_focus!(
554///     state.field1 => {
555///         // do this
556///         true
557///     },
558///     state.field2 => {
559///         // do that
560///         true
561///     },
562///     else => {
563///         false
564///     }
565/// );
566///
567/// if res {
568///     // react
569/// }
570/// ```
571///
572#[macro_export]
573macro_rules! match_focus {
574    ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
575        use $crate::HasFocus;
576        if false {
577            unreachable!();
578        }
579        $(else if $field.is_focused() { $block })*
580        $(else { $final })?
581    }};
582}
583
584/// Create the implementation of HasFocus for the
585/// given list of struct members.
586///
587/// Create a container with no identity.
588/// ```
589/// # use rat_focus::{impl_has_focus, FocusFlag};
590/// # struct MyState { field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
591/// impl_has_focus!(field1, field2, field3 for MyState);
592/// ```
593///
594/// Create a container with an identity.
595/// ```
596/// # use rat_focus::{impl_has_focus, FocusFlag};
597/// # struct MyState { container: FocusFlag, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
598/// impl_has_focus!(container: field1, field2, field3 for MyState);
599/// ```
600///
601/// Create a container with an identity and an area that will react to mouse clicks.
602/// ```
603/// # use ratatui::layout::Rect;
604/// # use rat_focus::{impl_has_focus, FocusFlag};
605/// # struct MyState { container: FocusFlag, area: Rect, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
606/// impl_has_focus!(container:area: field1, field2, field3 for MyState);
607/// ```
608#[macro_export]
609macro_rules! impl_has_focus {
610    ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
611        impl $crate::HasFocus for $ty {
612            fn build(&self, builder: &mut $crate::FocusBuilder) {
613                let tag = builder.start(self);
614                $(builder.widget(&self.$n);)*
615                builder.end(tag);
616            }
617
618            fn focus(&self) -> $crate::FocusFlag {
619                self.$cc.clone()
620            }
621
622            fn area(&self) -> ratatui::layout::Rect {
623                self.$area
624            }
625        }
626    };
627    ($cc:ident: $($n:ident),* for $ty:ty) => {
628        impl $crate::HasFocus for $ty {
629            fn build(&self, builder: &mut $crate::FocusBuilder) {
630                let tag = builder.start(self);
631                $(builder.widget(&self.$n);)*
632                builder.end(tag);
633            }
634
635            fn focus(&self) -> $crate::FocusFlag {
636                self.$cc.clone()
637            }
638
639            fn area(&self) -> ratatui::layout::Rect {
640                ratatui::layout::Rect::default()
641            }
642        }
643    };
644    ($($n:ident),* for $ty:ty) => {
645        impl $crate::HasFocus for $ty {
646            fn build(&self, builder: &mut $crate::FocusBuilder) {
647                $(builder.widget(&self.$n);)*
648            }
649
650            fn focus(&self) -> $crate::FocusFlag {
651                unimplemented!("not defined")
652            }
653
654            fn area(&self) -> ratatui::layout::Rect {
655                unimplemented!("not defined")
656            }
657        }
658    };
659}