use crate::core::{Rect, Size};
use crate::style::{MotionSlot, PropertyDriver};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevealDirection {
Down,
Up,
}
#[derive(Debug, Clone, Copy)]
pub struct PopupReveal {
progress: PropertyDriver,
}
impl Default for PopupReveal {
fn default() -> Self {
Self::new(false)
}
}
impl PopupReveal {
pub fn new(open: bool) -> Self {
Self { progress: PropertyDriver::at(if open { 1.0 } else { 0.0 }, MotionSlot::Fast) }
}
pub fn value(&self) -> f32 {
self.progress.value()
}
pub fn set_open(&mut self, open: bool) {
self.progress.set_target(if open { 1.0 } else { 0.0 });
}
pub fn jump_to(&mut self, open: bool) {
self.progress.jump_to(if open { 1.0 } else { 0.0 });
}
pub fn tick(&mut self, delta_ms: u32) -> bool {
self.progress.tick(delta_ms)
}
pub fn is_animating(&self) -> bool {
self.progress.is_moving()
}
pub fn revealed(&self, full: Rect, direction: RevealDirection, travel_cap: u32) -> Rect {
let progress = self.value();
if progress <= 0.0 {
return Rect::new(full.x, full.y, full.width, 0);
}
let height = if full.height <= travel_cap {
(full.height as f32 * progress) as u32
} else {
let eased = progress * progress;
(full.height as f32 * eased) as u32
};
let height = height.clamp(if progress > 0.0 { 1 } else { 0 }, full.height.max(1));
match direction {
RevealDirection::Down => Rect::new(full.x, full.y, full.width, height),
RevealDirection::Up => {
Rect::new(full.x, full.y + full.height as i32 - height as i32, full.width, height)
}
}
}
pub fn is_closed(&self) -> bool {
self.value() <= 0.0
}
pub fn travel_cap_for(full: Size) -> u32 {
(full.height / 3).clamp(6, 24)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_settled_reveal_is_the_panel_itself() {
let full = Rect::new(10, 20, 120, 90);
let open = PopupReveal::new(true);
assert_eq!(open.value(), 1.0);
assert!(!open.is_animating(), "an open popup owes no frames");
assert_eq!(
open.revealed(full, RevealDirection::Down, 24),
full,
"a settled reveal must be byte-identical to the un-animated panel, or every \
snapshot of every menu would move"
);
assert!(!open.is_closed());
}
#[test]
fn a_closed_reveal_paints_nothing() {
let full = Rect::new(10, 20, 120, 90);
let closed = PopupReveal::new(false);
assert!(closed.is_closed(), "a closed popup must be reported as closed");
assert_eq!(closed.value(), 0.0);
assert!(!closed.is_animating());
assert_eq!(
closed.revealed(full, RevealDirection::Down, 24).height,
0,
"a closed popup must reveal a zero-height panel, not a one-pixel sliver: a sliver \
reads as a rendering artifact rather than as a closed menu"
);
}
#[test]
fn opening_takes_interior_frames_downward() {
let full = Rect::new(0, 0, 100, 90);
let mut reveal = PopupReveal::new(false);
reveal.set_open(true);
assert!(reveal.is_animating(), "opening owes frames");
assert!(reveal.tick(1), "still moving after a single millisecond");
let mid = reveal.revealed(full, RevealDirection::Down, 30);
assert!(
mid.height > 0 && mid.height < full.height,
"an opening panel must take an interior height, not jump to either end (got {})",
mid.height
);
assert_eq!(mid.y, full.y, "growing down must keep the anchor edge fixed");
while reveal.tick(16) {}
let open = reveal.revealed(full, RevealDirection::Down, 30);
assert_eq!(open.height, full.height, "and settle at the panel's own height");
assert!(!reveal.is_animating());
}
#[test]
fn opening_upward_keeps_the_bottom_edge() {
let full = Rect::new(0, 100, 100, 60);
let mut reveal = PopupReveal::new(false);
reveal.set_open(true);
assert!(reveal.tick(1));
let mid = reveal.revealed(full, RevealDirection::Up, 20);
assert!(
mid.height > 0 && mid.height < full.height,
"an upward panel must also take an interior height (got {})",
mid.height
);
assert_eq!(
mid.y + mid.height as i32,
full.y + full.height as i32,
"growing up must keep the bottom edge fixed, or the panel detaches from its anchor"
);
}
#[test]
fn a_panel_shorter_than_the_cap_grows_to_its_own_height() {
let full = Rect::new(0, 0, 100, 12);
let mut reveal = PopupReveal::new(false);
reveal.set_open(true);
assert!(reveal.tick(1));
let mid = reveal.revealed(full, RevealDirection::Down, 24);
assert!(
mid.height <= full.height,
"a 12 px panel must never be revealed taller than itself (got {})",
mid.height
);
}
#[test]
fn the_travel_cap_is_bounded() {
assert_eq!(PopupReveal::travel_cap_for(Size::new(1, 3)), 6, "a tiny panel still moves");
assert_eq!(PopupReveal::travel_cap_for(Size::new(1, 900)), 24, "and a tall one is capped");
}
}