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