mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Preflight: show a pending first check as checking, and rebuild document indexes on grid changes
- The status bar and the Preflight panel show a gray light and "Checking" until the active document's first check ends, instead of a green "No errors". - The cross-reference and running-header indexes are keyed on the baseline grid and the font scope as well, so a change to either builds them again, in a cache and in a cache over it. - The test helper that waits for the preflight worker fails when the worker ends without a result.
This commit is contained in:
1 parent
2e1d17e1fa
commit
bdabdc8155
7 files changed
+101
-21
No files matched your search
@@ -88,6 +88,9 @@ fn doc_signature(doc: &Document) -> u64 {
|
||||
(Arc::as_ptr(&doc.styles) as usize).hash(&mut h);
|
||||
format!("{:?}", doc.text_variables).hash(&mut h);
|
||||
sections_hash(doc).hash(&mut h);
|
||||
// The compositions the indexes are built from also depend on these (see `signature`).
|
||||
let g = &doc.settings.baseline_grid;
|
||||
(doc.font_scope, g.relative_to == designcraft_doc::GridRelative::TopMargin, g.start.to_bits(), g.increment.to_bits()).hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
|
||||
@@ -8175,6 +8175,32 @@ fn a_cache_over_another_reads_it_without_writing_to_it() {
|
||||
assert!(base.composed_for(&old, sid).is_none(), "the base holds nothing of the older state");
|
||||
}
|
||||
|
||||
/// The cross-reference and running-header indexes are built from compositions that depend on the
|
||||
/// baseline grid and the font scope, so a change to either builds them again, in a cache and in a
|
||||
/// cache over it.
|
||||
#[test]
|
||||
fn the_document_indexes_are_rebuilt_when_the_grid_or_font_scope_changes() {
|
||||
let para = ParaAttrs { grid_align: Some(GridAlign::AllLines), ..Default::default() };
|
||||
let (d, _, _) = doc_with(LOREM, Rect::new(0.0, 0.0, 200.0, 600.0), para);
|
||||
let base = Arc::new(Cache::new());
|
||||
let (xrefs, running) = (base.xref_index(&d), base.running_index(&d));
|
||||
let mut grid = d.clone();
|
||||
grid.settings.baseline_grid.increment = 30.0;
|
||||
let mut scope = d.clone();
|
||||
scope.font_scope += 1;
|
||||
for (changed, what) in [(&grid, "grid"), (&scope, "font scope")] {
|
||||
let reader = Cache::over(Arc::clone(&base));
|
||||
assert!(!Arc::ptr_eq(&reader.xref_index(changed), &xrefs), "{what}: the base's cross-reference index is not used");
|
||||
assert!(!Arc::ptr_eq(&reader.running_index(changed), &running), "{what}: the base's running-header index is not used");
|
||||
let own = Cache::new();
|
||||
let (x, r) = (own.xref_index(&d), own.running_index(&d));
|
||||
assert!(!Arc::ptr_eq(&own.xref_index(changed), &x), "{what}: cross-reference index rebuilt");
|
||||
assert!(!Arc::ptr_eq(&own.running_index(changed), &r), "{what}: running-header index rebuilt");
|
||||
}
|
||||
let reader = Cache::over(Arc::clone(&base));
|
||||
assert!(Arc::ptr_eq(&reader.xref_index(&d), &xrefs), "the same state uses the base's index");
|
||||
}
|
||||
|
||||
/// Two single pages 618 pt tall, top margins 36 and 48, each with an overset 11/13 story aligned
|
||||
/// to a baseline grid (start 0, increment 13) and a frame filling its margin box (bottom 582).
|
||||
fn grid_margin_doc(relative_to: designcraft_doc::GridRelative) -> (Document, [StoryId; 2]) {
|
||||
|
||||
@@ -169,19 +169,24 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.painter().circle_filled(
|
||||
r.center(),
|
||||
4.0,
|
||||
if errors == 0 { Color32::from_rgb(60, 200, 90) } else { Color32::from_rgb(235, 50, 50) },
|
||||
match errors {
|
||||
None => t.text_dim,
|
||||
Some(0) => Color32::from_rgb(60, 200, 90),
|
||||
Some(_) => Color32::from_rgb(235, 50, 50),
|
||||
},
|
||||
);
|
||||
let count = if errors == 0 {
|
||||
format!("{} ▾", crate::i18n::tr(&app.ui.language, "No errors"))
|
||||
} else {
|
||||
format!(
|
||||
let count = match errors {
|
||||
// The active document's first check hasn't ended.
|
||||
None => format!("{} ▾", crate::i18n::tr(&app.ui.language, "Checking")),
|
||||
Some(0) => format!("{} ▾", crate::i18n::tr(&app.ui.language, "No errors")),
|
||||
Some(errors) => format!(
|
||||
"{} ▾",
|
||||
crate::i18n::count_label(
|
||||
&app.ui.language,
|
||||
if errors == 1 && !crate::i18n::category_first(&app.ui.language) { "error" } else { "errors" },
|
||||
errors
|
||||
)
|
||||
)
|
||||
),
|
||||
};
|
||||
// As in InDesign, the count opens a menu that leads to the Preflight panel.
|
||||
let menu = crate::menus::menu_button(ui, egui::RichText::new(count).font(small.clone()), |ui| {
|
||||
|
||||
@@ -299,9 +299,13 @@ pub fn text_attrs(app: &mut DesignApp) -> Option<Value> {
|
||||
if v.is_null() { None } else { Some(serde_json::to_value(v).ok()?) }
|
||||
}
|
||||
|
||||
/// Count the preflight errors last found for the active document (0 until its first check ends).
|
||||
pub fn preflight_errors(app: &mut DesignApp, ctx: &egui::Context) -> usize {
|
||||
crate::preflight_worker::issues(app, ctx).map_or(0, |issues| issues.iter().filter(|i| i.severity == "error").count())
|
||||
/// Count the preflight errors last found for the active document: `None` until its first check
|
||||
/// ends, and 0 with no document open.
|
||||
pub fn preflight_errors(app: &mut DesignApp, ctx: &egui::Context) -> Option<usize> {
|
||||
match crate::preflight_worker::issues(app, ctx) {
|
||||
Some(issues) => Some(issues.iter().filter(|i| i.severity == "error").count()),
|
||||
None => app.session.active().is_none().then_some(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Height of one swatch-menu row.
|
||||
|
||||
@@ -300,18 +300,29 @@ pub fn info_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
|
||||
pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let issues = crate::preflight_worker::issues(app, ui.ctx()).unwrap_or_default();
|
||||
let found = crate::preflight_worker::issues(app, ui.ctx());
|
||||
// The active document's first check hasn't ended.
|
||||
let checking = found.is_none() && app.session.active().is_some();
|
||||
let issues = found.unwrap_or_default();
|
||||
let errors = issues.iter().filter(|i| i.severity == "error").count();
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(10.0, 10.0), egui::Sense::hover());
|
||||
ui.painter().circle_filled(
|
||||
r.center(),
|
||||
4.5,
|
||||
if errors == 0 { egui::Color32::from_rgb(60, 200, 90) } else { egui::Color32::from_rgb(235, 50, 50) },
|
||||
if checking {
|
||||
t.text_dim
|
||||
} else if errors == 0 {
|
||||
egui::Color32::from_rgb(60, 200, 90)
|
||||
} else {
|
||||
egui::Color32::from_rgb(235, 50, 50)
|
||||
},
|
||||
);
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
if issues.is_empty() {
|
||||
if checking {
|
||||
crate::i18n::tr(&app.ui.language, "Checking").to_string()
|
||||
} else if issues.is_empty() {
|
||||
crate::i18n::tr(&app.ui.language, "No errors").to_string()
|
||||
} else {
|
||||
format!(
|
||||
|
||||
@@ -420,13 +420,5 @@ pub(crate) fn start_worker(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
app.preflight.worker.channels = spawn(ctx.clone(), &app.preflight.worker.live);
|
||||
}
|
||||
|
||||
/// Waits in a test for the worker's next result and keeps it, as a frame that polls would.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn wait_for_worker(app: &mut DesignApp) {
|
||||
if let Some(found) = app.preflight.worker.wait() {
|
||||
app.preflight.keep(found);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::vec2;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
use super::Checked;
|
||||
@@ -30,6 +31,14 @@ fn found(app: &crate::DesignApp) -> Option<(usize, bool)> {
|
||||
Some((f.issues.iter().filter(|i| i.severity == "error").count(), f.of.same(&now)))
|
||||
}
|
||||
|
||||
/// Waits for the worker's next result and keeps it, as a frame that polls would. Panics when the
|
||||
/// worker has ended without one.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn wait_for_worker(app: &mut crate::DesignApp) {
|
||||
let Some(found) = app.preflight.worker.wait() else { panic!("the preflight worker ended before it sent a result") };
|
||||
app.preflight.keep(found);
|
||||
}
|
||||
|
||||
/// A worker whose thread never takes a job: the test reads the queue.
|
||||
fn stalled_worker(app: &mut crate::DesignApp) -> (std::sync::mpsc::Receiver<super::Job>, std::sync::mpsc::Sender<super::Found>) {
|
||||
let (tx, jobs) = std::sync::mpsc::sync_channel(1);
|
||||
@@ -298,9 +307,39 @@ fn the_worker_checks_and_the_interface_thread_does_not() {
|
||||
assert_eq!(h.state().app.preflight.inline_checks, 0, "no check ran on the interface thread");
|
||||
// The last frame handed the current state to the worker, whose last answer is for it.
|
||||
while found(&h.state().app) != Some((1, true)) {
|
||||
super::wait_for_worker(&mut h.state_mut().app);
|
||||
wait_for_worker(&mut h.state_mut().app);
|
||||
}
|
||||
let issues = super::issues(&mut h.state_mut().app, &ctx).unwrap();
|
||||
assert_eq!(issues.iter().filter(|i| i.severity == "error").count(), 1);
|
||||
assert_eq!(h.state().app.preflight.inline_checks, 0, "the worker's answer is kept, not checked again");
|
||||
}
|
||||
|
||||
/// Until the active document's first check ends, the status bar and the Preflight panel show that
|
||||
/// it is being checked, not a clean result.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn the_first_check_shows_as_checking() {
|
||||
let (mut app, _) = overset_app();
|
||||
app.ui.dock_tab = "properties".into();
|
||||
let (_jobs, _done) = stalled_worker(&mut app);
|
||||
let mut h = test_window::open(app, vec2(1200.0, 700.0));
|
||||
h.state_mut().app.run("window.panel", json!({"panel": "preflight"})).unwrap();
|
||||
h.run_steps(5);
|
||||
assert!(h.state().app.preflight.found.is_empty(), "nothing found yet");
|
||||
assert_eq!(h.state().app.preflight.inline_checks, 0);
|
||||
assert!(h.query_by_label("Checking ▾").is_some(), "the status bar shows the check");
|
||||
assert!(h.query_by_label("No errors ▾").is_none());
|
||||
assert!(h.query_by_label("Checking").is_some(), "the Preflight panel shows the check");
|
||||
assert!(h.query_by_label("No errors").is_none());
|
||||
}
|
||||
|
||||
/// Waiting for a worker that ended before it answered fails the test instead of leaving it
|
||||
/// waiting.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
#[should_panic(expected = "the preflight worker ended before it sent a result")]
|
||||
fn waiting_for_a_worker_that_ended_fails() {
|
||||
let (mut app, _) = overset_app();
|
||||
drop(stalled_worker(&mut app));
|
||||
wait_for_worker(&mut app);
|
||||
}
|
||||
Reference in new issue
Block a user