use std::ops::Range;
use crate::phase::Phase;
use crate::Keyable;
#[derive(Debug, Clone, Copy, Keyable)]
pub struct Window {
start: f64,
end: f64,
current: f64,
}
impl Window {
pub const fn new(start: f64, end: f64, current: f64) -> Self {
Self {
start,
end,
current,
}
}
pub const fn start(self) -> f64 {
self.start
}
pub const fn end(self) -> f64 {
self.end
}
pub const fn current(self) -> f64 {
self.current
}
pub fn width(self) -> f64 {
self.span()
}
pub fn phase(self) -> Phase {
Phase::saturating(((self.current - self.start) / self.span()) as f32)
}
pub fn sub_secs(self, range: Range<f64>) -> Window {
assert!(
range.start.is_finite() && range.end.is_finite() && range.end > range.start,
"Window::sub_secs requires a finite range with end > start"
);
Self {
start: self.start + range.start,
end: self.start + range.end,
current: self.current,
}
}
pub fn clamped(self) -> Window {
Self {
start: self.start,
end: self.end,
current: self.current.clamp(self.start, self.end),
}
}
pub fn elapsed(self) -> f64 {
(self.current - self.start).max(0.0)
}
pub fn remaining(self) -> f64 {
(self.end - self.current).max(0.0)
}
pub fn envelope(self, fade_in: f64, fade_out: f64) -> Phase {
let rise = if fade_in <= 0.0 {
1.0
} else {
(self.elapsed() / fade_in).min(1.0)
};
let fall = if fade_out <= 0.0 {
1.0
} else {
(self.remaining() / fade_out).min(1.0)
};
Phase::saturating((rise * fall) as f32)
}
pub fn before(self) -> f64 {
(self.start - self.current).max(0.0)
}
pub fn after(self) -> f64 {
(self.current - self.end).max(0.0)
}
pub fn is_inside(self) -> bool {
self.current >= self.start && self.current < self.end
}
pub fn is_before(self) -> bool {
self.current < self.start
}
pub fn is_after(self) -> bool {
self.current >= self.end
}
pub fn raw_progress(self) -> f64 {
(self.current - self.start) / self.span()
}
fn span(self) -> f64 {
assert!(
self.start.is_finite() && self.end.is_finite() && self.end > self.start,
"Window requires finite start/end with end > start"
);
self.end - self.start
}
}
#[cfg(test)]
mod tests {
use super::*;
fn w(start: f64, end: f64, current: f64) -> Window {
Window::new(start, end, current)
}
#[test]
fn phase_saturates_at_endpoints() {
assert_eq!(w(3.0, 5.0, 2.0).phase().get(), 0.0);
assert_eq!(w(3.0, 5.0, 6.0).phase().get(), 1.0);
assert!((w(3.0, 5.0, 4.0).phase().get() - 0.5).abs() < 1e-6);
}
#[test]
fn sub_secs_offsets_against_the_start() {
let sub = w(3.0, 5.0, 4.0).sub_secs(0.5..1.5);
assert_eq!(sub.start(), 3.5);
assert_eq!(sub.end(), 4.5);
assert_eq!(sub.current(), 4.0);
assert!((sub.phase().get() - 0.5).abs() < 1e-6);
}
#[test]
fn sub_secs_saturates_outside_inner_window() {
let parent = w(3.0, 5.0, 3.1);
assert_eq!(parent.sub_secs(0.5..1.0).phase().get(), 0.0);
let parent = w(3.0, 5.0, 4.9);
assert_eq!(parent.sub_secs(0.5..1.0).phase().get(), 1.0);
}
#[test]
fn sub_secs_may_extend_past_the_parent_end() {
let sub = w(3.0, 5.0, 5.5).sub_secs(1.5..3.0);
assert_eq!(sub.end(), 6.0);
assert!((sub.phase().get() - (5.5 - 4.5) / 1.5).abs() < 1e-6);
}
#[test]
fn sub_secs_chains_in_local_seconds() {
let sub = w(3.0, 5.0, 4.0).sub_secs(0.5..2.0).sub_secs(0.25..0.75);
assert_eq!(sub.start(), 3.75);
assert_eq!(sub.end(), 4.25);
}
#[test]
#[should_panic(expected = "Window::sub_secs requires a finite range with end > start")]
fn sub_secs_rejects_empty_range() {
let _ = w(3.0, 5.0, 4.0).sub_secs(0.5..0.5);
}
#[test]
fn clamped_freezes_outside_the_window() {
let snap = w(3.0, 5.0, 1.0).clamped();
assert_eq!(snap.current(), 3.0);
assert_eq!(w(3.0, 5.0, 4.2).clamped().current(), 4.2);
assert_eq!(w(3.0, 5.0, 6.0).clamped(), w(3.0, 5.0, 99.0).clamped());
assert_eq!(w(3.0, 5.0, 6.0).clamped().phase().get(), 1.0);
}
#[test]
fn elapsed_keeps_counting_past_end() {
assert_eq!(w(3.0, 5.0, 2.0).elapsed(), 0.0);
assert!((w(3.0, 5.0, 4.0).elapsed() - 1.0).abs() < 1e-6);
assert!((w(3.0, 5.0, 7.5).elapsed() - 4.5).abs() < 1e-6);
}
#[test]
fn remaining_counts_down_then_zero() {
assert_eq!(w(3.0, 5.0, 2.0).remaining(), 3.0);
assert!((w(3.0, 5.0, 4.5).remaining() - 0.5).abs() < 1e-6);
assert_eq!(w(3.0, 5.0, 5.0).remaining(), 0.0);
assert_eq!(w(3.0, 5.0, 9.0).remaining(), 0.0);
}
#[test]
fn envelope_rises_holds_falls() {
let e = |c: f64| w(0.0, 3.0, c).envelope(0.5, 0.5);
assert_eq!(e(-1.0).get(), 0.0);
assert_eq!(e(0.0).get(), 0.0);
assert!((e(0.25).get() - 0.5).abs() < 1e-6);
assert!((e(0.5).get() - 1.0).abs() < 1e-6);
assert!((e(1.5).get() - 1.0).abs() < 1e-6);
assert!((e(2.75).get() - 0.5).abs() < 1e-6);
assert_eq!(e(3.0).get(), 0.0);
assert_eq!(e(4.0).get(), 0.0);
}
#[test]
fn envelope_skips_non_positive_fades() {
assert_eq!(w(0.0, 3.0, 0.0).envelope(0.0, 0.5).get(), 1.0);
assert_eq!(w(0.0, 3.0, 3.0).envelope(0.5, 0.0).get(), 1.0);
}
#[test]
fn before_counts_down_then_zero() {
assert!((w(3.0, 5.0, 1.0).before() - 2.0).abs() < 1e-6);
assert_eq!(w(3.0, 5.0, 3.0).before(), 0.0);
assert_eq!(w(3.0, 5.0, 4.0).before(), 0.0);
assert_eq!(w(3.0, 5.0, 6.0).before(), 0.0);
}
#[test]
fn after_zero_until_close_then_climbs() {
assert_eq!(w(3.0, 5.0, 4.0).after(), 0.0);
assert_eq!(w(3.0, 5.0, 5.0).after(), 0.0);
assert!((w(3.0, 5.0, 6.5).after() - 1.5).abs() < 1e-6);
}
#[test]
fn gates_partition_the_timeline() {
let win = w(3.0, 5.0, 2.0);
assert!(win.is_before() && !win.is_inside() && !win.is_after());
let win = w(3.0, 5.0, 4.0);
assert!(!win.is_before() && win.is_inside() && !win.is_after());
let win = w(3.0, 5.0, 5.0);
assert!(!win.is_before() && !win.is_inside() && win.is_after());
let win = w(3.0, 5.0, 6.0);
assert!(!win.is_before() && !win.is_inside() && win.is_after());
}
#[test]
fn raw_progress_is_unclamped() {
assert!((w(3.0, 5.0, 2.0).raw_progress() - (-0.5)).abs() < 1e-6);
assert!((w(3.0, 5.0, 7.0).raw_progress() - 2.0).abs() < 1e-6);
}
#[test]
fn width_matches_declared_span() {
assert_eq!(w(3.0, 5.0, 999.0).width(), 2.0);
}
#[test]
#[should_panic(expected = "Window requires finite start/end with end > start")]
fn phase_rejects_equal_bounds() {
let _ = w(5.0, 5.0, 5.0).phase();
}
#[test]
#[should_panic(expected = "Window requires finite start/end with end > start")]
fn raw_progress_rejects_equal_bounds() {
let _ = w(5.0, 5.0, 7.0).raw_progress();
}
}