Files
ElyOffice/apps/ely-show/src/slideshow/steps.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

172 lines
5.3 KiB
Rust

//! Where a show stands, and where each key takes it.
use ely_lok::show::{Effect, ShowSlide};
use crate::preview::timeline::{Run, length, schedule};
/// Effects in click groups; group 0 runs on entry.
pub fn groups(effects: &[Effect]) -> Vec<Vec<Effect>> {
let mut groups = vec![Vec::new()];
for effect in effects {
if effect.start == "click" {
groups.push(Vec::new());
}
groups.last_mut().expect("one group").push(effect.clone());
}
groups
}
/// A slide and its clicks done, or the end.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place {
Slide { part: usize, clicks: usize },
End,
}
/// The next place on a click.
pub fn next(slides: &[ShowSlide], place: Place) -> Place {
match place {
Place::Slide { part, clicks } if clicks + 1 < groups(&slides[part].effects).len() => {
Place::Slide {
part,
clicks: clicks + 1,
}
}
Place::Slide { part, .. } => (part + 1..slides.len())
.find(|at| !slides[*at].hidden)
.map_or(Place::End, |part| Place::Slide { part, clicks: 0 }),
Place::End => Place::End,
}
}
/// The previous slide, fully built; the first stays.
pub fn previous(slides: &[ShowSlide], place: Place) -> Place {
let from = match place {
Place::Slide { part, .. } => part,
Place::End => slides.len(),
};
let built = |part: usize| Place::Slide {
part,
clicks: groups(&slides[part].effects).len() - 1,
};
match (0..from).rev().find(|at| !slides[*at].hidden) {
Some(part) => built(part),
None => place,
}
}
/// Runs from entry: group `n` starts at `started[n]` seconds.
pub fn runs(effects: &[Effect], started: &[f64]) -> Vec<Run> {
(groups(effects).iter().zip(started))
.flat_map(|(group, at)| {
schedule(group).into_iter().map(move |run| Run {
begin: run.begin + at,
end: run.end + at,
..run
})
})
.collect()
}
/// A click group starts now or once all before end.
pub fn click_at(effects: &[Effect], clicks: &[f64], now: f64) -> f64 {
let before = clicks.iter().copied().fold(now, f64::max);
length(&runs(effects, clicks)).max(before)
}
#[cfg(test)]
mod tests {
use super::*;
use ely_lok::show::Transition;
fn effect(start: &str) -> Effect {
Effect {
preset: "ooo-entrance-fade-in".into(),
label: String::new(),
class: "entrance".into(),
target: Some(0),
hash: start.into(),
start: start.into(),
duration: 1.0,
delay: 0.0,
}
}
fn slide(hidden: bool, effects: Vec<Effect>) -> ShowSlide {
ShowSlide {
hidden,
notes: String::new(),
transition: Transition {
preset: String::new(),
duration: 0.0,
auto: false,
time: 0.0,
},
effects,
media: Vec::new(),
}
}
#[test]
fn with_and_after_join_the_click_before() {
let sizes = |effects: &[Effect]| groups(effects).iter().map(Vec::len).collect::<Vec<_>>();
assert_eq!(sizes(&[]), [0]);
assert_eq!(sizes(&[effect("with"), effect("click")]), [1, 1]);
assert_eq!(sizes(&[effect("click"), effect("after"), effect("click")]), [0, 2, 1]);
}
#[test]
fn clicks_play_effects_then_skip_hidden_slides_to_the_end() {
let deck = [
slide(false, vec![effect("click")]),
slide(true, Vec::new()),
slide(false, Vec::new()),
];
let mut place = Place::Slide { part: 0, clicks: 0 };
let mut seen = Vec::new();
for _ in 0..4 {
place = next(&deck, place);
seen.push(place);
}
assert_eq!(
seen,
[
Place::Slide { part: 0, clicks: 1 },
Place::Slide { part: 2, clicks: 0 },
Place::End,
Place::End
]
);
assert_eq!(previous(&deck, Place::End), Place::Slide { part: 2, clicks: 0 });
assert_eq!(
previous(&deck, Place::Slide { part: 2, clicks: 0 }),
Place::Slide { part: 0, clicks: 1 },
"back to a built slide, past the hidden one"
);
let first = Place::Slide { part: 0, clicks: 0 };
assert_eq!(previous(&deck, first), first);
}
#[test]
fn a_quick_click_waits_for_the_groups_and_transition_before() {
let long = |start| Effect {
duration: 10.0,
..effect(start)
};
let effects = [long("click"), long("click")];
assert_eq!(click_at(&effects, &[0.0], 0.1), 0.1, "nothing runs yet");
assert_eq!(click_at(&effects, &[0.0, 0.1], 0.2), 10.1, "after the first");
assert_eq!(click_at(&effects, &[2.0], 0.5), 2.0, "after the transition");
}
#[test]
fn each_group_runs_from_its_own_click() {
let effects = [effect("with"), effect("click"), effect("after")];
let times: Vec<(f64, f64)> = runs(&effects, &[0.0, 5.0])
.iter()
.map(|r| (r.begin, r.end))
.collect();
assert_eq!(times, [(0.0, 1.0), (5.0, 6.0), (6.0, 7.0)]);
}
}