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}