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