pub struct Interaction<Id> { /* private fields */ }Expand description
Ties Pointer, HitTester, and FocusRing together into the one
piece of state a draw pass needs to make its widgets interactive.
§Frame lifecycle
frame is the documented way to drive one frame: it wraps a closure with
begin_frame and end_frame, and hands the closure a
Ui pairing the surface passed in with self.
use retroglyph_core::backend::{Backend, Headless};
use retroglyph_core::grid::Rect;
use retroglyph_core::terminal::Terminal;
use retroglyph_ui::{Interaction, Sense};
#[derive(Clone, Copy, PartialEq, Eq)]
enum WidgetId {
SaveButton,
}
let mut term = Terminal::new(Headless::new(20, 10));
let mut interaction = Interaction::<WidgetId>::new();
let clicked = interaction.frame(&mut term.surface(), |ui| {
let area = Rect::new(0, 0, 10, 1);
let response = ui.interaction().interact(area, WidgetId::SaveButton, Sense::click());
// ... draw the button, using response.hovered()/focused() to pick a style ...
response.clicked()
});
assert!(!clicked); // nothing clicked yet: no input was fed inbegin_frame/handle_event/end_frame stay public for callers driving the lifecycle
themselves (e.g. to interleave event handling between frames rather than all at once), but
frame is what each step below describes:
interaction.begin_frame(); // 1
for event in poll_events() {
interaction.handle_event(&event); // 2
}
draw(&mut term, &mut interaction, &state); // 3: calls interaction.interact(...)
interaction.end_frame(); // 4begin_framesnapshots which id (if any) is under the pointer, and whether it pressed/released/scrolled, using last frame’s hit registrations and pointer events: this frame’s registrations aren’t complete until step 3 finishes, and this frame’s events haven’t arrived yet (they’re step 2), so everyResponsein a given frame is one frame stale relative to what’s being drawn/fed in this frame: uniformly for hover, press, release, click, and scroll, all resolved from that one snapshot. At typical redraw rates this is imperceptible; it’s the same kind of trade-offListState::ensure_visibledocuments for a different reason (only the caller knows the current viewport height), applied here because only the previous frame knows the full hit list and the pointer’s position as of the input that’s about to be processed.draggingandResponse::heldare exceptions: both re-check the pointer’s live position (viaPointer::pos/Pointer::is_down) rather than the frame-stale snapshot, because a drag-in-progress or a press-cancel needs to react the instant the pointer moves, not one frame later. Keyboard focus is the remaining exception:Response::focusedand Enter/Space activation readFocusRing’scurrentlive, since it’s plain level state with no hit-testing involved: no staleness to trade off.handle_eventupdates pointer position/buttons and, by default, cycles focus on Tab/Shift+Tab, then reports whether this interaction claimed the event, resolved against the same last-frame registrations step 1 just read: an app juggling more than oneInteractioncan stop routing an event the moment one of them claims it, without waiting for step 3 to run.- Each widget calls
interactwith its rect, a caller-chosen id, and aSensedescribing what it cares about; it gets back aResponseand, as a side effect, registers itself for step 1 of the next frame. end_framereleases the active widget if step 1 saw the pointer go up.
One consequence worth knowing: a full press-then-release gesture that
arrives as two events in the same handle_event
batch (both fed in during step 2 of one frame, e.g. a synthetic test
firing them back to back) takes an extra frame to resolve versus a
realistic press and release arriving in separate frames, because step
1’s hover snapshot for that frame still reflects the pointer’s
position from before those events. Real input rarely lands this way
(a physical click’s down and up are milliseconds apart, i.e. several
frames at typical redraw rates), so this only tends to show up in tests.
§Why Id is a type parameter, not a hash
Immediate-mode toolkits like egui derive a widget’s identity from its
call-site source location (optionally salted with data) hashed down to
an opaque integer, flexible, but it means two widgets can collide onto
the same id at runtime with no compile-time signal, and the id carries
no meaning a debugger can show you. Interaction<Id> instead asks the
app for whatever id type it already has lying around: typically a
small Copy enum like the hand-rolled hit-target enum an app would
otherwise define anyway. Collisions become unrepresentable if the enum
is exhaustive, and {:?}-printing an id tells you exactly which widget
it is. The cost is one generic parameter; Id: Copy + PartialEq is all
any of this module asks for.
Consistently with that: everything here holds its state in a plain,
explicitly-owned struct threaded through &mut self, the same
convention ListState uses, rather than the
interior-mutability/global-context pattern egui’s Memory relies on to
keep its implicit ids from needing to be threaded everywhere.
Implementations§
Source§impl<Id> Interaction<Id>
impl<Id> Interaction<Id>
Sourcepub const fn with_drag_threshold(self, cells: u16) -> Self
pub const fn with_drag_threshold(self, cells: u16) -> Self
Override how far (in cells) the pointer must move from its press
origin before a Sense::DRAG widget reports
Response::dragging rather than a click-in-progress. Defaults to
DEFAULT_DRAG_THRESHOLD.
Sourcepub const fn with_double_click_window(self, frames: u16) -> Self
pub const fn with_double_click_window(self, frames: u16) -> Self
Override how many begin_frame calls may separate two clicks on the
same widget for the second to report Response::double_clicked. Defaults to
DEFAULT_DOUBLE_CLICK_WINDOW.
Sourcepub const fn pointer(&self) -> &Pointer
pub const fn pointer(&self) -> &Pointer
Read access to the pointer’s current position/button/scroll state, e.g. to draw a custom cursor glyph.
Source§impl<Id: Copy + PartialEq> Interaction<Id>
impl<Id: Copy + PartialEq> Interaction<Id>
Sourcepub const fn hovered(&self) -> Option<Id>
pub const fn hovered(&self) -> Option<Id>
The id the pointer resolved to this frame, if any: the same value
interact compares against to decide each
widget’s Response::hovered, resolved from last frame’s hit-test
(see Interaction for why there’s a frame of latency).
Unlike Response::hovered, this isn’t filtered by the hovered
widget’s Sense: it’s the topmost id under the pointer
regardless of what that id is listening for, which is what makes it
useful for drawing a hover-driven readout (a tooltip, a cost
preview) before the widget it depends on has been registered this
frame, rather than having to stash the value for next frame by hand.
Sourcepub fn frame<R>(
&mut self,
surface: &mut Surface<'_>,
f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R,
) -> R
pub fn frame<R>( &mut self, surface: &mut Surface<'_>, f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R, ) -> R
Run one frame: begin_frame, then f (given a Ui pairing
surface with self), then end_frame.
This is the documented way to drive the frame lifecycle: the three
calls are easy to get right once and easy to forget (particularly end_frame) when spread
across a caller’s own draw loop by hand.
Sourcepub fn begin_frame(&mut self)
pub fn begin_frame(&mut self)
Resolve hover/press against last frame’s registrations, finalize the
focus order, and clear the hit registry for this frame’s
interact calls. Call once per frame, before
processing input or drawing.
Sourcepub fn handle_event(&mut self, event: &Event) -> Consumed
pub fn handle_event(&mut self, event: &Event) -> Consumed
Feed a raw input event: updates the pointer, and (by default) Tab cycles focus (see
FocusRing::handle_event if you need to override that), then reports whether this
interaction claimed it.
Resolved against last frame’s registrations, the same snapshot
wants_pointer/wants_keyboard read: this
frame’s interact calls haven’t run yet (they’re step 3 of the frame
lifecycle; handle_event is step 2), so self’s hit/focus
registrations at the time this runs are still whatever the previous frame left behind.
A pointer event (Event::Mouse) is claimed if its position lands on a registered rect,
regardless of that rect’s Sense (even a Sense::hover-only widget still owns the
pointer at its own position; a wheel event over it shouldn’t fall through to whatever’s
behind it either). Tab/Shift+Tab are claimed whenever anything is registered as
focusable, matching FocusRing::advance/retreat’s own condition
for actually moving focus. Enter/Space are claimed only when they double as
Sense::CLICK activation, i.e. a Sense::FOCUSABLE widget currently holds focus:
otherwise the same keys reach an app’s own text input or other key handling unclaimed.
Everything else (Event::Resize, Event::Paste, Event::FocusGained/
Event::FocusLost, and any key this interaction doesn’t bind) is never claimed, even
while a widget is focused and active: those need to reach whatever’s behind an open
overlay (a resize still has to reflow the screen under a dropdown), which a coarser “is
the overlay open” gate cannot express without also swallowing them.
Sourcepub fn wants_pointer(&self) -> bool
pub fn wants_pointer(&self) -> bool
Whether the pointer is over a rect registered last frame, so a pointer event delivered right now would land on a widget rather than fall through to whatever’s behind this interaction.
Answerable before this frame has drawn anything: interact registers
each rect for the next frame’s hit test (see the frame lifecycle),
so last frame’s registrations, and therefore this answer, are already complete the moment
begin_frame returns. An app with more than one Interaction (a
menu bar above a screen stack, say) can call this on the frontmost one before routing a
pointer event anywhere, the same role io.WantCaptureMouse plays in Dear ImGui.
Reads the pointer’s live position, not a frame-stale snapshot: unlike
Response::hovered, which only updates once a frame via begin_frame, this is meant to
be called right after handle_event to decide where to route the
event that was just fed in, so it has to reflect that event’s effect on the pointer
immediately, not wait for the next begin_frame.
Sourcepub const fn wants_keyboard(&self) -> bool
pub const fn wants_keyboard(&self) -> bool
Whether a widget currently holds keyboard focus, so a key event delivered right now is likely to be consumed rather than fall through.
Unlike wants_pointer, this reads live, not a frame-stale
snapshot: focus is plain level state that persists across frames until
FocusRing::request/clear moves it, the same reasoning
Response::focused documents.
Sourcepub fn animate(
&mut self,
id: Id,
target: bool,
duration: Duration,
frame: &Frame,
) -> f32
pub fn animate( &mut self, id: Id, target: bool, duration: Duration, frame: &Frame, ) -> f32
Eases toward 1.0 when target is true, 0.0 otherwise, over duration, advancing by
frame.delta. Owns one Tween per id, created (at rest, on whichever side target
starts on) the first time this is called for that id, and dropped once it settles back
at rest at its current target, so this doesn’t grow unbounded across a long-running app
with many transient Ids (an overlay’s per-item ids, say).
The bridge from Response to crate::animate: a widget’s
render (already handed a Response) calls this once per frame with, say,
response.hovered() as target, and blends its idle/hover style by the result, without
declaring a Tween field of its own or hand-diffing this frame’s hovered() against
last frame’s to find the edge that should retarget it, both of which an app would
otherwise need to do once per animated Id.
duration only takes effect while id’s tween is created, i.e. the first call for that
id, or the first call after a previous one settled and was pruned: changing it on a
later call while the tween is still in flight has no effect, the same as
Tween::retarget leaving duration untouched.
Sourcepub fn push_barrier(&mut self, rect: Rect)
pub fn push_barrier(&mut self, rect: Rect)
Trait Implementations§
Source§impl<Id: Clone> Clone for Interaction<Id>
impl<Id: Clone> Clone for Interaction<Id>
Source§fn clone(&self) -> Interaction<Id>
fn clone(&self) -> Interaction<Id>
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more