use std::collections::HashSet;
use rustc_hash::FxHashMap;
use web_time::Instant;
use crate::anim::{AnimProp, AnimValue, Animation, Timing};
use crate::state::query::target_in_subtree;
use crate::state::{AnimationMode, EnvelopeKind};
use crate::tree::{El, InteractionState};
#[derive(Copy, Clone, Default)]
pub(crate) struct HotTargets<'a> {
pub hovered: Option<&'a str>,
pub focused: Option<&'a str>,
pub pressed: Option<&'a str>,
}
const APP_PROPS: &[AnimProp] = &[
AnimProp::AppFill,
AnimProp::AppStroke,
AnimProp::AppTextColor,
AnimProp::AppOpacity,
AnimProp::AppScale,
AnimProp::AppTranslateX,
AnimProp::AppTranslateY,
];
const STATE_PROPS: &[AnimProp] = &[
AnimProp::HoverAmount,
AnimProp::PressAmount,
AnimProp::FocusRingAlpha,
];
const SUBTREE_PROPS: &[AnimProp] = &[
AnimProp::SubtreeHoverAmount,
AnimProp::SubtreePressAmount,
AnimProp::SubtreeFocusAmount,
];
#[allow(clippy::too_many_arguments)]
pub(crate) fn tick_node(
node: &mut El,
anims: &mut FxHashMap<(String, AnimProp), Animation>,
envelopes: &mut FxHashMap<(String, EnvelopeKind), f32>,
node_states: &FxHashMap<String, InteractionState>,
hot: HotTargets<'_>,
focus_visible: bool,
visited: &mut HashSet<(String, AnimProp)>,
now: Instant,
mode: AnimationMode,
needs_redraw: &mut bool,
) {
if !node.computed_id.is_empty() {
if let Some(timing) = node.animate {
for &prop in APP_PROPS {
process_prop(
node,
prop,
timing,
anims,
envelopes,
node_states,
hot,
focus_visible,
visited,
now,
mode,
needs_redraw,
);
}
}
if node.key.is_some() {
for &prop in STATE_PROPS {
let timing = state_timing_for(prop);
process_prop(
node,
prop,
timing,
anims,
envelopes,
node_states,
hot,
focus_visible,
visited,
now,
mode,
needs_redraw,
);
}
}
if node.focusable || node.hover_alpha.is_some() {
for &prop in SUBTREE_PROPS {
let timing = state_timing_for(prop);
process_prop(
node,
prop,
timing,
anims,
envelopes,
node_states,
hot,
focus_visible,
visited,
now,
mode,
needs_redraw,
);
}
}
}
for child in &mut node.children {
tick_node(
child,
anims,
envelopes,
node_states,
hot,
focus_visible,
visited,
now,
mode,
needs_redraw,
);
}
}
#[allow(clippy::too_many_arguments)]
fn process_prop(
node: &mut El,
prop: AnimProp,
timing: Timing,
anims: &mut FxHashMap<(String, AnimProp), Animation>,
envelopes: &mut FxHashMap<(String, EnvelopeKind), f32>,
node_states: &FxHashMap<String, InteractionState>,
hot: HotTargets<'_>,
focus_visible: bool,
visited: &mut HashSet<(String, AnimProp)>,
now: Instant,
mode: AnimationMode,
needs_redraw: &mut bool,
) {
let state = node_states
.get(&node.computed_id)
.copied()
.unwrap_or_default();
let Some(target) = compute_target(node, prop, state, hot, focus_visible) else {
return;
};
let key = (node.computed_id.clone(), prop);
visited.insert(key.clone());
let anim = anims
.entry(key)
.or_insert_with(|| Animation::new(target, target, timing, now));
anim.retarget(target, now);
let settled = match mode {
AnimationMode::Live => anim.step(now),
AnimationMode::Settled => {
anim.settle();
true
}
};
write_prop(node, prop, anim.current, envelopes);
if !settled {
*needs_redraw = true;
}
}
fn compute_target(
n: &El,
prop: AnimProp,
state: InteractionState,
hot: HotTargets<'_>,
focus_visible: bool,
) -> Option<AnimValue> {
let in_subtree = |target: Option<&str>| -> bool {
target.is_some_and(|t| target_in_subtree(&n.computed_id, t))
};
match prop {
AnimProp::HoverAmount => Some(AnimValue::Float(
if matches!(state, InteractionState::Hover) {
1.0
} else {
0.0
},
)),
AnimProp::PressAmount => Some(AnimValue::Float(
if matches!(state, InteractionState::Press) {
1.0
} else {
0.0
},
)),
AnimProp::FocusRingAlpha => Some(AnimValue::Float(
if hot.focused == Some(n.computed_id.as_str())
&& (focus_visible || n.always_show_focus_ring)
{
1.0
} else {
0.0
},
)),
AnimProp::SubtreeHoverAmount => Some(AnimValue::Float(if in_subtree(hot.hovered) {
1.0
} else {
0.0
})),
AnimProp::SubtreePressAmount => Some(AnimValue::Float(if in_subtree(hot.pressed) {
1.0
} else {
0.0
})),
AnimProp::SubtreeFocusAmount => Some(AnimValue::Float(if in_subtree(hot.focused) {
1.0
} else {
0.0
})),
AnimProp::AppFill => n.fill.map(AnimValue::Color),
AnimProp::AppStroke => n.stroke.map(AnimValue::Color),
AnimProp::AppTextColor => n.text_color.map(AnimValue::Color),
AnimProp::AppOpacity => Some(AnimValue::Float(n.opacity)),
AnimProp::AppScale => Some(AnimValue::Float(n.scale)),
AnimProp::AppTranslateX => Some(AnimValue::Float(n.translate.0)),
AnimProp::AppTranslateY => Some(AnimValue::Float(n.translate.1)),
}
}
fn state_timing_for(prop: AnimProp) -> Timing {
match prop {
AnimProp::HoverAmount
| AnimProp::PressAmount
| AnimProp::FocusRingAlpha
| AnimProp::SubtreeHoverAmount
| AnimProp::SubtreePressAmount
| AnimProp::SubtreeFocusAmount => Timing::SPRING_QUICK,
_ => Timing::SPRING_QUICK,
}
}
fn write_prop(
n: &mut El,
prop: AnimProp,
value: AnimValue,
envelopes: &mut FxHashMap<(String, EnvelopeKind), f32>,
) {
match (prop, value) {
(AnimProp::AppFill, AnimValue::Color(c)) => n.fill = Some(c),
(AnimProp::AppStroke, AnimValue::Color(c)) => n.stroke = Some(c),
(AnimProp::AppTextColor, AnimValue::Color(c)) => n.text_color = Some(c),
(AnimProp::HoverAmount, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::Hover),
v.clamp(0.0, 1.0),
);
}
(AnimProp::PressAmount, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::Press),
v.clamp(0.0, 1.0),
);
}
(AnimProp::FocusRingAlpha, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::FocusRing),
v.clamp(0.0, 1.0),
);
}
(AnimProp::SubtreeHoverAmount, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::SubtreeHover),
v.clamp(0.0, 1.0),
);
}
(AnimProp::SubtreePressAmount, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::SubtreePress),
v.clamp(0.0, 1.0),
);
}
(AnimProp::SubtreeFocusAmount, AnimValue::Float(v)) => {
envelopes.insert(
(n.computed_id.clone(), EnvelopeKind::SubtreeFocus),
v.clamp(0.0, 1.0),
);
}
(AnimProp::AppOpacity, AnimValue::Float(v)) => n.opacity = v.clamp(0.0, 1.0),
(AnimProp::AppScale, AnimValue::Float(v)) => n.scale = v.max(0.0),
(AnimProp::AppTranslateX, AnimValue::Float(v)) => n.translate.0 = v,
(AnimProp::AppTranslateY, AnimValue::Float(v)) => n.translate.1 = v,
_ => {}
}
}
pub(crate) fn is_in_flight(anim: &Animation) -> bool {
let cur = anim.current.channels();
let tgt = anim.target.channels();
if cur.n != tgt.n {
return true;
}
for i in 0..cur.n {
if (cur.v[i] - tgt.v[i]).abs() > f32::EPSILON {
return true;
}
if anim.velocity.n == cur.n && anim.velocity.v[i].abs() > f32::EPSILON {
return true;
}
}
false
}