use super::support::*;
#[test]
fn settled_mode_snaps_hover_envelope_to_one() {
let (mut tree, mut state) = lay_out_counter();
state.set_animation_mode(AnimationMode::Settled);
state.hovered = Some(target(&tree, &state, "inc"));
state.apply_to_state();
let needs_redraw = state.tick_visual_animations(&mut tree, Instant::now());
assert!(!needs_redraw, "Settled mode should never report in flight");
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::Hover),
Some(1.0)
);
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::Press),
Some(0.0)
);
}
#[test]
fn live_mode_eases_hover_envelope_over_multiple_ticks() {
let (mut tree, mut state) = lay_out_counter();
let t0 = Instant::now();
state.tick_visual_animations(&mut tree, t0);
state.hovered = Some(target(&tree, &state, "inc"));
state.apply_to_state();
let needs_redraw =
state.tick_visual_animations(&mut tree, t0 + std::time::Duration::from_millis(8));
let mid = envelope_for(&tree, &state, "inc", EnvelopeKind::Hover).expect("hover envelope");
assert!(
needs_redraw,
"spring should still be in flight after one 8 ms tick"
);
assert!(
mid > 0.0 && mid < 1.0,
"expected envelope mid-flight, got {mid}",
);
}
#[test]
fn build_value_change_survives_hover_envelope() {
let mut tree_a =
column([row([button("X").key("x").fill(Color::rgb(255, 0, 0))])]).padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
state.hovered = Some(target(&tree_a, &state, "x"));
state.apply_to_state();
state.tick_visual_animations(&mut tree_a, Instant::now());
assert_eq!(
envelope_for(&tree_a, &state, "x", EnvelopeKind::Hover),
Some(1.0)
);
let mut tree_b =
column([row([button("X").key("x").fill(Color::rgb(0, 0, 255))])]).padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.apply_to_state();
state.tick_visual_animations(&mut tree_b, Instant::now());
let observed = find_fill(&tree_b, "x").expect("x fill");
assert_eq!(
(observed.r, observed.g, observed.b),
(0, 0, 255),
"build fill should pass through unchanged — envelope handles state delta separately",
);
assert_eq!(
envelope_for(&tree_b, &state, "x", EnvelopeKind::Hover),
Some(1.0)
);
}
#[test]
fn focus_ring_alpha_eases_in_and_out() {
let (mut tree, mut state) = lay_out_counter();
state.set_animation_mode(AnimationMode::Settled);
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::FocusRing),
Some(0.0)
);
let (mut tree, _) = lay_out_counter();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.focused = Some(target(&tree, &state, "inc"));
state.set_focus_visible(true);
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::FocusRing),
Some(1.0)
);
let (mut tree, _) = lay_out_counter();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.focused = None;
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::FocusRing),
Some(0.0)
);
}
#[test]
fn focus_ring_stays_dim_when_focus_is_not_visible() {
let (mut tree, mut state) = lay_out_counter();
state.set_animation_mode(AnimationMode::Settled);
state.focused = Some(target(&tree, &state, "inc"));
assert!(!state.focus_visible);
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "inc", EnvelopeKind::FocusRing),
Some(0.0),
"ring must stay off until focus_visible is raised",
);
}
#[test]
fn focus_ring_lights_up_on_always_show_focus_ring_widgets_even_without_focus_visible() {
let selection = crate::selection::Selection::default();
let mut tree = column([row([crate::widgets::text_input::text_input(
"", &selection, "field",
)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
state.focused = Some(target(&tree, &state, "field"));
assert!(!state.focus_visible);
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "field", EnvelopeKind::FocusRing),
Some(1.0),
"always_show_focus_ring widgets ring on click too",
);
}
#[test]
fn focus_ring_stays_on_when_focused_node_is_also_hovered() {
let selection = crate::selection::Selection::default();
let mut tree = column([row([crate::widgets::text_input::text_input(
"", &selection, "field",
)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
let field = target(&tree, &state, "field");
state.focused = Some(field.clone());
state.hovered = Some(field);
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
assert_eq!(
envelope_for(&tree, &state, "field", EnvelopeKind::FocusRing),
Some(1.0),
"focus ring must stay on while the focused node is also the hover target",
);
}
#[test]
fn app_fill_settles_to_new_value_in_settled_mode() {
use crate::anim::Timing;
let mut tree_a = column([
crate::text("0"),
row([button("X")
.key("x")
.fill(Color::rgb(255, 0, 0))
.animate(Timing::SPRING_STANDARD)]),
])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
state.tick_visual_animations(&mut tree_a, Instant::now());
assert_eq!(
find_fill(&tree_a, "x").map(|c| (c.r, c.g, c.b)),
Some((255, 0, 0))
);
let mut tree_b = column([
crate::text("0"),
row([button("X")
.key("x")
.fill(Color::rgb(0, 0, 255))
.animate(Timing::SPRING_STANDARD)]),
])
.padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.tick_visual_animations(&mut tree_b, Instant::now());
assert_eq!(
find_fill(&tree_b, "x").map(|c| (c.r, c.g, c.b)),
Some((0, 0, 255)),
"Settled mode should snap to the new build value",
);
}
#[test]
fn app_fill_eases_in_live_mode() {
use crate::anim::Timing;
let mut tree_a = column([row([button("X")
.key("x")
.fill(Color::rgb(255, 0, 0))
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let t0 = Instant::now();
state.tick_visual_animations(&mut tree_a, t0);
let mut tree_b = column([row([button("X")
.key("x")
.fill(Color::rgb(0, 0, 255))
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let needs_redraw =
state.tick_visual_animations(&mut tree_b, t0 + std::time::Duration::from_millis(8));
let mid = find_fill(&tree_b, "x").expect("mid fill");
assert!(
needs_redraw,
"spring should still be in flight after one tick"
);
assert!(
mid.r < 255 && mid.b < 255,
"expected mid-flight, got {mid:?}",
);
assert!(mid.r > 0 || mid.b > 0, "should have moved off the start",);
}
#[test]
fn token_tagged_fill_eases_through_draw_ops_without_snapping() {
use crate::anim::Timing;
use crate::draw_ops::draw_ops_with_theme;
use crate::ir::DrawOp;
use crate::shader::UniformValue;
use crate::theme::Theme;
use crate::tokens;
let mut tree_a = column([row([button("X")
.key("x")
.fill(tokens::PRIMARY)
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
let t0 = Instant::now();
state.tick_visual_animations(&mut tree_a, t0);
let mut tree_b = column([row([button("X")
.key("x")
.fill(tokens::CARD)
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.tick_visual_animations(&mut tree_b, t0 + std::time::Duration::from_millis(8));
let theme = Theme::default();
let ops = draw_ops_with_theme(&tree_b, &state, &theme);
let quad_fill = ops
.iter()
.find_map(|op| match op {
DrawOp::Quad { id, uniforms, .. } if id.contains("x") => {
uniforms.get("fill").and_then(|v| match v {
UniformValue::Color(c) => Some(*c),
_ => None,
})
}
_ => None,
})
.expect("button quad fill");
let p = tokens::PRIMARY;
let c = tokens::CARD;
assert_ne!(
(quad_fill.r, quad_fill.g, quad_fill.b),
(p.r, p.g, p.b),
"rendered fill must not snap back to the source token's rgb",
);
assert_ne!(
(quad_fill.r, quad_fill.g, quad_fill.b),
(c.r, c.g, c.b),
"rendered fill must not snap forward to the target token's rgb",
);
}
#[test]
fn app_translate_eases_on_rebuild() {
use crate::anim::Timing;
let mut tree_a = column([row([button("slide")
.key("s")
.translate(0.0, 0.0)
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
state.tick_visual_animations(&mut tree_a, Instant::now());
let mut tree_b = column([row([button("slide")
.key("s")
.translate(100.0, 50.0)
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.tick_visual_animations(&mut tree_b, Instant::now());
let n = find_node(&tree_b, "s").expect("s node");
assert!((n.translate.0 - 100.0).abs() < 0.5);
assert!((n.translate.1 - 50.0).abs() < 0.5);
}
#[test]
fn state_envelope_composes_on_app_eased_fill() {
use crate::anim::Timing;
let mut tree = column([row([button("X")
.key("x")
.fill(Color::rgb(100, 100, 100))
.animate(Timing::SPRING_STANDARD)])])
.padding(20.0);
let mut state = UiState::new();
layout(&mut tree, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.set_animation_mode(AnimationMode::Settled);
state.hovered = Some(target(&tree, &state, "x"));
state.apply_to_state();
state.tick_visual_animations(&mut tree, Instant::now());
let n_fill = find_fill(&tree, "x").expect("x fill");
assert_eq!((n_fill.r, n_fill.g, n_fill.b), (100, 100, 100));
assert_eq!(
envelope_for(&tree, &state, "x", EnvelopeKind::Hover),
Some(1.0)
);
}
#[test]
fn app_animation_skipped_when_animate_not_set() {
fn swatch(fill: Color) -> El {
El::new(Kind::Custom("swatch"))
.key("x")
.fill(fill)
.width(Size::Fixed(40.0))
.height(Size::Fixed(20.0))
}
let mut tree_a = column([row([swatch(Color::rgb(255, 0, 0))])]).padding(20.0);
let mut state = UiState::new();
layout(&mut tree_a, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.tick_visual_animations(&mut tree_a, Instant::now());
let mut tree_b = column([row([swatch(Color::rgb(0, 0, 255))])]).padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.tick_visual_animations(&mut tree_b, Instant::now());
let observed = find_fill(&tree_b, "x").expect("x fill");
assert_eq!(
(observed.r, observed.g, observed.b),
(0, 0, 255),
"no .animate() — value should snap",
);
}
#[test]
fn animation_entries_gc_when_node_leaves_tree() {
let (mut tree_a, mut state) = lay_out_counter();
state.hovered = Some(target(&tree_a, &state, "inc"));
state.apply_to_state();
state.tick_visual_animations(&mut tree_a, Instant::now());
let inc_id_a = find_id(&tree_a, "inc").expect("inc id");
assert!(
state
.animation
.animations
.keys()
.any(|(id, _)| id == &inc_id_a),
"expected at least one entry for inc"
);
let mut tree_b = column([crate::text("0"), row([button("-").key("dec")])]).padding(20.0);
layout(&mut tree_b, &mut state, Rect::new(0.0, 0.0, 400.0, 200.0));
state.hovered = None;
state.apply_to_state();
state.tick_visual_animations(&mut tree_b, Instant::now());
assert!(
!state
.animation
.animations
.keys()
.any(|(id, _)| id == &inc_id_a),
"stale entries for inc were not GC'd"
);
}