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.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,315 @@
|
||||
//! 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(©) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user