use std::fmt;
use std::time::Duration;
use crate::attrs::Attr;
use crate::fmt::secs;
use crate::options::{Common, common_setters};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Position {
After,
WithPrevious,
AfterPreviousStart(Duration),
Gap(Duration),
Overlap(Duration),
At(Duration),
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::After => f.write_str(">"),
Self::WithPrevious => f.write_str("<"),
Self::AfterPreviousStart(d) => write!(f, "<{}", secs(d)),
Self::Gap(d) => write!(f, "+={}", secs(d)),
Self::Overlap(d) => write!(f, "-={}", secs(d)),
Self::At(d) => f.write_str(&secs(d)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[must_use]
pub struct Timeline {
common: Common,
}
impl Timeline {
pub fn new() -> Self {
Self::default()
}
common_setters!();
#[must_use]
pub fn attributes(&self) -> Vec<Attr> {
let mut out = vec![("data-gsap-timeline", String::new())];
self.common.push(&mut out);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::is_position;
use crate::scroll::{Edge, Play, ScrollPos, Scrub};
use crate::{Ease, EaseDir, Repeat};
use autumn_web::html;
use proptest::prelude::*;
#[test]
fn positions_write_gsap_values() {
let ms = Duration::from_millis;
assert_eq!(Position::After.to_string(), ">");
assert_eq!(Position::WithPrevious.to_string(), "<");
assert_eq!(Position::AfterPreviousStart(ms(250)).to_string(), "<0.25");
assert_eq!(Position::Gap(ms(500)).to_string(), "+=0.5");
assert_eq!(Position::Overlap(ms(200)).to_string(), "-=0.2");
assert_eq!(Position::At(ms(1500)).to_string(), "1.5");
}
#[test]
fn default_timeline_writes_only_the_marker() {
let t = Timeline::new();
assert_eq!(t.attributes(), vec![("data-gsap-timeline", String::new())]);
}
#[test]
fn options_write_attributes() {
let t = Timeline::new()
.play(Play::Load)
.duration(Duration::from_millis(600))
.ease(Ease::Sine(EaseDir::InOut))
.repeat(Repeat::Forever)
.yoyo()
.start(ScrollPos::new(Edge::Top, Edge::Top))
.end(ScrollPos::Distance(1200.0))
.scrub(Scrub::Linked)
.pin();
let a = t.attributes();
assert_eq!(a[0], ("data-gsap-timeline", String::new()));
for want in [
("data-gsap-on", "load"),
("data-gsap-duration", "0.6"),
("data-gsap-ease", "sine.inOut"),
("data-gsap-repeat", "-1"),
("data-gsap-yoyo", ""),
("data-gsap-start", "top top"),
("data-gsap-end", "+=1200"),
("data-gsap-scrub", "true"),
("data-gsap-pin", ""),
] {
assert!(
a.iter().any(|(k, v)| *k == want.0 && v == want.1),
"{want:?} in {a:?}"
);
}
}
#[test]
fn wrap_in_writes_tag_and_class() {
let html = Timeline::new()
.class("hero")
.wrap_in(crate::Tag::Section, html! {})
.into_string();
assert_eq!(
html,
r#"<section class="hero" data-gsap-timeline></section>"#
);
}
#[test]
fn wrap_keeps_children() {
let html = Timeline::new()
.wrap(html! { p data-gsap="fade" { "x" } })
.into_string();
assert_eq!(
html,
r#"<div data-gsap-timeline><p data-gsap="fade">x</p></div>"#
);
}
fn position() -> impl Strategy<Value = Position> {
let d = (0u64..100_000).prop_map(Duration::from_millis);
prop_oneof![
Just(Position::After),
Just(Position::WithPrevious),
d.clone().prop_map(Position::AfterPreviousStart),
d.clone().prop_map(Position::Gap),
d.clone().prop_map(Position::Overlap),
d.prop_map(Position::At),
]
}
proptest! {
#[test]
fn every_position_matches_the_js_grammar(p in position()) {
let s = p.to_string();
prop_assert!(is_position(&s), "{s}");
}
}
}