//! Starting a show: a copy read, screens picked, windows open. use std::{ io, path::PathBuf, rc::Rc, sync::{Arc, Mutex}, }; use ely_gpui_component::{ buttons::{Button, ButtonVariant}, data_display::DescriptionList, forms::{Choice, Select, Switch}, overlays::Dialog as Card, primitives::Severity, }; use ely_lok::{Doc, Worker, show::ShowSlide}; use ely_office::prelude::*; use ely_platform::AppId; use gpui::{ AnyElement, AnyWindowHandle, App, AppContext as _, Context, Entity, EntityId, IntoElement, ParentElement as _, PlatformDisplay, Window, WindowBounds, WindowOptions, }; use super::{Show, audience::Audience, presenter::Presenter}; use crate::deck::Deck; use crate::dialog::{Dialog, edit}; /// Layer pixels never pass this width. const WIDEST: f32 = 1920.0; /// A show waiting for its screens. pub struct Starting { tab: u64, slides: Vec, first: usize, /// The copy the show plays, and its file. doc: Doc, copy: PathBuf, /// The audience's screen, by place in the list. audience: usize, presenter: bool, } /// Closes and deletes the show's copy, off the window. pub fn discard(worker: Arc>, doc: Doc, copy: PathBuf, cx: &App) { cx.background_spawn(async move { if let Err(error) = worker.lock().expect("the worker lock").close_doc(doc) { log::error!("show: the copy did not close: {error}"); } if let Err(error) = std::fs::remove_file(©) { log::warn!("show: {} stays: {error}", copy.display()); } }) .detach(); } /// Saves `doc` as the show's copy and reads it. fn copy( worker: &mut Worker, doc: &Doc, (copy, scheme): (&std::path::Path, Option<&'static str>), ) -> io::Result<(usize, Doc, Vec)> { let current = doc.sheets(worker)?.current; let url = ely_lok::file_url(copy); doc.save_as(worker, &url, "odp")?; let shown = worker.load(&url)?; let read = |worker: &mut Worker| -> io::Result> { if let Some(scheme) = scheme { worker.run(&shown, "view.theme", &[scheme])?; } ely_lok::show::slide_show(&worker.values(&shown, ely_lok::show::SLIDE_SHOW)?) }; match read(worker) { Ok(slides) => Ok((current, shown, slides)), Err(error) => { worker.close_doc(shown)?; Err(error) } } } fn options(display: &Rc) -> WindowOptions { WindowOptions { window_bounds: Some(WindowBounds::Fullscreen(display.bounds())), display_id: Some(display.id()), titlebar: None, focus: true, app_id: Some(AppId::Show.slug().into()), ..Default::default() } } /// Opens one show window; its root scope holds the keys. pub fn show_window( show: &Entity, display: &Rc, presenter: bool, cx: &mut App, ) -> gpui::Result { let show = show.clone(); Ok(match presenter { true => cx .open_window(options(display), |window, cx| { let view = cx.new(|cx| Presenter::new(show, cx)); window.focus(&view.read(cx).focus.clone(), cx); view })? .into(), false => cx .open_window(options(display), |window, cx| { let view = cx.new(|cx| Audience::new(show, cx)); window.focus(&view.read(cx).focus.clone(), cx); view })? .into(), }) } impl Deck { /// Plays a copy from the first or shown slide. pub fn start_show(&mut self, here: bool, cx: &mut Context) { if self.show.is_some() || self.show_starting { return log::info!("show: already playing or starting"); } let Some(id) = self.core.active else { return; }; self.show_starting = true; log::info!("show: starts, here {here}"); if !self.settled(id, cx) { return self.when_settled(id, move |deck, _, cx| deck.read_show(id, here, cx)); } self.read_show(id, here, cx); } fn read_show(&mut self, id: u64, here: bool, cx: &mut Context) { let Some(tab) = self.tab(id) else { self.show_starting = false; return; }; let scheme = tab.view.read(cx).dark().map(ely_lok::scheme); let path = tab.work.join(format!("show-{}.odp", self.shows)); self.shows += 1; let into = path.clone(); let worker = self.core.worker.clone(); self.read_in( id, move |worker, doc| copy(worker, doc, (&into, scheme)), move |deck, read, _, cx| { deck.show_starting = false; let (current, doc, slides) = match read { Ok(read) => read, Err(error) => return deck.engine_failed(&format!("the show: {error}"), cx), }; let first = match here { true => Some(current).filter(|at| *at < slides.len()), false => slides.iter().position(|slide| !slide.hidden), }; let Some(first) = first.filter(|_| deck.core.active == Some(id)) else { discard(worker, doc, path, cx); if deck.core.active != Some(id) { return log::info!("show: tab {id} is no longer shown"); } let text = Deck::text(cx, "show.play.none", &[]); return deck.toast(text, Severity::Warning, cx); }; let starting = Starting { tab: id, slides, first, doc, copy: path, audience: 0, presenter: true, }; match cx.displays().len() { 1 => deck.open_show(starting, cx), _ => { deck.show_starting = true; deck.dialog = Some(Dialog::Show(starting)); cx.notify(); } } }, cx, ); } fn open_show(&mut self, starting: Starting, cx: &mut Context) { self.show_starting = false; let worker = self.core.worker.clone(); let displays = cx.displays(); let (Some(tab), Some(audience)) = (self.tab(starting.tab), displays.get(starting.audience)) else { log::warn!("show: the tab or screen {} is gone", starting.audience); return discard(worker, starting.doc, starting.copy, cx); }; let presenter = (displays.iter()) .find(|display| display.id() != audience.id()) .filter(|_| starting.presenter); let slide = crate::design::mm100(tab.extra.size); let slide = (slide.0 as f32, slide.1 as f32); let room = audience.bounds().size; let fitted = super::stage::fit(slide, (room.width, room.height)); let width = (f32::from(fitted.0) * 2.0).min(WIDEST).round() as u32; let pixels = (width, (width as f32 * slide.1 / slide.0).round() as u32); let deck = cx.weak_entity(); let played = (worker, starting.doc, starting.copy); let show = cx.new(|cx| { Show::new(deck, played, (starting.slides, starting.first), (slide, pixels), cx) }); let mut windows = Vec::new(); // The audience window opens last, so it is in front. let screens = presenter.map(|d| (d, true)).into_iter().chain([(audience, false)]); for (display, presenter) in screens { match show_window(&show, display, presenter, cx) { Ok(handle) => windows.push(handle), Err(error) => { show.update(cx, |show, cx| { show.windows = windows; show.close(cx); }); return self.engine_failed(&format!("the show window: {error}"), cx); } } } log::info!("show: {} windows open", windows.len()); show.update(cx, |show, _| { show.presenter = windows.len() > 1; show.windows = windows; }); self.show = Some(show); cx.notify(); } /// Show `id` closed its windows. pub fn show_ended(&mut self, id: EntityId, cx: &mut Context) { if self.show.as_ref().is_some_and(|show| show.entity_id() == id) { self.show = None; cx.notify(); } } /// Picks screens before the show opens. pub fn show_dialog(&self, starting: &Starting, cx: &mut Context) -> AnyElement { let this = cx.entity(); let label = |key: &str| Deck::text(cx, key, &[]); let screens: Vec = (cx.displays().iter().enumerate()) .map(|(at, display)| { let size = display.bounds().size; let name = Deck::text( cx, "show.play.screen", &[ ("number", &(at + 1).to_string()), ("width", &f32::from(size.width).round().to_string()), ("height", &f32::from(size.height).round().to_string()), ], ); Choice::new(at.to_string(), name) }) .collect(); let (pick, flip) = (this.clone(), this.clone()); let screen = Select::new("show-play-screen", screens) .label(label("show.play.audience")) .selected(starting.audience.to_string()) .on_change(move |value, _, cx| { let at = value.parse().expect("screens go by number"); edit(&pick, cx, |dialog, _| { if let Dialog::Show(starting) = dialog { starting.audience = at; } }) }); let presenter = Switch::new("show-play-presenter", starting.presenter) .name(label("show.play.presenter")) .on_change(move |on, _, cx| { edit(&flip, cx, |dialog, _| { if let Dialog::Show(starting) = dialog { starting.presenter = on; } }) }); let rows = DescriptionList::new() .item(label("show.play.audience"), screen) .item(label("show.play.presenter"), presenter); let closing = this.clone(); let dismiss = move |_: &mut Window, cx: &mut App| { closing.update(cx, |deck, cx| { if let Some(Dialog::Show(starting)) = deck.dialog.take() { deck.show_starting = false; discard(deck.core.worker.clone(), starting.doc, starting.copy, cx); } cx.notify(); }) }; let go = label("show.play.start"); Card::new("show-play", label("show.play.title"), dismiss) .child(rows) .action(ely_office::cancel(cx)) .action(move |_| { Button::new("show-play-go", go) .variant(ButtonVariant::Primary) .on_click(move |_, _, cx| { this.update(cx, |deck, cx| { if let Some(Dialog::Show(starting)) = deck.dialog.take() { deck.open_show(starting, cx); } }) }) }) .into_any_element() } }