rat_focus/lib.rs
1#![doc = include_str!("../readme.md")]
2
3mod focus;
4
5/// Reexport of types used by a macro.
6pub mod ratatui {
7 pub mod layout {
8 pub use ratatui_core::layout::Rect;
9 }
10}
11
12pub use crate::focus::{Focus, FocusBuilder, handle_focus};
13use std::cell::{Cell, RefCell};
14use std::fmt::{Debug, Display, Formatter};
15use std::hash::{Hash, Hasher};
16use std::ptr;
17use std::rc::Rc;
18
19/// Holds the flags for the focus.
20///
21/// Add this to the widget state and implement [HasFocus] to
22/// manage your widgets focus state.
23///
24/// __Note__
25///
26/// This struct is intended to be cloned and uses a Rc internally
27/// to share the state.
28///
29/// __Note__
30///
31/// Equality and Hash and the id() function use the memory address of the
32/// FocusFlag behind the internal Rc<>.
33///
34/// __See__
35/// [HasFocus], [on_gained!](crate::on_gained!) and
36/// [on_lost!](crate::on_lost!).
37///
38#[derive(Clone, Default)]
39pub struct FocusFlag(Rc<FocusFlagCore>);
40
41/// Equality for FocusFlag means pointer equality of the underlying
42/// Rc using Rc::ptr_eq.
43impl PartialEq for FocusFlag {
44 fn eq(&self, other: &Self) -> bool {
45 Rc::ptr_eq(&self.0, &other.0)
46 }
47}
48
49impl Eq for FocusFlag {}
50
51impl Hash for FocusFlag {
52 fn hash<H: Hasher>(&self, state: &mut H) {
53 ptr::hash(Rc::as_ptr(&self.0), state);
54 }
55}
56
57impl Display for FocusFlag {
58 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59 let name = self.0.name.borrow();
60 if let Some(name) = &*name {
61 write!(f, "|{}|", name)
62 } else {
63 write!(f, "")
64 }
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) -> ratatui::layout::Rect {
78 ratatui::layout::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#[derive(Default)]
91struct FocusFlagCore {
92 /// Field name for debugging purposes.
93 name: RefCell<Option<Box<str>>>,
94 /// Focus.
95 focus: Cell<bool>,
96 /// This widget just gained the focus. This flag is set by [Focus::handle]
97 /// if there is a focus transfer, and will be reset by the next
98 /// call to [Focus::handle].
99 ///
100 /// See [on_gained!](crate::on_gained!)
101 gained: Cell<bool>,
102 /// Callback for set of gained.
103 on_gained: RefCell<Option<Box<dyn Fn()>>>,
104 /// This widget just lost the focus. This flag is set by [Focus::handle]
105 /// if there is a focus transfer, and will be reset by the next
106 /// call to [Focus::handle].
107 ///
108 /// See [on_lost!](crate::on_lost!)
109 lost: Cell<bool>,
110 /// Callback for set of lost.
111 on_lost: RefCell<Option<Box<dyn Fn()>>>,
112}
113
114/// Focus navigation for widgets.
115///
116/// The effects that hinder focus-change (`Reach*`, `Lock`) only work
117/// when navigation changes via next()/prev()/focus_at().
118///
119/// Programmatic focus changes are always possible.
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121pub enum Navigation {
122 /// Widget is not reachable with normal keyboard or mouse navigation.
123 None,
124 /// Focus is locked to stay with this widget. No mouse or keyboard navigation
125 /// can change that.
126 Lock,
127 /// Widget is not reachable with keyboard navigation, but can be focused with the mouse.
128 Mouse,
129 /// Widget cannot be reached with normal keyboard navigation, but can be left.
130 /// (e.g. Tabs, Split-Divider)
131 Leave,
132 /// Widget can be reached with normal keyboard navigation, but not left.
133 /// (e.g. TextArea)
134 Reach,
135 /// Widget can be reached with normal keyboard navigation, but only be left with
136 /// backward navigation.
137 /// (e.g. some widget with internal structure)
138 ReachLeaveFront,
139 /// Widget can be reached with normal keyboard navigation, but only be left with
140 /// forward navigation.
141 /// (e.g. some widget with internal structure)
142 ReachLeaveBack,
143 /// Widget can be reached and left with normal keyboard navigation.
144 #[default]
145 Regular,
146}
147
148/// Trait for a widget that takes part of focus handling.
149///
150/// When used for a simple widget implement
151/// - build()
152/// - focus()
153/// - area()
154///
155/// and optionally
156///
157/// - area_z() and navigable()
158///
159/// ```rust no_run
160/// use rat_focus::ratatui::layout::Rect;
161/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
162///
163/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
164///
165/// impl HasFocus for MyWidgetState {
166/// fn build(&self, builder: &mut FocusBuilder) {
167/// builder.leaf_widget(self);
168/// }
169///
170/// fn focus(&self) -> FocusFlag {
171/// self.focus.clone()
172/// }
173///
174/// fn area(&self) -> Rect {
175/// self.area
176/// }
177/// }
178/// ```
179///
180///
181/// When used for a container widget implement
182/// - build()
183/// ```rust no_run
184/// use rat_focus::ratatui::layout::Rect;
185/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
186///
187/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
188/// # impl HasFocus for MyWidgetState {
189/// # fn build(&self, builder: &mut FocusBuilder) {
190/// # builder.leaf_widget(self);
191/// # }
192/// #
193/// # fn focus(&self) -> FocusFlag {
194/// # self.focus.clone()
195/// # }
196/// #
197/// # fn area(&self) -> Rect {
198/// # self.area
199/// # }
200/// # }
201/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
202///
203/// impl HasFocus for SomeWidgetState {
204/// fn build(&self, builder: &mut FocusBuilder) {
205/// let tag = builder.start(self);
206/// builder.widget(&self.component_a);
207/// builder.widget(&self.component_b);
208/// builder.end(tag);
209/// }
210///
211/// fn focus(&self) -> FocusFlag {
212/// self.focus.clone()
213/// }
214///
215/// fn area(&self) -> Rect {
216/// self.area
217/// }
218/// }
219/// ```
220/// Creates a container with an identity.
221///
222/// Or
223/// ```rust no_run
224/// use rat_focus::ratatui::layout::Rect;
225/// use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
226///
227/// struct MyWidgetState { pub focus: FocusFlag, pub area: Rect }
228/// # impl HasFocus for MyWidgetState {
229/// # fn build(&self, builder: &mut FocusBuilder) {
230/// # builder.leaf_widget(self);
231/// # }
232/// #
233/// # fn focus(&self) -> FocusFlag {
234/// # self.focus.clone()
235/// # }
236/// #
237/// # fn area(&self) -> Rect {
238/// # self.area
239/// # }
240/// # }
241/// struct SomeWidgetState { pub focus: FocusFlag, pub area: Rect, pub component_a: MyWidgetState, pub component_b: MyWidgetState }
242///
243/// impl HasFocus for SomeWidgetState {
244/// fn build(&self, builder: &mut FocusBuilder) {
245/// builder.widget(&self.component_a);
246/// builder.widget(&self.component_b);
247/// }
248///
249/// fn focus(&self) -> FocusFlag {
250/// unimplemented!("not in use")
251/// }
252///
253/// fn area(&self) -> Rect {
254/// unimplemented!("not in use")
255/// }
256/// }
257/// ```
258/// for an anonymous container.
259///
260/// focus(), area() and area_z() are only used for the first case.
261/// navigable() is ignored for containers, leave it at the default.
262///
263pub trait HasFocus {
264 /// Build the focus-structure for the container/widget.
265 fn build(&self, builder: &mut FocusBuilder);
266
267 /// Build the focus-structure for the container/widget.
268 /// This is called when the default navigation will be
269 /// overridden by the builder.
270 ///
271 /// It defaults to calling build and ignoring the navigable flag.
272 ///
273 /// You still have to implement build() for the baseline functionality.
274 /// This is just an extra.
275 #[allow(unused_variables)]
276 fn build_nav(&self, navigable: Navigation, builder: &mut FocusBuilder) {
277 self.build(builder);
278 }
279
280 /// Access to the flag for the rest.
281 fn focus(&self) -> FocusFlag;
282
283 /// Provide a unique id for the widget.
284 fn id(&self) -> usize {
285 self.focus().widget_id()
286 }
287
288 /// Area for mouse focus.
289 ///
290 /// This area shouldn't overlap with areas returned by other widgets.
291 /// If it does, the widget should use `area_z()` for clarification.
292 /// Otherwise, the areas are searched in order of addition.
293 fn area(&self) -> ratatui::layout::Rect;
294
295 /// Z value for the area.
296 ///
297 /// When testing for mouse interactions the z-value is taken into
298 /// account too.
299 fn area_z(&self) -> u16 {
300 0
301 }
302
303 /// Declares how the widget interacts with focus.
304 ///
305 /// Default is [Navigation::Regular].
306 fn navigable(&self) -> Navigation {
307 Navigation::Regular
308 }
309
310 /// Focused?
311 fn is_focused(&self) -> bool {
312 self.focus().get()
313 }
314
315 /// Just lost focus.
316 fn lost_focus(&self) -> bool {
317 self.focus().lost()
318 }
319
320 /// Just gained focus.
321 fn gained_focus(&self) -> bool {
322 self.focus().gained()
323 }
324}
325
326impl Debug for FocusFlag {
327 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
328 f.debug_struct("FocusFlag")
329 .field("name", &self.0.name)
330 .field("focus", &self.0.focus.get())
331 .field("widget_id", &self.widget_id())
332 .field("gained", &self.0.gained.get())
333 .field("on_gained", &self.0.on_gained.borrow().is_some())
334 .field("lost", &self.0.lost.get())
335 .field("on_lost", &self.0.on_lost.borrow().is_some())
336 .finish()
337 }
338}
339
340impl FocusFlag {
341 /// Create a default flag.
342 pub fn new() -> Self {
343 Self::default()
344 }
345
346 /// Create a deep copy of the FocusFlag.
347 ///
348 /// Caution
349 ///
350 /// It will lose the on_gained() and on_lost() callbacks.
351 /// Those can not be replicated/cloned as they will
352 /// most probably hold some Rc's to somewhere.
353 ///
354 /// You will need to set them anew.
355 pub fn new_instance(&self) -> Self {
356 Self(Rc::new(self.0.fake_clone()))
357 }
358
359 /// Return an identity value.
360 ///
361 /// This uses the memory address of the backing Rc so it will
362 /// be unique during the runtime but will be different for each
363 /// run.
364 pub fn widget_id(&self) -> usize {
365 Rc::as_ptr(&self.0) as usize
366 }
367
368 /// Create a named flag.
369 ///
370 /// The name is only used for debugging.
371 #[deprecated(
372 since = "1.4.0",
373 note = "to dangerous, use FocusFlag::new().with_name(..) or FocusFlag::fake_clone(..) for a clone."
374 )]
375 pub fn named(name: impl AsRef<str>) -> Self {
376 Self(Rc::new(FocusFlagCore::default().named(name.as_ref())))
377 }
378
379 /// Set a name for a FocusFlag.
380 pub fn with_name(self, name: &str) -> Self {
381 self.set_name(name);
382 self
383 }
384
385 /// Has the focus.
386 #[inline]
387 pub fn get(&self) -> bool {
388 self.0.focus.get()
389 }
390
391 /// Set the focus.
392 #[inline]
393 pub fn set(&self, focus: bool) {
394 self.0.focus.set(focus);
395 }
396
397 /// Get the field-name.
398 #[inline]
399 pub fn name(&self) -> Box<str> {
400 self.0.name.borrow().clone().unwrap_or_default()
401 }
402
403 /// Set the field-name.
404 #[inline]
405 pub fn set_name(&self, name: &str) {
406 *self.0.name.borrow_mut() = Some(Box::from(name))
407 }
408
409 /// Just lost the focus.
410 #[inline]
411 pub fn lost(&self) -> bool {
412 self.0.lost.get()
413 }
414
415 /// Set the lost-flag.
416 ///
417 /// This doesn't call the on_lost callback.
418 #[inline]
419 pub fn set_lost(&self, lost: bool) {
420 self.0.lost.set(lost);
421 }
422
423 /// Set an on_lost callback. The intention is that widget-creators
424 /// can use this to get guaranteed notifications on focus-changes.
425 ///
426 /// This is not an api for widget *users.
427 #[inline]
428 pub fn on_lost(&self, on_lost: impl Fn() + 'static) {
429 *(self.0.on_lost.borrow_mut()) = Some(Box::new(on_lost));
430 }
431
432 /// Notify an on_lost() tragedy.
433 #[inline]
434 pub fn call_on_lost(&self) {
435 let borrow = self.0.on_lost.borrow();
436 if let Some(f) = borrow.as_ref() {
437 f();
438 }
439 }
440
441 /// Just gained the focus.
442 #[inline]
443 pub fn gained(&self) -> bool {
444 self.0.gained.get()
445 }
446
447 /// Set the gained-flag.
448 ///
449 /// This doesn't call the on_gained callback.
450 #[inline]
451 pub fn set_gained(&self, gained: bool) {
452 self.0.gained.set(gained);
453 }
454
455 /// Set an on_gained callback. The intention is that widget-creators
456 /// can use this to get guaranteed notifications on focus-changes.
457 ///
458 /// This is not an api for widget *users.
459 #[inline]
460 pub fn on_gained(&self, on_gained: impl Fn() + 'static) {
461 *(self.0.on_gained.borrow_mut()) = Some(Box::new(on_gained));
462 }
463
464 /// Notify an on_gained() comedy.
465 #[inline]
466 pub fn call_on_gained(&self) {
467 let borrow = self.0.on_gained.borrow();
468 if let Some(f) = borrow.as_ref() {
469 f();
470 }
471 }
472
473 /// Reset all flags to false.
474 #[inline]
475 pub fn clear(&self) {
476 self.0.focus.set(false);
477 self.0.lost.set(false);
478 self.0.gained.set(false);
479 }
480}
481
482impl FocusFlagCore {
483 #[inline(always)]
484 pub(crate) fn named(self, name: &str) -> Self {
485 *self.name.borrow_mut() = Some(Box::from(name));
486 self
487 }
488
489 pub(crate) fn fake_clone(&self) -> Self {
490 Self {
491 name: self.name.clone(),
492 focus: Cell::new(self.focus.get()),
493 gained: Cell::new(self.gained.get()),
494 on_gained: RefCell::new(None),
495 lost: Cell::new(self.lost.get()),
496 on_lost: RefCell::new(None),
497 }
498 }
499}
500
501/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
502/// the block is executed. This requires that `widget_state` implements [HasFocus],
503/// but that's the basic requirement for this whole crate.
504///
505/// ```rust ignore
506/// use rat_focus::on_lost;
507///
508/// on_lost!(
509/// state.field1 => {
510/// // do checks
511/// },
512/// state.field2 => {
513/// // do checks
514/// }
515/// );
516/// ```
517#[macro_export]
518macro_rules! on_lost {
519 ($($field:expr => $validate:expr),*) => {{
520 use $crate::HasFocus;
521 $(if $field.lost_focus() { _ = $validate })*
522 }};
523}
524
525/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
526/// the block is executed. This requires that `widget_state` implements [HasFocus],
527/// but that's the basic requirement for this whole crate.
528///
529/// ```rust ignore
530/// use rat_focus::on_gained;
531///
532/// on_gained!(
533/// state.field1 => {
534/// // do prep
535/// },
536/// state.field2 => {
537/// // do prep
538/// }
539/// );
540/// ```
541#[macro_export]
542macro_rules! on_gained {
543 ($($field:expr => $validate:expr),*) => {{
544 use $crate::HasFocus;
545 $(if $field.gained_focus() { _ = $validate })*
546 }};
547}
548
549/// Does a match on several fields and can return a result.
550/// Does a `widget_state.is_focused()` for each field and returns
551/// the first that is true. There is an `else` branch too.
552///
553/// This requires that `widget_state` implements [HasFocus],
554/// but that's the basic requirement for this whole crate.
555///
556/// ```rust ignore
557/// use rat_focus::match_focus;
558///
559/// let res = match_focus!(
560/// state.field1 => {
561/// // do this
562/// true
563/// },
564/// state.field2 => {
565/// // do that
566/// true
567/// },
568/// else => {
569/// false
570/// }
571/// );
572///
573/// if res {
574/// // react
575/// }
576/// ```
577///
578#[macro_export]
579macro_rules! match_focus {
580 ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
581 use $crate::HasFocus;
582 if false {
583 unreachable!();
584 }
585 $(else if $field.is_focused() { $block })*
586 $(else { $final })?
587 }};
588}
589
590/// Create the implementation of HasFocus for the
591/// given list of struct members.
592///
593/// Create a container with no identity.
594/// ```
595/// # use rat_focus::{impl_has_focus, FocusFlag};
596/// # struct MyState { field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
597/// impl_has_focus!(field1, field2, field3 for MyState);
598/// ```
599///
600/// Create a container with an identity.
601/// ```
602/// # use rat_focus::{impl_has_focus, FocusFlag};
603/// # struct MyState { container: FocusFlag, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
604/// impl_has_focus!(container: field1, field2, field3 for MyState);
605/// ```
606///
607/// Create a container with an identity and an area that will react to mouse clicks.
608/// ```
609/// # use rat_focus::ratatui::layout::Rect;
610/// # use rat_focus::{impl_has_focus, FocusFlag};
611/// # struct MyState { container: FocusFlag, area: Rect, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
612/// impl_has_focus!(container:area: field1, field2, field3 for MyState);
613/// ```
614#[macro_export]
615macro_rules! impl_has_focus {
616 ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
617 impl $crate::HasFocus for $ty {
618 fn build(&self, builder: &mut $crate::FocusBuilder) {
619 let tag = builder.start(self);
620 $(builder.widget(&self.$n);)*
621 builder.end(tag);
622 }
623
624 fn focus(&self) -> $crate::FocusFlag {
625 self.$cc.clone()
626 }
627
628 fn area(&self) -> $crate::ratatui::layout::Rect {
629 self.$area
630 }
631 }
632 };
633 ($cc:ident: $($n:ident),* for $ty:ty) => {
634 impl $crate::HasFocus for $ty {
635 fn build(&self, builder: &mut $crate::FocusBuilder) {
636 let tag = builder.start(self);
637 $(builder.widget(&self.$n);)*
638 builder.end(tag);
639 }
640
641 fn focus(&self) -> $crate::FocusFlag {
642 self.$cc.clone()
643 }
644
645 fn area(&self) -> $crate::ratatui::layout::Rect {
646 $crate::ratatui::layout::Rect::default()
647 }
648 }
649 };
650 ($($n:ident),* for $ty:ty) => {
651 impl $crate::HasFocus for $ty {
652 fn build(&self, builder: &mut $crate::FocusBuilder) {
653 $(builder.widget(&self.$n);)*
654 }
655
656 fn focus(&self) -> $crate::FocusFlag {
657 unimplemented!("not defined")
658 }
659
660 fn area(&self) -> $crate::ratatui::layout::Rect {
661 unimplemented!("not defined")
662 }
663 }
664 };
665}