//! Plays a slide's animations or transition over the canvas (SHOW-09). use std::{sync::Arc, time::Instant}; use ely_lok::{ LokView, show::{Layers, Part}, }; use ely_office::prelude::*; use gpui::{ AnyElement, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, RenderImage, Styled as _, Window, div, }; use image::{Frame, ImageBuffer, Rgba}; use crate::deck::Deck; pub(crate) mod compose; pub(crate) mod timeline; pub(crate) mod transition; /// About this wide; the height keeps the slide's shape. const WIDTH: u32 = 1280; /// What a preview plays. pub enum Playing { /// The slide's layers and its effects on the clock. Effects { layers: Vec<(Part, Arc)>, runs: Vec, /// The slide in 1/100 mm. slide: (f32, f32), }, /// The previous slide, if any, giving way to this one. Transition { from: Option)>>, to: Vec<(Part, Arc)>, preset: String, }, } /// Tab, slide, undo list, edits landed and pending. type Stamp = (u64, String, Vec, u64, bool); pub struct Preview { stamp: Stamp, pub playing: Playing, started: Instant, length: f64, } fn picture((width, height): (u32, u32), pixels: Vec) -> Arc { let image = ImageBuffer::, _>::from_raw(width, height, pixels) .expect("the worker paints the asked size"); Arc::new(RenderImage::new([Frame::new(image)])) } /// The layers as pictures. pub(crate) fn pictures(layers: Layers) -> Vec<(Part, Arc)> { let size = layers.size; (layers.list.into_iter()) .map(|layer| (layer.part, picture(size, layer.pixels))) .collect() } /// The preview's pixel size for a slide of `size`. fn pixels(size: (i64, i64)) -> (u32, u32) { let height = (WIDTH as f64 * size.1 as f64 / size.0.max(1) as f64).round(); (WIDTH, height.max(1.0) as u32) } impl Deck { /// The shown slide now; previews play only while it holds. fn stamp(&self, cx: &gpui::App) -> Option { let tab = self.active_tab()?; let slide = tab .extra .slides .list .get(tab.extra.slides.current)? .name .clone(); let history = tab.extra.history.as_ref()?.0.clone(); let view = tab.view.read(cx); Some((tab.id, slide, history, view.edited(), view.edit_pending())) } /// A new preview request; older ones no longer play. fn ask_preview(&mut self, cx: &gpui::App) -> Option<(u64, Stamp)> { self.preview_asked += 1; Some((self.preview_asked, self.stamp(cx)?)) } /// Whether request `asked` from `stamp` may still play. fn may_play(&self, asked: u64, stamp: &Stamp, cx: &gpui::App) -> bool { self.preview_asked == asked && self.stamp(cx).as_ref() == Some(stamp) } /// Plays the shown slide's effects from the kernel's layers. pub fn preview_effects(&mut self, cx: &mut Context) { let Some(tab) = self.active_tab() else { return; }; let (id, doc, part) = (tab.id, tab.doc.clone(), tab.extra.slides.current); let Some((_, effects)) = tab.extra.motion.clone() else { return; }; let (asked, slide) = (pixels(tab.extra.size), crate::design::mm100(tab.extra.size)); let Some((request, stamp)) = self.ask_preview(cx) else { return; }; log::info!("show: previews the effects of slide {part}"); ely_lok::read_off( &self.core.worker, &doc, move |worker, doc| doc.slide_layers(worker, part, asked), move |deck: &mut Self, read: std::io::Result, cx| match read { Ok(layers) if deck.may_play(request, &stamp, cx) => { let layers = pictures(layers); let runs = timeline::schedule(&effects); let length = timeline::length(&runs); let slide = (slide.0 as f32, slide.1 as f32); deck.play( stamp, Playing::Effects { layers, runs, slide, }, length, cx, ); } Ok(_) => log::info!("show: a preview for tab {id} is no longer due"), Err(error) => deck.engine_failed(&format!("the preview: {error}"), cx), }, cx, ); } /// Plays the shown slide's transition from the one before. pub fn preview_transition(&mut self, cx: &mut Context) { let Some(tab) = self.active_tab() else { return; }; let Some((transition, _)) = tab.extra.motion.clone() else { return; }; let (id, doc, part) = (tab.id, tab.doc.clone(), tab.extra.slides.current); let asked = pixels(tab.extra.size); let Some((request, stamp)) = self.ask_preview(cx) else { return; }; log::info!("show: previews the transition into slide {part}"); ely_lok::read_off( &self.core.worker, &doc, move |worker, doc| { let from = (part > 0) .then(|| doc.slide_layers(worker, part - 1, asked)) .transpose()?; Ok((from, doc.slide_layers(worker, part, asked)?)) }, move |deck: &mut Self, read: std::io::Result<(Option, Layers)>, cx| match read { Ok((from, to)) if deck.may_play(request, &stamp, cx) => { let playing = Playing::Transition { from: from.map(pictures), to: pictures(to), preset: transition.preset.clone(), }; deck.play(stamp, playing, transition.duration, cx); } Ok(_) => log::info!("show: a preview for tab {id} is no longer due"), Err(error) => deck.engine_failed(&format!("the preview: {error}"), cx), }, cx, ); } fn play(&mut self, stamp: Stamp, playing: Playing, length: f64, cx: &mut Context) { let layers = match &playing { Playing::Effects { layers, .. } => layers.len(), Playing::Transition { .. } => 2, }; log::info!("show: plays {layers} layers for {length:.2} s"); self.end_preview(); self.preview = Some(Preview { stamp, playing, started: Instant::now(), length, }); cx.notify(); } /// Ends the preview; its layers wait to be freed. fn end_preview(&mut self) -> bool { let Some(preview) = self.preview.take() else { return false; }; let pictures: Vec<&Arc> = match &preview.playing { Playing::Effects { layers, .. } => layers.iter().map(|(_, image)| image).collect(), Playing::Transition { from, to, .. } => from.iter().flatten().chain(to).map(|(_, image)| image).collect(), }; for picture in pictures { self.retired.push(picture.clone()); } true } /// Drops a preview whose slide or document moved on. pub(crate) fn drop_stale_preview(&mut self, cx: &gpui::App) { if (self.preview.as_ref()).is_some_and(|p| self.stamp(cx).as_ref() != Some(&p.stamp)) { log::info!("show: the slide changed under the preview"); self.end_preview(); } } /// Stops a preview at once. pub fn stop_preview(&mut self, cx: &mut Context) { // Reads still out are not wanted either. self.preview_asked += 1; if self.end_preview() { log::info!("show: the preview stops"); cx.notify(); } } /// The playing preview over the slide; a press stops it. pub fn preview_layer( &mut self, view: &Entity, window: &mut Window, cx: &mut Context, ) -> Option { let size = self.active_tab()?.extra.size; self.drop_stale_preview(cx); let preview = self.preview.as_ref()?; let t = preview.started.elapsed().as_secs_f64(); // The last frame stays a moment, then the canvas returns. if t > preview.length + 0.5 { log::info!("show: the preview ends"); self.end_preview(); return None; } window.request_animation_frame(); let lok = view.read(cx); let (top_left, bottom_right) = (lok.place((0, 0)), lok.place((size.0 as i32, size.1 as i32))); let frame = (bottom_right.x - top_left.x, bottom_right.y - top_left.y); let this = cx.entity(); // A show starts from black; effects play on the page. let behind = match &preview.playing { Playing::Transition { .. } => gpui::black(), Playing::Effects { .. } => ely_lok::paper(cx), }; let content = match &preview.playing { Playing::Effects { layers, runs, slide, } => compose::effects(layers, runs, *slide, t, frame), Playing::Transition { from, to, preset } => { let p = if preview.length <= 0.0 { 1.0 } else { (t / preview.length) as f32 }; let paper = ely_lok::paper(cx); // Exits are not read; the old slide shows everything. let from = (from.as_deref()) .map(|from| compose::slide(from, |_| true, frame, paper).into_any_element()); compose::transition( (from, to), &transition::frame(preset, p), frame, paper, ) } }; Some( div() .id("show-preview") .absolute() .left(top_left.x) .top(top_left.y) .w(frame.0) .h(frame.1) .overflow_hidden() .bg(behind) .on_mouse_down(gpui::MouseButton::Left, move |_, _, cx| { this.update(cx, |deck, cx| deck.stop_preview(cx)) }) .child(content) .into_any_element(), ) } }