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,265 @@
|
||||
//! What may go wrong in a show, and where (SHOW-14).
|
||||
|
||||
use ely_gpui_component::{
|
||||
lists::{ListItem, SelectableList},
|
||||
overlays::Dialog as Card,
|
||||
primitives::Severity,
|
||||
};
|
||||
use ely_lok::show::{Check, PageCheck, ShowSlide};
|
||||
use ely_office::prelude::*;
|
||||
use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, SharedString, Window, div};
|
||||
|
||||
use crate::deck::Deck;
|
||||
use crate::dialog::Dialog;
|
||||
|
||||
/// What a finding is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Problem {
|
||||
Font,
|
||||
Media,
|
||||
/// A linked file; `missing` says if it is gone.
|
||||
Link,
|
||||
/// Something the kernel cannot show.
|
||||
Object,
|
||||
}
|
||||
|
||||
/// One thing to fix, and where it is.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Finding {
|
||||
pub problem: Problem,
|
||||
pub name: String,
|
||||
pub missing: bool,
|
||||
/// Slide indexes from zero.
|
||||
pub slides: Vec<usize>,
|
||||
pub masters: Vec<String>,
|
||||
}
|
||||
|
||||
/// Every page's checks: slides by index, masters by name.
|
||||
fn pages(check: &Check) -> impl Iterator<Item = (Result<usize, &str>, &PageCheck)> {
|
||||
let slides = check.slides.iter().enumerate().map(|(at, page)| (Ok(at), page));
|
||||
slides.chain(check.masters.iter().map(|(name, page)| (Err(name.as_str()), page)))
|
||||
}
|
||||
|
||||
fn add(found: &mut Vec<Finding>, (problem, name, missing): (Problem, &str, bool), at: Result<usize, &str>) {
|
||||
let index = found.iter().position(|f| f.problem == problem && f.name == name);
|
||||
let finding = match index {
|
||||
Some(index) => &mut found[index],
|
||||
None => {
|
||||
found.push(Finding {
|
||||
problem,
|
||||
name: name.to_string(),
|
||||
missing,
|
||||
slides: Vec::new(),
|
||||
masters: Vec::new(),
|
||||
});
|
||||
found.last_mut().expect("just pushed")
|
||||
}
|
||||
};
|
||||
match at {
|
||||
Ok(slide) if !finding.slides.contains(&slide) => finding.slides.push(slide),
|
||||
Err(master) if !finding.masters.iter().any(|m| m == master) => {
|
||||
finding.masters.push(master.to_string())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Missing fonts, missing media, links, then broken objects.
|
||||
pub fn findings(check: &Check, slides: &[ShowSlide], missing_fonts: &[String]) -> Vec<Finding> {
|
||||
let mut found = Vec::new();
|
||||
for font in missing_fonts {
|
||||
let fonts = (Problem::Font, font.as_str(), true);
|
||||
let mut placed = false;
|
||||
for (at, _) in pages(check).filter(|(_, page)| page.fonts.contains(font)) {
|
||||
add(&mut found, fonts, at);
|
||||
placed = true;
|
||||
}
|
||||
// Missing, yet no slide text asks for it.
|
||||
if !placed {
|
||||
found.push(Finding {
|
||||
problem: Problem::Font,
|
||||
name: font.clone(),
|
||||
missing: true,
|
||||
slides: Vec::new(),
|
||||
masters: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (at, slide) in slides.iter().enumerate() {
|
||||
for media in &slide.media {
|
||||
match (media.missing, media.link) {
|
||||
(true, _) => add(&mut found, (Problem::Media, &media.url, true), Ok(at)),
|
||||
(false, true) => add(&mut found, (Problem::Link, &media.url, false), Ok(at)),
|
||||
(false, false) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (at, page) in pages(check) {
|
||||
for (url, missing) in &page.links {
|
||||
add(&mut found, (Problem::Link, url, *missing), at);
|
||||
}
|
||||
}
|
||||
for (at, page) in pages(check) {
|
||||
for name in &page.broken {
|
||||
add(&mut found, (Problem::Object, name, true), at);
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
/// Reads every slide and master, then lists what to fix.
|
||||
pub fn check_show(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.active_tab() else {
|
||||
let text = Deck::text(cx, "command.blocked.no_document", &[]);
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
let (id, missing) = (tab.id, tab.view.read(cx).missing_fonts().to_vec());
|
||||
let asked = self.ask_dialog();
|
||||
log::info!("show: checks the deck");
|
||||
self.read_in(
|
||||
id,
|
||||
|worker, doc| {
|
||||
let check = ely_lok::show::check(&worker.values(doc, ely_lok::show::SLIDE_CHECK)?)?;
|
||||
let slides = ely_lok::show::slide_show(&worker.values(doc, ely_lok::show::SLIDE_SHOW)?)?;
|
||||
Ok((check, slides))
|
||||
},
|
||||
move |deck, read, _, cx| match read {
|
||||
Ok((check, slides)) if deck.dialog_due(asked, id) => {
|
||||
let found = findings(&check, &slides, &missing);
|
||||
log::info!("show: the check found {} things", found.len());
|
||||
deck.dialog = Some(Dialog::Check(found));
|
||||
cx.notify();
|
||||
}
|
||||
Ok(_) => log::info!("show: the check is no longer due"),
|
||||
Err(error) => deck.engine_failed(&format!("the check: {error}"), cx),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// The findings; a row goes to its first slide.
|
||||
pub fn check_dialog(&self, found: &[Finding], cx: &mut Context<Self>) -> AnyElement {
|
||||
let this = cx.entity();
|
||||
let text = |key: &str| Deck::text(cx, key, &[]);
|
||||
let closing = this.clone();
|
||||
let dismiss = move |_: &mut Window, cx: &mut App| {
|
||||
closing.update(cx, |deck, cx| {
|
||||
deck.dialog = None;
|
||||
cx.notify();
|
||||
})
|
||||
};
|
||||
let card = Card::new("show-check", text("show.check.title"), dismiss);
|
||||
if found.is_empty() {
|
||||
return card.child(div().child(text("show.check.clear"))).into_any_element();
|
||||
}
|
||||
let mut list = SelectableList::new("show-check-list").label(text("show.check.title"));
|
||||
for (at, finding) in found.iter().enumerate() {
|
||||
let kind = match (finding.problem, finding.missing) {
|
||||
(Problem::Font, _) => "show.check.font",
|
||||
(Problem::Media, _) => "show.check.media",
|
||||
(Problem::Link, true) => "show.check.link_missing",
|
||||
(Problem::Link, false) => "show.check.link",
|
||||
(Problem::Object, _) => "show.check.object",
|
||||
};
|
||||
let mut places: Vec<String> = (finding.slides.iter())
|
||||
.map(|slide| Deck::text(cx, "show.check.slide", &[("n", &(slide + 1).to_string())]).to_string())
|
||||
.collect();
|
||||
places.extend(finding.masters.iter().map(|name| {
|
||||
Deck::text(cx, "show.check.master", &[("name", name)]).to_string()
|
||||
}));
|
||||
let name = match finding.name.is_empty() {
|
||||
true => text("show.check.unnamed"),
|
||||
false => SharedString::from(finding.name.clone()),
|
||||
};
|
||||
let place = match places.is_empty() {
|
||||
true => text("show.check.nowhere").to_string(),
|
||||
false => places.join(", "),
|
||||
};
|
||||
let item = ListItem::new(SharedString::from(format!("show-check-{at}")), name)
|
||||
.description(format!("{} · {place}", text(kind)));
|
||||
list = list.row(SharedString::from(at.to_string()), item);
|
||||
}
|
||||
let firsts: Vec<Option<usize>> = found.iter().map(|f| f.slides.first().copied()).collect();
|
||||
let list = list.on_change(move |keys, _, cx| {
|
||||
let first = keys.first().and_then(|key| key.parse::<usize>().ok()).and_then(|at| firsts[at]);
|
||||
this.update(cx, |deck, cx| {
|
||||
if let (Some(part), Some(tab)) = (first, deck.active_tab()) {
|
||||
let view = tab.view.clone();
|
||||
view.update(cx, |view, _| view.show_part(part));
|
||||
deck.dialog = None;
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
card.child(list).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ely_lok::show::{Media, Transition};
|
||||
|
||||
fn slide(media: Vec<Media>) -> ShowSlide {
|
||||
ShowSlide {
|
||||
hidden: false,
|
||||
notes: String::new(),
|
||||
transition: Transition {
|
||||
preset: String::new(),
|
||||
duration: 0.0,
|
||||
auto: false,
|
||||
time: 0.0,
|
||||
},
|
||||
effects: Vec::new(),
|
||||
media,
|
||||
}
|
||||
}
|
||||
|
||||
fn media(url: &str, link: bool, missing: bool) -> Media {
|
||||
Media {
|
||||
index: 0,
|
||||
url: url.into(),
|
||||
link,
|
||||
file: url.into(),
|
||||
missing,
|
||||
repeat: false,
|
||||
bounds: (0, 0, 1, 1),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn findings_say_what_and_where_in_order() {
|
||||
let page = |fonts: &[&str], links: &[(&str, bool)], broken: &[&str]| PageCheck {
|
||||
fonts: fonts.iter().map(|f| f.to_string()).collect(),
|
||||
links: links.iter().map(|(u, m)| (u.to_string(), *m)).collect(),
|
||||
broken: broken.iter().map(|b| b.to_string()).collect(),
|
||||
};
|
||||
let check = Check {
|
||||
slides: vec![
|
||||
page(&["Lost", "Inter"], &[("file:///a.png", true)], &[]),
|
||||
page(&["Lost"], &[], &["Chart 1"]),
|
||||
],
|
||||
masters: vec![("Plain".into(), page(&["Lost"], &[("file:///a.png", true)], &[]))],
|
||||
};
|
||||
let slides = [slide(vec![media("file:///clip.mp4", false, true)]), slide(vec![media("file:///b.mp3", true, false)])];
|
||||
let missing = ["Lost".to_string(), "Gone".to_string()];
|
||||
let found = findings(&check, &slides, &missing);
|
||||
let short: Vec<(Problem, &str, Vec<usize>, Vec<String>)> = found
|
||||
.iter()
|
||||
.map(|f| (f.problem, f.name.as_str(), f.slides.clone(), f.masters.clone()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
short,
|
||||
[
|
||||
(Problem::Font, "Lost", vec![0, 1], vec!["Plain".to_string()]),
|
||||
(Problem::Font, "Gone", vec![], vec![]),
|
||||
(Problem::Media, "file:///clip.mp4", vec![0], vec![]),
|
||||
(Problem::Link, "file:///b.mp3", vec![1], vec![]),
|
||||
(Problem::Link, "file:///a.png", vec![0], vec!["Plain".to_string()]),
|
||||
(Problem::Object, "Chart 1", vec![1], vec![]),
|
||||
]
|
||||
);
|
||||
assert!(found[4].missing && !found[3].missing);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user