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