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