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

316 lines
12 KiB
Rust

//! 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<ShowSlide>,
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<Mutex<Worker>>, 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(&copy) {
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<ShowSlide>)> {
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<Vec<ShowSlide>> {
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<dyn PlatformDisplay>) -> 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<Show>,
display: &Rc<dyn PlatformDisplay>,
presenter: bool,
cx: &mut App,
) -> gpui::Result<AnyWindowHandle> {
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<Self>) {
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<Self>) {
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>) {
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<Self>) {
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<Self>) -> AnyElement {
let this = cx.entity();
let label = |key: &str| Deck::text(cx, key, &[]);
let screens: Vec<Choice> = (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()
}
}