Skip to main content

fret_ui_kit/declarative/
hover_intent.rs

1use fret_ui::{ElementContext, UiHost};
2
3use crate::declarative::scheduling;
4use crate::declarative::transition;
5use crate::headless::hover_intent::{HoverIntentConfig, HoverIntentState, HoverIntentUpdate};
6
7#[derive(Debug, Default, Clone, Copy)]
8struct HoverIntentDriverState {
9    last_frame_tick: Option<u64>,
10    tick: u64,
11    intent: HoverIntentState,
12}
13
14/// Drive [`HoverIntentState`] using the UI runtime's frame clock, and request continuous frames
15/// while the intent is in a delayed-transition state.
16#[track_caller]
17pub fn hover_intent<H: UiHost>(
18    cx: &mut ElementContext<'_, H>,
19    hovered: bool,
20    cfg: HoverIntentConfig,
21) -> HoverIntentUpdate {
22    let (open_delay_ticks, close_delay_ticks) = transition::effective_transition_durations_for_cx(
23        &*cx,
24        cfg.open_delay_ticks,
25        cfg.close_delay_ticks,
26    );
27    let cfg = HoverIntentConfig::new(open_delay_ticks, close_delay_ticks);
28
29    let frame_tick = cx.app.frame_id().0;
30    let update = cx.slot_state(HoverIntentDriverState::default, |st| {
31        match st.last_frame_tick {
32            None => {
33                st.last_frame_tick = Some(frame_tick);
34                st.tick = frame_tick;
35            }
36            Some(prev) if prev != frame_tick => {
37                st.last_frame_tick = Some(frame_tick);
38                st.tick = frame_tick;
39            }
40            Some(_) => {
41                // In some unit tests the runner-owned frame clock may not advance; fall back to a
42                // per-call monotonic tick so delays can still elapse deterministically.
43                st.tick = st.tick.saturating_add(1);
44            }
45        }
46        st.intent.update(hovered, st.tick, cfg)
47    });
48
49    scheduling::set_continuous_frames(cx, update.wants_continuous_ticks);
50    update
51}