use crate::style::{MotionSlot, PropertyDriver};
pub struct InteractionFade {
pub hover: PropertyDriver,
pub press: PropertyDriver,
}
impl InteractionFade {
pub fn new() -> Self {
Self {
hover: PropertyDriver::at(0.0, MotionSlot::Fast),
press: PropertyDriver::at(0.0, MotionSlot::Fast),
}
}
pub fn sync(&mut self, hovered: bool, pressed: bool) -> bool {
self.hover.set_target(if hovered { 1.0 } else { 0.0 });
self.press.set_target(if pressed { 1.0 } else { 0.0 });
self.is_animating()
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
let hover_moving = self.hover.tick(delta_ms);
let press_moving = self.press.tick(delta_ms);
hover_moving || press_moving
}
pub fn is_animating(&self) -> bool {
self.hover.is_moving() || self.press.is_moving()
}
pub fn intensity(&self) -> f32 {
self.hover.value().max(self.press.value()).clamp(0.0, 1.0)
}
}
impl Default for InteractionFade {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::InteractionFade;
#[test]
fn a_new_fade_rests_at_exactly_zero() {
let fade = InteractionFade::new();
assert_eq!(fade.hover.value(), 0.0);
assert_eq!(fade.press.value(), 0.0);
assert_eq!(fade.intensity(), 0.0);
assert!(!fade.is_animating(), "a resting control owes no frames");
}
#[test]
fn sync_retargets_the_driver_that_changed() {
let mut fade = InteractionFade::new();
assert!(fade.sync(true, false), "a hover arrival owes frames at once");
assert_eq!(fade.hover.target(), 1.0);
assert_eq!(fade.press.target(), 0.0);
assert!(fade.is_animating());
}
#[test]
fn syncing_the_same_state_does_not_reset_progress() {
let mut fade = InteractionFade::new();
let _ = fade.sync(true, false);
let _ = fade.tick(16);
let partway = fade.hover.value();
assert!(partway > 0.0 && partway < 1.0, "the hover is in flight: {partway}");
let _ = fade.sync(true, false);
assert_eq!(
fade.hover.value(),
partway,
"a repeated MouseEnter must re-aim the same movement, not restart it"
);
}
#[test]
fn a_fade_settles_exactly_at_the_target() {
let mut fade = InteractionFade::new();
let _ = fade.sync(true, false);
let mut frames = 0;
while fade.tick(16) {
frames += 1;
assert!(frames < 500, "the fade must terminate rather than tick forever");
}
assert_eq!(fade.hover.value(), 1.0, "the hover arrives exactly at its target");
assert_eq!(fade.press.value(), 0.0, "an untouched driver does not move");
assert!(!fade.is_animating());
}
#[test]
fn reduced_motion_settles_on_the_first_tick() {
use crate::compat::Box;
use crate::style::environment::{
install_environment, uninstall_environment, EnvironmentProvider,
};
use crate::style::MotionPreference;
struct Reduced;
impl EnvironmentProvider for Reduced {
fn motion_preference(&self) -> MotionPreference {
MotionPreference::ReduceMotion
}
}
let _guard = crate::style::theme_test_guard();
let previous = install_environment(Box::new(Reduced));
let mut fade = InteractionFade::new();
let _ = fade.sync(true, false);
let moving = fade.tick(16);
assert!(!moving, "a zero-duration fade settles on the first tick");
assert_eq!(fade.hover.value(), 1.0, "and it reaches the target, not half way");
assert!(!fade.is_animating());
let _ = uninstall_environment();
if let Some(previous) = previous {
install_environment(previous);
}
}
}