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.
259 fn build(&self, builder: &mut FocusBuilder);
260
261 /// Access to the flag for the rest.
262 fn focus(&self) -> FocusFlag;
263
264 /// Provide a unique id for the widget.
265 fn id(&self) -> usize {
266 self.focus().widget_id()
267 }
268
269 /// Area for mouse focus.
270 ///
271 /// This area shouldn't overlap with areas returned by other widgets.
272 /// If it does, the widget should use `area_z()` for clarification.
273 /// Otherwise, the areas are searched in order of addition.
274 fn area(&self) -> Rect;
275
276 /// Z value for the area.
277 ///
278 /// When testing for mouse interactions the z-value is taken into
279 /// account too.
280 fn area_z(&self) -> u16 {
281 0
282 }
283
284 /// Declares how the widget interacts with focus.
285 ///
286 /// Default is [Navigation::Regular].
287 fn navigable(&self) -> Navigation {
288 Navigation::Regular
289 }
290
291 /// Focused?
292 fn is_focused(&self) -> bool {
293 self.focus().get()
294 }
295
296 /// Just lost focus.
297 fn lost_focus(&self) -> bool {
298 self.focus().lost()
299 }
300
301 /// Just gained focus.
302 fn gained_focus(&self) -> bool {
303 self.focus().gained()
304 }
305}
306
307impl Debug for FocusFlag {
308 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
309 f.debug_struct("FocusFlag")
310 .field("name", &self.0.name)
311 .field("focus", &self.0.focus.get())
312 .field("widget_id", &self.widget_id())
313 .field("gained", &self.0.gained.get())
314 .field("on_gained", &self.0.on_gained.borrow().is_some())
315 .field("lost", &self.0.lost.get())
316 .field("on_lost", &self.0.on_lost.borrow().is_some())
317 .finish()
318 }
319}
320
321impl FocusFlag {
322 /// Create a default flag.
323 pub fn new() -> Self {
324 Self::default()
325 }
326
327 /// Create a deep copy of the FocusFlag.
328 ///
329 /// Caution
330 ///
331 /// It will lose the on_gained() and on_lost() callbacks.
332 /// Those can not be replicated/cloned as they will
333 /// most probably hold some Rc's to somewhere.
334 ///
335 /// You will need to set them anew.
336 pub fn new_instance(&self) -> Self {
337 Self(Rc::new(self.0.fake_clone()))
338 }
339
340 /// Return an identity value.
341 ///
342 /// This uses the memory address of the backing Rc so it will
343 /// be unique during the runtime but will be different for each
344 /// run.
345 pub fn widget_id(&self) -> usize {
346 Rc::as_ptr(&self.0) as usize
347 }
348
349 /// Create a named flag.
350 ///
351 /// The name is only used for debugging.
352 #[deprecated(
353 since = "1.4.0",
354 note = "to dangerous, use FocusFlag::new().with_name(..) or FocusFlag::fake_clone(..) for a clone."
355 )]
356 pub fn named(name: impl AsRef<str>) -> Self {
357 Self(Rc::new(FocusFlagCore::default().named(name.as_ref())))
358 }
359
360 /// Set a name for a FocusFlag.
361 pub fn with_name(self, name: &str) -> Self {
362 self.set_name(name);
363 self
364 }
365
366 /// Has the focus.
367 #[inline]
368 pub fn get(&self) -> bool {
369 self.0.focus.get()
370 }
371
372 /// Set the focus.
373 #[inline]
374 pub fn set(&self, focus: bool) {
375 self.0.focus.set(focus);
376 }
377
378 /// Get the field-name.
379 #[inline]
380 pub fn name(&self) -> Box<str> {
381 self.0.name.borrow().clone().unwrap_or(Default::default())
382 }
383
384 /// Set the field-name.
385 #[inline]
386 pub fn set_name(&self, name: &str) {
387 *self.0.name.borrow_mut() = Some(Box::from(name))
388 }
389
390 /// Just lost the focus.
391 #[inline]
392 pub fn lost(&self) -> bool {
393 self.0.lost.get()
394 }
395
396 /// Set the lost-flag.
397 ///
398 /// This doesn't call the on_lost callback.
399 #[inline]
400 pub fn set_lost(&self, lost: bool) {
401 self.0.lost.set(lost);
402 }
403
404 /// Set an on_lost callback. The intention is that widget-creators
405 /// can use this to get guaranteed notifications on focus-changes.
406 ///
407 /// This is not an api for widget *users.
408 #[inline]
409 pub fn on_lost(&self, on_lost: impl Fn() + 'static) {
410 *(self.0.on_lost.borrow_mut()) = Some(Box::new(on_lost));
411 }
412
413 /// Notify an on_lost() tragedy.
414 #[inline]
415 pub fn call_on_lost(&self) {
416 let borrow = self.0.on_lost.borrow();
417 if let Some(f) = borrow.as_ref() {
418 f();
419 }
420 }
421
422 /// Just gained the focus.
423 #[inline]
424 pub fn gained(&self) -> bool {
425 self.0.gained.get()
426 }
427
428 /// Set the gained-flag.
429 ///
430 /// This doesn't call the on_gained callback.
431 #[inline]
432 pub fn set_gained(&self, gained: bool) {
433 self.0.gained.set(gained);
434 }
435
436 /// Set an on_gained callback. The intention is that widget-creators
437 /// can use this to get guaranteed notifications on focus-changes.
438 ///
439 /// This is not an api for widget *users.
440 #[inline]
441 pub fn on_gained(&self, on_gained: impl Fn() + 'static) {
442 *(self.0.on_gained.borrow_mut()) = Some(Box::new(on_gained));
443 }
444
445 /// Notify an on_gained() comedy.
446 #[inline]
447 pub fn call_on_gained(&self) {
448 let borrow = self.0.on_gained.borrow();
449 if let Some(f) = borrow.as_ref() {
450 f();
451 }
452 }
453
454 /// Reset all flags to false.
455 #[inline]
456 pub fn clear(&self) {
457 self.0.focus.set(false);
458 self.0.lost.set(false);
459 self.0.gained.set(false);
460 }
461}
462
463impl FocusFlagCore {
464 #[inline(always)]
465 pub(crate) fn named(self, name: &str) -> Self {
466 *self.name.borrow_mut() = Some(Box::from(name));
467 self
468 }
469
470 pub(crate) fn fake_clone(&self) -> Self {
471 Self {
472 name: self.name.clone(),
473 focus: Cell::new(self.focus.get()),
474 gained: Cell::new(self.gained.get()),
475 on_gained: RefCell::new(None),
476 lost: Cell::new(self.lost.get()),
477 on_lost: RefCell::new(None),
478 }
479 }
480}
481
482/// Does a match on the state struct of a widget. If `widget_state.lost_focus()` is true
483/// the block is executed. This requires that `widget_state` implements [HasFocus],
484/// but that's the basic requirement for this whole crate.
485///
486/// ```rust ignore
487/// use rat_focus::on_lost;
488///
489/// on_lost!(
490/// state.field1 => {
491/// // do checks
492/// },
493/// state.field2 => {
494/// // do checks
495/// }
496/// );
497/// ```
498#[macro_export]
499macro_rules! on_lost {
500 ($($field:expr => $validate:expr),*) => {{
501 use $crate::HasFocus;
502 $(if $field.lost_focus() { _ = $validate })*
503 }};
504}
505
506/// Does a match on the state struct of a widget. If `widget_state.gained_focus()` is true
507/// the block is executed. This requires that `widget_state` implements [HasFocus],
508/// but that's the basic requirement for this whole crate.
509///
510/// ```rust ignore
511/// use rat_focus::on_gained;
512///
513/// on_gained!(
514/// state.field1 => {
515/// // do prep
516/// },
517/// state.field2 => {
518/// // do prep
519/// }
520/// );
521/// ```
522#[macro_export]
523macro_rules! on_gained {
524 ($($field:expr => $validate:expr),*) => {{
525 use $crate::HasFocus;
526 $(if $field.gained_focus() { _ = $validate })*
527 }};
528}
529
530/// Does a match on several fields and can return a result.
531/// Does a `widget_state.is_focused()` for each field and returns
532/// the first that is true. There is an `else` branch too.
533///
534/// This requires that `widget_state` implements [HasFocus],
535/// but that's the basic requirement for this whole crate.
536///
537/// ```rust ignore
538/// use rat_focus::match_focus;
539///
540/// let res = match_focus!(
541/// state.field1 => {
542/// // do this
543/// true
544/// },
545/// state.field2 => {
546/// // do that
547/// true
548/// },
549/// else => {
550/// false
551/// }
552/// );
553///
554/// if res {
555/// // react
556/// }
557/// ```
558///
559#[macro_export]
560macro_rules! match_focus {
561 ($($field:expr => $block:expr),* $(, else => $final:expr)?) => {{
562 use $crate::HasFocus;
563 if false {
564 unreachable!();
565 }
566 $(else if $field.is_focused() { $block })*
567 $(else { $final })?
568 }};
569}
570
571/// Create the implementation of HasFocus for the
572/// given list of struct members.
573///
574/// Create a container with no identity.
575/// ```
576/// # use rat_focus::{impl_has_focus, FocusFlag};
577/// # struct MyState { field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
578/// impl_has_focus!(field1, field2, field3 for MyState);
579/// ```
580///
581/// Create a container with an identity.
582/// ```
583/// # use rat_focus::{impl_has_focus, FocusFlag};
584/// # struct MyState { container: FocusFlag, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
585/// impl_has_focus!(container: field1, field2, field3 for MyState);
586/// ```
587///
588/// Create a container with an identity and an area that will react to mouse clicks.
589/// ```
590/// # use ratatui::layout::Rect;
591/// # use rat_focus::{impl_has_focus, FocusFlag};
592/// # struct MyState { container: FocusFlag, area: Rect, field1: FocusFlag, field2: FocusFlag, field3: FocusFlag }
593/// impl_has_focus!(container:area: field1, field2, field3 for MyState);
594/// ```
595#[macro_export]
596macro_rules! impl_has_focus {
597 ($cc:ident:$area:ident: $($n:ident),* for $ty:ty) => {
598 impl $crate::HasFocus for $ty {
599 fn build(&self, builder: &mut $crate::FocusBuilder) {
600 let tag = builder.start(self);
601 $(builder.widget(&self.$n);)*
602 builder.end(tag);
603 }
604
605 fn focus(&self) -> $crate::FocusFlag {
606 self.$cc.clone()
607 }
608
609 fn area(&self) -> ratatui::layout::Rect {
610 self.$area
611 }
612 }
613 };
614 ($cc:ident: $($n:ident),* for $ty:ty) => {
615 impl $crate::HasFocus for $ty {
616 fn build(&self, builder: &mut $crate::FocusBuilder) {
617 let tag = builder.start(self);
618 $(builder.widget(&self.$n);)*
619 builder.end(tag);
620 }
621
622 fn focus(&self) -> $crate::FocusFlag {
623 self.$cc.clone()
624 }
625
626 fn area(&self) -> ratatui::layout::Rect {
627 ratatui::layout::Rect::default()
628 }
629 }
630 };
631 ($($n:ident),* for $ty:ty) => {
632 impl $crate::HasFocus for $ty {
633 fn build(&self, builder: &mut $crate::FocusBuilder) {
634 $(builder.widget(&self.$n);)*
635 }
636
637 fn focus(&self) -> $crate::FocusFlag {
638 unimplemented!("not defined")
639 }
640
641 fn area(&self) -> ratatui::layout::Rect {
642 unimplemented!("not defined")
643 }
644 }
645 };
646}