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}