Files
ElyOffice/apps/ely-show/src/slideshow.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

351 lines
11 KiB
Rust

//! The show: full screen, presenter view, keys (SHOW-11, 12).
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use ely_lok::{
Doc, Worker,
show::{Part, ShowSlide},
};
use gpui::{AnyWindowHandle, Context, RenderImage, SharedString, Task, WeakEntity};
use crate::deck::Deck;
use crate::preview::timeline::{self, Run};
use steps::Place;
mod audience;
mod keys;
mod layers;
mod media;
mod presenter;
mod stage;
pub(crate) mod start;
pub(crate) mod steps;
pub use keys::{bind, bind_keys, menu};
/// A slide's layers as pictures, bottom first.
pub type Stack = Arc<Vec<(Part, Arc<RenderImage>)>>;
/// Clicks long done: a slide shown fully built.
const BUILT: f64 = -1.0e6;
/// The slide left, as it looked when left.
pub struct Leaving {
pub stack: Stack,
pub runs: Vec<Run>,
/// Seconds after its entry.
pub at: f64,
}
/// How a step reaches its place.
#[derive(Clone, Copy, PartialEq)]
enum How {
/// The next click group on the same slide.
Click,
/// The next slide, with its transition.
Enter,
/// Straight there: jumps, Home, End.
Jump,
/// Back to a slide, all its effects done.
Back,
}
pub struct Show {
deck: WeakEntity<Deck>,
worker: Arc<Mutex<Worker>>,
/// The copy the show plays; edits never reach it.
doc: Doc,
copy: PathBuf,
pub slides: Vec<ShowSlide>,
/// The slide in 1/100 mm.
pub slide: (f32, f32),
pixels: (u32, u32),
pub place: Place,
/// When each click group began, seconds after entry.
pub clicks: Vec<f64>,
pub entered: Instant,
/// The slide's layers are on their way; its clock waits.
pub waiting: bool,
pub leaving: Option<Leaving>,
pub black: bool,
pub laser: bool,
/// The pointer on the slide, as shares.
pub pointer: Option<(f32, f32)>,
pub began: Instant,
layers: HashMap<usize, Stack>,
/// Stacks no longer wanted; freed once nothing draws them.
gone: Vec<Stack>,
/// Per window, audience then presenter: images to free.
retired: [ely_gpui_component::primitives::Retired; 2],
asking: HashSet<usize>,
/// Digits typed toward a jump.
pub typed: String,
pub windows: Vec<AnyWindowHandle>,
/// The presenter view is open.
pub presenter: bool,
/// What went wrong, for the presenter.
pub notice: Option<SharedString>,
advance: Option<Task<()>>,
/// Lets the left slide go once the transition ends.
release: Option<Task<()>>,
media: Option<media::Playing>,
ended: bool,
}
impl Show {
pub fn new(
deck: WeakEntity<Deck>,
(worker, doc, copy): (Arc<Mutex<Worker>>, Doc, PathBuf),
(slides, first): (Vec<ShowSlide>, usize),
(slide, pixels): ((f32, f32), (u32, u32)),
cx: &mut Context<Self>,
) -> Self {
let mut show = Show {
deck,
worker,
doc,
copy,
slides,
slide,
pixels,
place: Place::Slide {
part: first,
clicks: 0,
},
clicks: vec![0.0],
entered: Instant::now(),
waiting: true,
leaving: None,
black: false,
laser: false,
pointer: None,
began: Instant::now(),
layers: HashMap::new(),
gone: Vec::new(),
retired: Default::default(),
asking: HashSet::new(),
typed: String::new(),
windows: Vec::new(),
presenter: false,
notice: None,
advance: None,
release: None,
media: None,
ended: false,
};
show.fetch(cx);
show.plan_advance(cx);
show
}
pub fn part(&self) -> Option<usize> {
match self.place {
Place::Slide { part, .. } => Some(part),
Place::End => None,
}
}
/// The next slide the show will reach.
pub fn upcoming(&self) -> Option<usize> {
let from = self.part()?;
(from + 1..self.slides.len()).find(|at| !self.slides[*at].hidden)
}
fn go(&mut self, to: Place, how: How, cx: &mut Context<Self>) {
self.typed.clear();
let now = match self.waiting {
true => 0.0,
false => self.entered.elapsed().as_secs_f64(),
};
match (how, to) {
(How::Click, Place::Slide { part, .. }) => {
let at = steps::click_at(&self.slides[part].effects, &self.clicks, now);
self.clicks.push(at);
}
(_, Place::Slide { part, clicks }) => {
let transition = &self.slides[part].transition;
let moving = how == How::Enter && !transition.preset.is_empty();
self.leaving = (self.part())
.filter(|_| moving)
.and_then(|from| {
Some(Leaving {
stack: self.layers.get(&from)?.clone(),
runs: steps::runs(&self.slides[from].effects, &self.clicks),
at: now,
})
});
let lead = match self.leaving {
Some(_) => transition.duration,
None => 0.0,
};
self.entered = Instant::now();
self.waiting = !self.layers.contains_key(&part);
self.clicks = match how {
How::Back => vec![BUILT; clicks + 1],
_ => vec![lead],
};
(self.media, self.notice) = (None, None);
}
(_, Place::End) => {
self.leaving = None;
(self.media, self.notice) = (None, None);
}
}
log::info!("show: goes to {to:?}");
self.place = to;
self.fetch(cx);
self.plan_advance(cx);
self.plan_release(cx);
cx.notify();
}
/// The slide's layers came: its clock starts.
pub(super) fn arrived(&mut self, cx: &mut Context<Self>) {
if std::mem::replace(&mut self.waiting, false) {
log::info!("show: the slide is drawn; its clock starts");
self.entered = Instant::now();
self.plan_advance(cx);
self.plan_release(cx);
}
}
/// Drops the left slide's layers when the transition ends.
fn plan_release(&mut self, cx: &mut Context<Self>) {
self.release = None;
let (Some(part), Some(_)) = (self.part(), &self.leaving) else {
return;
};
if self.waiting {
return;
}
let rest = self.slides[part].transition.duration - self.entered.elapsed().as_secs_f64();
self.release = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_secs_f64(rest.max(0.0)))
.await;
let dropped = this.update(cx, |show, cx| {
show.leaving = None;
cx.notify();
});
if dropped.is_err() {
log::info!("show: ended before its transition");
}
}));
}
pub fn next(&mut self, cx: &mut Context<Self>) {
if self.place == Place::End {
return self.close(cx);
}
let to = steps::next(&self.slides, self.place);
let how = match (self.part(), to) {
(Some(from), Place::Slide { part, .. }) if from == part => How::Click,
_ => How::Enter,
};
self.go(to, how, cx);
}
pub fn previous(&mut self, cx: &mut Context<Self>) {
self.go(steps::previous(&self.slides, self.place), How::Back, cx);
}
/// Goes to slide `number`, counted from one.
pub fn jump(&mut self, number: usize, cx: &mut Context<Self>) {
if (1..=self.slides.len()).contains(&number) {
let to = Place::Slide {
part: number - 1,
clicks: 0,
};
self.go(to, How::Jump, cx);
} else {
log::info!("show: no slide {number}");
self.typed.clear();
cx.notify();
}
}
pub fn toggle_black(&mut self, cx: &mut Context<Self>) {
self.black = !self.black;
log::info!("show: black screen {}", self.black);
cx.notify();
}
pub fn toggle_laser(&mut self, cx: &mut Context<Self>) {
self.laser = !self.laser;
self.pointer = None;
log::info!("show: laser pointer {}", self.laser);
cx.notify();
}
pub fn point(&mut self, at: Option<(f32, f32)>, cx: &mut Context<Self>) {
if self.laser && self.pointer != at {
self.pointer = at;
cx.notify();
}
}
/// When the slide's effects have all run.
fn settled_at(&self) -> f64 {
let Some(part) = self.part() else {
return 0.0;
};
let runs = steps::runs(&self.slides[part].effects, &self.clicks);
timeline::length(&runs).max(self.clicks.iter().copied().fold(0.0, f64::max))
}
/// Whether frames still change.
pub fn moving(&self) -> bool {
!self.waiting && self.entered.elapsed().as_secs_f64() < self.settled_at()
}
/// Advances by itself after the slide's time.
fn plan_advance(&mut self, cx: &mut Context<Self>) {
self.advance = None;
let Some(part) = self.part() else {
return;
};
let transition = &self.slides[part].transition;
if !transition.auto || self.waiting {
return;
}
let wait = (self.settled_at() - self.entered.elapsed().as_secs_f64()).max(0.0)
+ transition.time;
self.advance = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_secs_f64(wait))
.await;
if this.update(cx, |show, cx| show.next(cx)).is_err() {
log::info!("show: ended before it advanced");
}
}));
}
/// Closes the windows and the copy.
pub fn close(&mut self, cx: &mut Context<Self>) {
if std::mem::replace(&mut self.ended, true) {
return;
}
log::info!("show: ends");
(self.advance, self.release) = (None, None);
self.media = None;
let (windows, deck, me) = (std::mem::take(&mut self.windows), self.deck.clone(), cx.entity_id());
start::discard(self.worker.clone(), self.doc.clone(), self.copy.clone(), cx);
// A key may come from a window being closed.
cx.defer(move |cx| {
for window in windows {
if let Err(error) = window.update(cx, |_, window, _| window.remove_window()) {
log::error!("show: a window did not close: {error}");
}
}
if let Some(deck) = deck.upgrade() {
deck.update(cx, |deck, cx| deck.show_ended(me, cx));
}
});
}
}