Reputation: 15375
The ratatui
library for building terminal apps has a widget for rendering rectangles on screen called Block
. https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html.
I would like to detect mouse click events on a Block when using ratatui with crossterm, i.e. invoke a callback when a click falls anywhere within the bounds of the block. I'm aware that this functionality isn't natively provided "out of the box" by either library, so I'm mostly asking
Upvotes: 2
Views: 838
Reputation: 886
Instead of doing this in the render callback, a better approach is to use some mutable state to store the most recent rendered area. Either impl Widget for &mut MyWidget
or impl StatefulWidget for MyWidget
(or &MyWidget
) is the approach to use there.
Prior to Ratatui 0.26, Widget was only implemented for Owned widgets, but since then, we added implementations for references to widgets, which makes retaining widgets between frames easier to do
impl Widget for &mut MyWidget {
fn render(self, area: Rect, buf &mut Buffer) {
self.last_area = area;
}
}
Then in your code later:
if my_widget.last_area.contains(Position::new(x, y)) { ... }
The benefit of doing it this way is that your rendering logic is just rendering, and your logic for handling behavior is outside of the render callback.
Nothing really stops you building retained mode concepts on top of Ratatui either. Something which is worth considering for mouse heavy apps is to store the mapping between widgets and locations in some secondary storage (e.g. a Vec<(WidgetName, Rect)>
) and updating that as part of the rendering process. This approach is compatible with the above (either StatefulWidget with some state value that has a copy of the screen mapping list or mutable ref Widgets with an Arc<Mutext>
or similar.
Upvotes: 0
Reputation: 15375
Because ratatui
is an immediate-mode rendering framework, the most natural way to do this is to detect mouse events in render()
and pass the events as a parameter to your widgets during the render loop:
pub struct MyWidget {
pub event: Option<MouseEvent>
}
Then in the implementation of your widget, you can check whether the event falls within the bounds that you're drawing:
impl Widget for MyWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
let Block = Block::default();
let target = block.inner(area);
if self.event.kind == MouseEventKind::Up(MouseButton::Left) &&
target.contains(Position::new(self.event.column, self.event.row)) {
// Handle click event
}
block.render(area, buf);
}
}
Unlike with a traditional retained-mode UI framework, it's standard to combine event handling code inline with drawing code in this manner.
Upvotes: 3