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