ElyDoc: kernel reads for styles, fonts, bookmarks and sections leave the UI thread; a late dialog read never replaces a newer dialog (D-03)
This commit is contained in:
7 files changed
+201
-105
No files matched your search
@@ -46,8 +46,14 @@ pub struct Docs {
|
||||
pub back: Vec<(u64, ely_lok::Rect)>,
|
||||
/// Last jump: tab, input, prior cursor moves, target.
|
||||
pub going: Option<(u64, u64, u64, ely_lok::Rect)>,
|
||||
/// Dialog reads asked; a dialog or tab change voids older.
|
||||
pub dialog_asked: u64,
|
||||
dialog_seen: Seen,
|
||||
}
|
||||
|
||||
/// Open dialog kinds, Ely's and the shell's, and the tab.
|
||||
type Seen = (Option<std::mem::Discriminant<Dialog>>, Option<std::mem::Discriminant<ely_office::Dialog>>, Option<u64>);
|
||||
|
||||
/// The right column's panels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Side {
|
||||
@@ -171,6 +177,7 @@ impl Docs {
|
||||
failed(docs, error, cx)
|
||||
})
|
||||
.detach();
|
||||
cx.observe_self(|docs, _| docs.watch_dialogs()).detach();
|
||||
Docs {
|
||||
core: Core::new(worker, work, cx),
|
||||
dialog: None,
|
||||
@@ -183,6 +190,18 @@ impl Docs {
|
||||
navigating: true,
|
||||
back: Vec::new(),
|
||||
going: None,
|
||||
dialog_asked: 0,
|
||||
dialog_seen: (None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// A dialog or tab change ends earlier dialog reads.
|
||||
fn watch_dialogs(&mut self) {
|
||||
let ely = self.dialog.as_ref().map(std::mem::discriminant);
|
||||
let now = (ely, self.core.dialog.as_ref().map(std::mem::discriminant), self.core.active);
|
||||
if self.dialog_seen != now {
|
||||
self.dialog_seen = now;
|
||||
self.dialog_asked += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-23
@@ -128,29 +128,28 @@ impl Docs {
|
||||
let text = Docs::text(cx, "command.blocked.no_document", &[]);
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
let listed = self
|
||||
.core
|
||||
.worker
|
||||
.lock()
|
||||
.expect("the worker lock")
|
||||
.values(&tab.doc, ".uno:StyleApply")
|
||||
.and_then(|json| ely_lok::commands::styles(&json, "8"));
|
||||
let names = match listed {
|
||||
Ok(names) => names,
|
||||
Err(error) => {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
};
|
||||
let name = (2..)
|
||||
.map(|n| Docs::text(cx, "doc.section.name", &[("n", &n.to_string())]).to_string())
|
||||
.find(|name| !names.contains(name))
|
||||
.expect("a free section name");
|
||||
log::info!("doc: a new section");
|
||||
self.run_active("style.new", &[&name, "8"], cx);
|
||||
self.run_active("page.section", &["3", &name], cx);
|
||||
self.props.update(cx, |props, _| props.refresh_lists());
|
||||
cx.notify();
|
||||
let id = tab.id;
|
||||
// Page style names are read off the window thread.
|
||||
self.ask_in(
|
||||
id,
|
||||
|worker, doc| ely_lok::commands::styles(&worker.values(doc, ".uno:StyleApply")?, "8"),
|
||||
move |docs, listed, _, cx| {
|
||||
let names = match listed {
|
||||
Ok(names) => names,
|
||||
Err(error) => return docs.engine_failed(&error.to_string(), cx),
|
||||
};
|
||||
let name = (2..)
|
||||
.map(|n| Docs::text(cx, "doc.section.name", &[("n", &n.to_string())]).to_string())
|
||||
.find(|name| !names.contains(name))
|
||||
.expect("a free section name");
|
||||
log::info!("doc: a new section");
|
||||
docs.run_in(Some(id), "style.new", &[&name, "8"], cx);
|
||||
docs.run_in(Some(id), "page.section", &["3", &name], cx);
|
||||
docs.props.update(cx, |props, _| props.refresh_lists());
|
||||
cx.notify();
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn on_page_number(&mut self, _: &InsertPageNumber, _: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
+38
-26
@@ -113,43 +113,55 @@ impl Props {
|
||||
self.read_styles(cx);
|
||||
}
|
||||
|
||||
/// The installed fonts, read off the window thread.
|
||||
fn read_fonts(&mut self, doc: &Doc, cx: &mut Context<Self>) {
|
||||
let read = self
|
||||
.worker
|
||||
.lock()
|
||||
.expect("the worker lock")
|
||||
.values(doc, ".uno:CharFontName")
|
||||
.and_then(|json| ely_lok::commands::fonts(&json));
|
||||
match read {
|
||||
Ok(fonts) => self.fonts = fonts.into_iter().map(Into::into).collect(),
|
||||
Err(error) => cx.emit(LokError(format!("the font list is unreadable: {error}"))),
|
||||
}
|
||||
ely_lok::read_off(
|
||||
&self.worker,
|
||||
doc,
|
||||
|worker, doc| ely_lok::commands::fonts(&worker.values(doc, ".uno:CharFontName")?),
|
||||
|props: &mut Props, read, cx| match read {
|
||||
Ok(fonts) => {
|
||||
props.fonts = fonts.into_iter().map(Into::into).collect();
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => cx.emit(LokError(format!("the font list is unreadable: {error}"))),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// The shown document's style lists, read off the window thread.
|
||||
fn read_styles(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(shown) = self.shown.as_mut() else {
|
||||
let Some(shown) = self.shown.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let read = self
|
||||
.worker
|
||||
.lock()
|
||||
.expect("the worker lock")
|
||||
.values(&shown.doc, ".uno:StyleApply")
|
||||
.and_then(|json| {
|
||||
let view = shown.view.entity_id();
|
||||
ely_lok::read_off(
|
||||
&self.worker,
|
||||
&shown.doc,
|
||||
|worker, doc| {
|
||||
let json = worker.values(doc, ".uno:StyleApply")?;
|
||||
let list = |family| -> std::io::Result<Vec<SharedString>> {
|
||||
let names = ely_lok::commands::styles(&json, family)?;
|
||||
Ok(names.into_iter().map(Into::into).collect())
|
||||
};
|
||||
Ok((list("2")?, list("1")?, list("8")?))
|
||||
});
|
||||
match read {
|
||||
Ok((paragraph, character, pages)) => {
|
||||
shown.paragraph = paragraph;
|
||||
shown.character = character;
|
||||
shown.pages = pages;
|
||||
}
|
||||
Err(error) => cx.emit(LokError(format!("the style list is unreadable: {error}"))),
|
||||
}
|
||||
},
|
||||
move |props: &mut Props, read, cx| {
|
||||
// Another tab shown since: its own read follows.
|
||||
let Some(shown) = props.shown.as_mut().filter(|shown| shown.view.entity_id() == view) else {
|
||||
return;
|
||||
};
|
||||
match read {
|
||||
Ok((paragraph, character, pages)) => {
|
||||
(shown.paragraph, shown.character, shown.pages) = (paragraph, character, pages);
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => cx.emit(LokError(format!("the style list is unreadable: {error}"))),
|
||||
}
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Re-reads the style lists once queued input lands.
|
||||
|
||||
@@ -11,13 +11,19 @@ use ely_office::prelude::*;
|
||||
|
||||
/// The panel, shown on the active tab.
|
||||
pub(super) fn panel(cx: &mut TestAppContext, docs: &Entity<Docs>) -> Entity<Props> {
|
||||
cx.update(|app| {
|
||||
let props = cx.update(|app| {
|
||||
let tab = docs.read(app).active_tab().expect("a tab");
|
||||
let (view, doc) = (tab.view.clone(), tab.doc.clone());
|
||||
let props = docs.read(app).props.clone();
|
||||
props.update(app, |props, cx| props.show(Some((&view, &doc)), cx));
|
||||
props
|
||||
})
|
||||
});
|
||||
// The lists come from a background read.
|
||||
settle(cx, |app| {
|
||||
let props = props.read(app);
|
||||
!props.fonts.is_empty() && props.shown.as_ref().is_some_and(|shown| !shown.paragraph.is_empty())
|
||||
});
|
||||
props
|
||||
}
|
||||
|
||||
fn run(cx: &mut TestAppContext, props: &Entity<Props>, id: &str, values: &[&str]) {
|
||||
|
||||
+63
-53
@@ -71,35 +71,51 @@ pub fn items(cx: &App) -> Vec<MenuItem> {
|
||||
}
|
||||
|
||||
impl Docs {
|
||||
/// A kernel list for the active document; failures toast.
|
||||
fn read(&self, command: &str, cx: &mut Context<Self>) -> Option<String> {
|
||||
/// Reads a kernel list off the window thread, then `then`.
|
||||
fn read_then(
|
||||
&mut self,
|
||||
command: &'static str,
|
||||
then: impl FnOnce(&mut Self, String, &mut Window, &mut Context<Self>) + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(tab) = self.active_tab() else {
|
||||
let text = Docs::text(cx, "command.blocked.no_document", &[]);
|
||||
self.toast(text, Severity::Warning, cx);
|
||||
return None;
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
let read = self
|
||||
.core
|
||||
.worker
|
||||
.lock()
|
||||
.expect("the worker lock")
|
||||
.values(&tab.doc, command);
|
||||
read.map_err(|error| {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
self.toast(text, Severity::Danger, cx);
|
||||
})
|
||||
.ok()
|
||||
let id = tab.id;
|
||||
self.dialog_asked += 1;
|
||||
let asked = self.dialog_asked;
|
||||
self.ask_in(
|
||||
id,
|
||||
move |worker, doc| worker.values(doc, command),
|
||||
move |docs, read, window, cx| {
|
||||
// A dialog opened since, or a later ask, wins.
|
||||
if docs.dialog_asked != asked || docs.dialog.is_some() || docs.core.dialog.is_some() {
|
||||
return log::info!("doc: a dialog read for tab {id} lapsed");
|
||||
}
|
||||
match read {
|
||||
Ok(json) => then(docs, json, window, cx),
|
||||
Err(error) => docs.engine_failed(&error.to_string(), cx),
|
||||
}
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn bookmarks(&self, cx: &mut Context<Self>) -> Option<Vec<String>> {
|
||||
let listed = ely_lok::review::bookmarks(&self.read(".uno:Bookmarks", cx)?);
|
||||
listed
|
||||
.map(|marks| marks.into_iter().map(|mark| mark.name).collect())
|
||||
.map_err(|error| {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
self.toast(text, Severity::Danger, cx);
|
||||
})
|
||||
.ok()
|
||||
/// The bookmarks' names, then `then`.
|
||||
fn with_bookmarks(
|
||||
&mut self,
|
||||
then: impl FnOnce(&mut Self, Vec<String>, &mut Window, &mut Context<Self>) + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.read_then(
|
||||
".uno:Bookmarks",
|
||||
move |docs, json, window, cx| match ely_lok::review::bookmarks(&json) {
|
||||
Ok(marks) => then(docs, marks.into_iter().map(|mark| mark.name).collect(), window, cx),
|
||||
Err(error) => docs.engine_failed(&error.to_string(), cx),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn ask(&mut self, ask: Ask, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -126,10 +142,8 @@ impl Docs {
|
||||
self.run_active("note.endnote", &[], cx);
|
||||
}
|
||||
|
||||
pub fn on_bookmark(&mut self, _: &InsertBookmark, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(taken) = self.bookmarks(cx) {
|
||||
self.ask(Ask::Bookmark(taken), "", window, cx);
|
||||
}
|
||||
pub fn on_bookmark(&mut self, _: &InsertBookmark, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.with_bookmarks(|docs, taken, window, cx| docs.ask(Ask::Bookmark(taken), "", window, cx), cx);
|
||||
}
|
||||
|
||||
pub fn on_link(&mut self, _: &InsertLink, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -141,19 +155,17 @@ impl Docs {
|
||||
}
|
||||
|
||||
pub fn on_reference(&mut self, _: &InsertReference, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(marks) = self.bookmarks(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(mark) = marks.first().cloned() else {
|
||||
let text = Docs::text(cx, "doc.reference.none", &[]);
|
||||
return self.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
self.dialog = Some(Dialog::Reference {
|
||||
marks,
|
||||
mark,
|
||||
show: "2",
|
||||
});
|
||||
cx.notify();
|
||||
self.with_bookmarks(
|
||||
|docs, marks, _, cx| {
|
||||
let Some(mark) = marks.first().cloned() else {
|
||||
let text = Docs::text(cx, "doc.reference.none", &[]);
|
||||
return docs.toast(text, Severity::Warning, cx);
|
||||
};
|
||||
docs.dialog = Some(Dialog::Reference { marks, mark, show: "2" });
|
||||
cx.notify();
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn on_caption(&mut self, _: &InsertCaption, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -173,18 +185,16 @@ impl Docs {
|
||||
self.ask(Ask::Caption(category), "", window, cx);
|
||||
}
|
||||
|
||||
pub fn on_formula(&mut self, _: &InsertFormula, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(json) = self.read(".uno:Formula", cx) else {
|
||||
return;
|
||||
};
|
||||
match ely_lok::review::formula(&json) {
|
||||
Ok(Some(formula)) => self.ask(Ask::Formula(true), &formula, window, cx),
|
||||
Ok(None) => self.ask(Ask::Formula(false), "", window, cx),
|
||||
Err(error) => {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
}
|
||||
pub fn on_formula(&mut self, _: &InsertFormula, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.read_then(
|
||||
".uno:Formula",
|
||||
|docs, json, window, cx| match ely_lok::review::formula(&json) {
|
||||
Ok(Some(formula)) => docs.ask(Ask::Formula(true), &formula, window, cx),
|
||||
Ok(None) => docs.ask(Ask::Formula(false), "", window, cx),
|
||||
Err(error) => docs.engine_failed(&error.to_string(), cx),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn on_comment(&mut self, _: &InsertComment, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::tests::{blank, docs, settle, settled, text_of, type_into, with_window
|
||||
|
||||
/// The open prompt's request and field.
|
||||
fn asked(cx: &TestAppContext, docs: &Entity<Docs>) -> (Ask, Entity<TextInput>) {
|
||||
// Prompts open once their kernel read comes back.
|
||||
cx.run_until_parked();
|
||||
cx.update(|app| match &docs.read(app).dialog {
|
||||
Some(Dialog::Ask { ask, field }) => (ask.clone(), field.clone()),
|
||||
_ => panic!("a prompt is open"),
|
||||
@@ -35,6 +37,7 @@ fn references_go_in_through_their_dialogs() {
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.on_reference(&InsertReference, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert!(
|
||||
cx.update(|app| docs.read(app).dialog.is_none()),
|
||||
"no bookmark, no cross-reference dialog"
|
||||
@@ -55,6 +58,7 @@ fn references_go_in_through_their_dialogs() {
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.on_reference(&InsertReference, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update(|app| docs.update(app, |docs, cx| docs.insert_reference(cx)));
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.on_link(&InsertLink, window, cx)
|
||||
@@ -188,3 +192,49 @@ fn a_long_formula_prefills_whole() {
|
||||
let (_, field) = asked(&cx, &docs);
|
||||
assert_eq!(cx.update(|app| field.read(app).text().to_string()), long);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_late_dialog_read_never_replaces_a_newer_dialog() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some(docs) = docs(&mut cx, dir.path()) else {
|
||||
eprintln!("SKIP: no LOK build");
|
||||
return;
|
||||
};
|
||||
blank(&mut cx, &docs);
|
||||
// The link dialog opens before the bookmarks come back.
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.on_bookmark(&InsertBookmark, window, cx);
|
||||
docs.on_link(&InsertLink, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert!(cx.update(|app| matches!(docs.read(app).dialog, Some(Dialog::Link { .. }))), "the link dialog stays");
|
||||
// Opened and closed meanwhile: the old ask lapses too.
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.dialog = None;
|
||||
cx.notify();
|
||||
docs.on_bookmark(&InsertBookmark, window, cx);
|
||||
docs.on_link(&InsertLink, window, cx);
|
||||
});
|
||||
cx.update(|app| docs.update(app, |docs, cx| {
|
||||
docs.dialog = None;
|
||||
cx.notify();
|
||||
}));
|
||||
cx.run_until_parked();
|
||||
assert!(cx.update(|app| docs.read(app).dialog.is_none()), "nothing pops up later");
|
||||
// A close prompt shown and cancelled meanwhile counts too.
|
||||
let id = cx.update(|app| docs.read(app).core.active.expect("a tab"));
|
||||
type_into(&mut cx, &docs, "edited");
|
||||
settled(&cx, &docs);
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.on_bookmark(&InsertBookmark, window, cx);
|
||||
docs.request_close(id, ely_office::After::Tab, window, cx);
|
||||
});
|
||||
assert!(cx.update(|app| docs.read(app).core.dialog.is_some()), "the close asks");
|
||||
cx.update(|app| docs.update(app, |docs, cx| {
|
||||
docs.core.dialog = None;
|
||||
cx.notify();
|
||||
}));
|
||||
cx.run_until_parked();
|
||||
assert!(cx.update(|app| docs.read(app).dialog.is_none()), "no bookmark prompt after the cancel");
|
||||
}
|
||||
Reference in new issue
Block a user