Focus handling for ratatui
This crate works by adding a FocusFlag to each widget's state.
Focus is used to build a list of all relevant FocusFlags. Focus holds references to all FocusFlags, in the order the widgets should be navigated. Additionally, it has the area of the widget for mouse interaction.
The focus list is constructed freshly after each render to account for any changed areas and other changes in the set of widgets.
The functions Focus::next()/Focus::prev() do the actual
navigation. They change the active FocusFlag and set flags for
focus-lost and focus-gained too.
A widget should implement HasFocusFlag for it's state, but this is not strictly necessary.
Event-Handling
Event-handling is implemented for crossterm and
uses only Tab/Backtab to navigate. Focus implements
HandleEvent
, and there are the functions handle_focus() and
handle_mouse_focus() too. All of these return Outcome::Changed
whenever something interesting happened.
There are a few complications, though:
This solution is a bit sub-par, admittedly.
If you are using rat-salsa
as your application framework, there is a queue for extra results
from event-handling. You can add the result of focus().handle()
directly to the queue, and everything's well.
If you already have your own framework, you might want something similar.
Mouse
Mouse support exists of course.
The trait HasFocusFlag has two methods to support this:
-
area() - Returns the area of the widget that should react to mouse-clicks. If you want to prevent mouse-focus for your widget, just return Rect::default().
-
z_areas() - Extends area(). Widgets can return a list of ZRect for mouse interaction. A ZRect is a Rect with an added z-index to handle overlapping areas. Those can occur whenever the widget renders a popup/overlay on top of other widgets.
If z_areas is used, area must return the union of all Rects. Area is used as fast filter, z_areas are used for the details.
This method is defaulted to return nothing which is good enough for most widgets.
Macros
There are the macros
on_lost,
on_gained and
match_focus
that ease the use of the focus-flags, providing a match like
syntax.
Composition
There is support for composite widgets too.
Use Focus::new_container() to create the list of widgets in
a container. This takes one extra ContainerFlag which creates a
summary of the individual FocusFlags of each widget. This way the
container widget has an answer to 'Does any of my widgets have
the focus?'. The container can also have its own area. If the
container area is clicked and not some specific widget, the first
widget in the container gets the focus.
Lost/Gained also work for the whole container.
The trait HasFocus indicates the existence of this behaviour.
Focus has a method add_container() for this too.
If you don't want the mouse-interaction, you can always use Rect::default() for the area.
Focus can handle recursive containers too.
FocusFlag and Focus
The FocusFlag internally uses Rc<Cell<>> for all the flags,
so it can be cheaply cloned and used separately from its origin
state struct. That way it is possible to hand out a mutable
reference to the state struct and a Focus if necessary. Which
is all the time, as you want to split up the event-handling
function sooner rather than later.
By cloning the FocusFlags, Focus needs no lifetime and could be stored for a longer period. It still would need and update for all involved areas and conditionally disabled widgets. As there is no widget tree or similar in ratatui there is no way to automagically do this, the sensible way is to drop the Focus at the end of event-handling and rebuild it anew next time.
HasFocusFlag
In addition to the before-mentioned methods there are
navigable()- The widget can indicate that it is not reachable with key navigation.is_focused(),lost_focus(),gained_focus()- These are useful when writing the application.