mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 11:50:20 -06:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5574b32600 | ||
|
|
8dc3f0cd5f | ||
|
|
f1b1077327 | ||
|
|
7b8c0ba5eb | ||
|
|
0b61f8b426 | ||
|
|
0a5d5d29f8 | ||
|
|
642f5dd6bf | ||
|
|
7834197fbc | ||
|
|
938e13e9c7 | ||
|
|
1787766d6f | ||
|
|
e34840a78a | ||
|
|
066af3b620 | ||
|
|
d0f273bbfd | ||
|
|
541483ce0b | ||
|
|
678af4368d | ||
|
|
fd27713d20 |
Generated
+22
-23
@@ -1081,7 +1081,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
|
||||
dependencies = [
|
||||
"krilla",
|
||||
"thiserror 2.0.21",
|
||||
@@ -1135,7 +1135,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -2981,7 +2981,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -6806,7 +6806,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
@@ -6844,7 +6844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-cli"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sys-locale",
|
||||
@@ -6857,11 +6857,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-control-key"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-doc"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"log",
|
||||
@@ -6875,7 +6875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-docbin"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
"chrono",
|
||||
@@ -6892,7 +6892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-docx"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.23.1",
|
||||
@@ -6917,7 +6917,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-engine"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
"chrono",
|
||||
@@ -6950,7 +6950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-fonts"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"brotli-decompressor",
|
||||
"harfrust 0.13.3",
|
||||
@@ -6964,7 +6964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-formats"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.23.1",
|
||||
@@ -6991,7 +6991,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-geom"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -6999,7 +6999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-layout"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"craft-proof",
|
||||
"image",
|
||||
@@ -7019,7 +7019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-mcp"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde_json",
|
||||
@@ -7031,7 +7031,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-metafile"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"thiserror 2.0.21",
|
||||
@@ -7039,7 +7039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-pdf"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-pdf",
|
||||
@@ -7060,7 +7060,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-render"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -7074,7 +7074,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-ui-egui"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"accesskit_consumer",
|
||||
"cfb",
|
||||
@@ -7107,7 +7107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-web"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"craft-web-api",
|
||||
"craft-web-host",
|
||||
@@ -7128,7 +7128,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-zotero"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -7291,7 +7291,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
@@ -7562,7 +7562,6 @@ dependencies = [
|
||||
"nix",
|
||||
"typed-path",
|
||||
"windows-sys 0.61.2",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/wordcraft", "apps/wordcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.15.1"
|
||||
version = "0.17.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -67,9 +67,9 @@ wuff = "0.2.9"
|
||||
brotli-decompressor = "5"
|
||||
vello_cpu = "0.2"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "tiff"] }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false, features = ["write"] }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "1b34eba41e62138fc2438d2734523b302840b45d", version = "=0.1.0", default-features = false, features = ["write"] }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] }
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
# zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates.
|
||||
@@ -134,7 +134,7 @@ debug = 0
|
||||
|
||||
# Required by craft-pdf: the backend rejects writers that failed during unwinding.
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "1b34eba41e62138fc2438d2734523b302840b45d" }
|
||||
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
|
||||
@@ -133,6 +133,7 @@ impl eframe::App for App {
|
||||
if let Some(key_file) = &self.2 {
|
||||
key_file.remove();
|
||||
}
|
||||
self.0.start_pending_update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +193,8 @@ fn schedule_macos_terminate(app: &mut WordApp, key_file: Option<&control_server:
|
||||
if let Some(key_file) = key_file {
|
||||
key_file.remove();
|
||||
}
|
||||
// `terminate:` ends the process without dropping the app, so a ready update's helper starts here.
|
||||
app.start_pending_update();
|
||||
dispatch2::DispatchQueue::main().exec_async(|| {
|
||||
if let Some(mtm) = objc2::MainThreadMarker::new() {
|
||||
objc2_app_kit::NSApplication::sharedApplication(mtm).terminate(None);
|
||||
|
||||
@@ -600,15 +600,7 @@ fn search_box(app: &mut WordApp, ui: &mut Ui, control_h: f32) {
|
||||
);
|
||||
let pfont = TypeRung::Control.regular();
|
||||
let ptrack = TypeRung::Control.tracking();
|
||||
paint_text(
|
||||
ui.painter(),
|
||||
pos2(crate::rtl::lead_x(rtl, r, 30.0), r.center().y - TypeRung::Control.line_height() / 2.0),
|
||||
crate::rtl::lead_top(rtl),
|
||||
ph,
|
||||
&pfont,
|
||||
ptrack,
|
||||
t.text_dim,
|
||||
);
|
||||
paint_text(ui.painter(), pos2(crate::rtl::lead_x(rtl, r, 30.0), r.center().y), crate::rtl::lead_center(rtl), ph, &pfont, ptrack, t.text_dim);
|
||||
if app.ui.keytips == crate::keytips::Phase::Tabs {
|
||||
app.keytip_rects.push((r, crate::keytips::SEARCH_LETTER.to_string()));
|
||||
}
|
||||
|
||||
@@ -490,14 +490,11 @@ fn body(ui: &mut Ui, d: &mut ColorsDialog) -> Option<bool> {
|
||||
ui.set_width(430.0);
|
||||
ui.label(egui::RichText::new(tl!("Colors")).font(crate::theme::semibold(14.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (tab, label) in [(ColorsTab::Standard, "Standard"), (ColorsTab::Custom, "Custom")] {
|
||||
if ui.selectable_label(d.tab == tab, tl!(label)).clicked() {
|
||||
d.tab = tab;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let names = [tl!("Standard").to_string(), tl!("Custom").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, d.tab as usize) {
|
||||
d.tab = if k == 0 { ColorsTab::Standard } else { ColorsTab::Custom };
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::vertical(ui, |ui| {
|
||||
ui.set_width(330.0);
|
||||
|
||||
@@ -269,14 +269,11 @@ fn symbol_cell(app: &mut WordApp, ui: &mut Ui, font: &str, c: char, selected: bo
|
||||
pub(super) fn symbol_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SymbolForm) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.set_width(GRID_COLS as f32 * CELL + 24.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (i, l) in ["Symbols", "Special Characters"].into_iter().enumerate() {
|
||||
if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() {
|
||||
f.tab = i as u8;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let names = [tl!("Symbols").to_string(), tl!("Special Characters").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) {
|
||||
f.tab = k as u8;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let mut insert: Option<(Value, String, String)> = None;
|
||||
if f.tab == 1 {
|
||||
if special_ui(app, ui, f, &mut insert, &t) {
|
||||
|
||||
@@ -557,14 +557,11 @@ impl BordersForm {
|
||||
|
||||
/// The Borders and Shading dialog body. Returns true to close.
|
||||
pub fn borders(app: &mut WordApp, ui: &mut Ui, f: &mut BordersForm) -> bool {
|
||||
ui.horizontal(|ui| {
|
||||
for (i, l) in ["Borders", "Page Border", "Shading"].into_iter().enumerate() {
|
||||
if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() {
|
||||
f.tab = i as u8;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let names = [tl!("Borders").to_string(), tl!("Page Border").to_string(), tl!("Shading").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) {
|
||||
f.tab = k as u8;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let theme = app.session.doc.settings.theme_colors.clone();
|
||||
match f.tab {
|
||||
0 => {
|
||||
|
||||
@@ -396,14 +396,11 @@ fn list_change(app: &mut WordApp, f: &mut AutoCorrectForm, params: Value) -> boo
|
||||
}
|
||||
|
||||
fn autocorrect_ui(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) -> bool {
|
||||
ui.horizontal(|ui| {
|
||||
for (i, l) in ["AutoCorrect", "AutoFormat As You Type", "Math AutoCorrect"].into_iter().enumerate() {
|
||||
if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() {
|
||||
f.tab = i as u8;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let names = [tl!("AutoCorrect").to_string(), tl!("AutoFormat As You Type").to_string(), tl!("Math AutoCorrect").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) {
|
||||
f.tab = k as u8;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
match f.tab {
|
||||
0 => autocorrect_tab(app, ui, f),
|
||||
1 => {
|
||||
|
||||
@@ -88,17 +88,14 @@ fn label_of(app: &WordApp, id: &str) -> String {
|
||||
|
||||
/// The dialog body; true closes it.
|
||||
pub fn customize_ribbon(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm) -> bool {
|
||||
ui.horizontal(|ui| {
|
||||
for (page, name) in [(Page::Ribbon, "Ribbon"), (Page::QuickAccess, "Quick Access Toolbar")] {
|
||||
if ui.selectable_label(f.page == page, tl!(name)).clicked() && f.page != page {
|
||||
f.page = page;
|
||||
f.renaming = None;
|
||||
f.confirm_reset = false;
|
||||
f.message.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
let names = [tl!("Ribbon").to_string(), tl!("Quick Access Toolbar").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, (f.page == Page::QuickAccess) as usize) {
|
||||
f.page = if k == 0 { Page::Ribbon } else { Page::QuickAccess };
|
||||
f.renaming = None;
|
||||
f.confirm_reset = false;
|
||||
f.message.clear();
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let (left, right, h) = (150.0, 230.0, 300.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut f.filter).hint_text(tl!("Search")).desired_width(left + right + 8.0));
|
||||
let mut cats: Vec<(String, String)> = categories(app).into_iter().map(|c| (c.clone(), tl!(&c).to_string())).collect();
|
||||
|
||||
@@ -274,7 +274,7 @@ fn paragraph_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let names = ["Indents and Spacing", "Line and Page Breaks", "Asian Typography"].map(|n| tl!(n).to_string());
|
||||
let names = [tl!("Indents and Spacing").to_string(), tl!("Line and Page Breaks").to_string(), tl!("Asian Typography").to_string()];
|
||||
if let Some(k) = crate::widgets::dialog_tabs(ui, &names, *tab as usize) {
|
||||
*tab = k as u8;
|
||||
}
|
||||
@@ -1161,7 +1161,7 @@ fn about_dialog(ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
return false;
|
||||
};
|
||||
ui.set_width(820.0);
|
||||
let names = ["About", "Contributors", "Models", "Language data"].map(|n| tl!(n).to_string());
|
||||
let names = [tl!("About").to_string(), tl!("Contributors").to_string(), tl!("Models").to_string(), tl!("Language data").to_string()];
|
||||
if let Some(i) = crate::widgets::dialog_tabs(ui, &names, *tab as usize) {
|
||||
*tab = i as u8;
|
||||
}
|
||||
|
||||
@@ -813,5 +813,8 @@ pub(crate) fn rust_sources(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
|
||||
files
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_dialog_tabs;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Dialogs with real tabs: a click on a tab shows its page.
|
||||
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
use wordcraft_engine::Session;
|
||||
|
||||
use crate::WordApp;
|
||||
use crate::dialogs::Dialog;
|
||||
|
||||
fn harness(name: &str) -> egui_kittest::Harness<'static, WordApp> {
|
||||
let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
let _ = app.run("ui.language", json!({"value": "en"}));
|
||||
let _ = app.run("document.setText", json!({"text": "Some text"}));
|
||||
let _ = app.run("select.all", json!({}));
|
||||
let _ = app.run("ui.dialog", json!({"name": name}));
|
||||
if app.dialog.is_none() && app.colors.is_none() {
|
||||
app.dialog = Dialog::open(name, &mut app);
|
||||
}
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_accessibility_check(false).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.ctx.set_fonts(crate::theme::font_definitions(false, false));
|
||||
h.ctx.set_missing_glyph_policy(egui::MissingGlyphPolicy::Tofu);
|
||||
settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
fn settle(h: &mut egui_kittest::Harness<'static, WordApp>) {
|
||||
for _ in 0..4 {
|
||||
h.step();
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_button<'a>(h: &'a egui_kittest::Harness<'static, WordApp>, label: &'a str) -> egui_kittest::Node<'a> {
|
||||
h.query_all_by_role_and_label(egui::accesskit::Role::Button, label).last().unwrap_or_else(|| panic!("no {label} button"))
|
||||
}
|
||||
|
||||
/// Click each tab in turn. After every click `page` must report its index.
|
||||
fn check(name: &str, tabs: &[&str], page: impl Fn(&WordApp) -> usize) {
|
||||
let mut h = harness(name);
|
||||
assert_eq!(page(h.state()), 0, "{name}: opens on the first tab");
|
||||
for (i, label) in tabs.iter().enumerate() {
|
||||
tab_button(&h, label).click();
|
||||
settle(&mut h);
|
||||
assert_eq!(page(h.state()), i, "{name}: the {label} tab shows its page");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colors_tabs_switch_pages() {
|
||||
check("colors", &["Standard", "Custom"], |app| app.colors.as_ref().map_or(9, |d| d.tab as usize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_tabs_switch_pages() {
|
||||
check("symbol", &["Symbols", "Special Characters"], |app| match &app.dialog {
|
||||
Some(Dialog::Insert(d)) => match &**d {
|
||||
crate::dialogs_insert::InsertDialog::Symbol(f) => f.tab as usize,
|
||||
_ => 9,
|
||||
},
|
||||
_ => 9,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borders_tabs_switch_pages() {
|
||||
check("borders", &["Borders", "Page Border", "Shading"], |app| match &app.dialog {
|
||||
Some(Dialog::Borders { form }) => form.tab as usize,
|
||||
_ => 9,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autocorrect_tabs_switch_pages() {
|
||||
check("autoCorrect", &["AutoCorrect", "AutoFormat As You Type", "Math AutoCorrect"], |app| match &app.dialog {
|
||||
Some(Dialog::Review(d)) => match &**d {
|
||||
crate::dialogs_review::ReviewDialog::AutoCorrect(f) => f.tab as usize,
|
||||
_ => 9,
|
||||
},
|
||||
_ => 9,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn customize_ribbon_tabs_switch_pages() {
|
||||
check("customizeRibbon", &["Ribbon", "Quick Access Toolbar"], |app| match &app.dialog {
|
||||
Some(Dialog::CustomizeRibbon { form }) => (form.page == crate::dialogs_ribbon::Page::QuickAccess) as usize,
|
||||
_ => 9,
|
||||
});
|
||||
}
|
||||
@@ -29,9 +29,9 @@ impl Default for Runtime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
updater: Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")),
|
||||
updater: Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
result_dialog: CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")),
|
||||
result_dialog: CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
startup_check: false,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -45,6 +45,13 @@ impl Default for Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
/// The update prompt's and the result window's words in the interface language: craft-update
|
||||
/// passes its English text and the values of its `{name}` placeholders each time it draws.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn translate(text: &str, args: &[(&str, &str)]) -> String {
|
||||
crate::i18n::fmt(crate::i18n::t(text), args)
|
||||
}
|
||||
|
||||
impl WordApp {
|
||||
/// Whether the app asks for a newer release when it starts (off until the desktop app turns it on).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -59,7 +66,15 @@ impl WordApp {
|
||||
pub fn disable_self_update(&mut self) {
|
||||
let rt = &mut self.updates_rt;
|
||||
rt.startup_check = false;
|
||||
rt.updater = Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_source(refused_check());
|
||||
rt.updater = Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate).with_source(refused_check());
|
||||
}
|
||||
|
||||
/// Starts the helper of an update that is ready, so that it installs once the process has
|
||||
/// ended. Call it once the exit is certain, before a path that ends the process without
|
||||
/// dropping the app. Later calls do nothing.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn start_pending_update(&mut self) {
|
||||
self.updates_rt.updater.start_pending_update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,15 +122,23 @@ pub(crate) fn tick(app: &mut WordApp, ctx: &egui::Context) {
|
||||
pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// The restart helper is scheduled before the app closes, so no update starts over unsaved work.
|
||||
// An update starts, and a ready one asks the app to close, only while no work is unsaved.
|
||||
let clean = !(app.session.dirty || app.save_unconfirmed() || app.printing.busy() || crate::panes::has_comment_drafts(app));
|
||||
match app.updates_rt.updater.show_if(ctx, clean) {
|
||||
Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)),
|
||||
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
||||
// The whole app quits, every window included, and first asks about each document with
|
||||
// unsaved changes.
|
||||
Action::Close => {
|
||||
let _ = app.ask_before_quit();
|
||||
}
|
||||
Action::None => {}
|
||||
}
|
||||
// The dialog answers Help ▸ Check for Updates; a found update leaves the status idle and the prompt above takes over.
|
||||
// It also reports what the last update came to, which the check at start leaves as the status.
|
||||
let rt = &mut app.updates_rt;
|
||||
if !matches!(rt.updater.status(), Status::Idle) {
|
||||
rt.manual_window_open = true;
|
||||
}
|
||||
if rt.manual_window_open {
|
||||
match rt.result_dialog.show(ctx, rt.updater.status()) {
|
||||
CheckResultResponse::None => {}
|
||||
@@ -124,6 +147,13 @@ pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
|
||||
rt.updater.dismiss_status();
|
||||
}
|
||||
CheckResultResponse::CheckAgain => rt.updater.check_now(ctx),
|
||||
// The window closes, and the next frame's `show_if` asks the app to quit once the
|
||||
// shown document has no unsaved work.
|
||||
CheckResultResponse::RestartToUpdate => {
|
||||
rt.updater.restart_to_update();
|
||||
rt.manual_window_open = false;
|
||||
rt.updater.dismiss_status();
|
||||
}
|
||||
}
|
||||
// An update was found: the prompt replaces the dialog.
|
||||
if matches!(rt.updater.status(), Status::Idle) {
|
||||
@@ -178,4 +208,79 @@ mod tests {
|
||||
assert!(matches!(status, Status::Failed(why) if why.contains("another program's AppImage")), "{status:?}");
|
||||
assert!(!app.updates_rt.updater.prompt_open(), "no update offered");
|
||||
}
|
||||
|
||||
/// Help ▸ Check for Updates runs while the ribbon draws, after the frame's `tick`: the result
|
||||
/// window shows the check from the next frame on.
|
||||
#[test]
|
||||
fn a_check_asked_for_while_drawing_opens_the_result_window() {
|
||||
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
let pins = std::env::temp_dir().join(format!("wordcraft-no-pins-{}.toml", std::process::id()));
|
||||
app.updates_rt.updater = craft_update::updater::Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION"))
|
||||
.with_source(std::sync::Arc::new(|| Err("offline".into())))
|
||||
.with_pins_path(pins);
|
||||
let ctx = egui::Context::default();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
super::tick(&mut app, ui.ctx());
|
||||
super::check(&mut app);
|
||||
super::show(&mut app, ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
super::tick(&mut app, ui.ctx());
|
||||
super::show(&mut app, ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let status = app.updates_rt.updater.status();
|
||||
assert!(matches!(status, Status::Checking | Status::Failed(_)), "{status:?}");
|
||||
assert!(app.updates_rt.manual_window_open, "the result window shows the check ({status:?})");
|
||||
}
|
||||
|
||||
/// The first start after an update reports what it came to without a menu click: the check at
|
||||
/// start reads the record, the result window opens, and Esc closes it and clears the status.
|
||||
#[test]
|
||||
fn the_last_update_opens_the_result_window_until_it_is_closed() {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
// The files craft-update writes for an installed update and for a failed one.
|
||||
let cases: [(&str, &str, fn(&Status) -> bool); 2] = [
|
||||
("data/self-update-done.txt", version, |s| matches!(s, Status::Updated(v) if v == env!("CARGO_PKG_VERSION"))),
|
||||
("data/logs/self-update-error.txt", "disk full", |s| matches!(s, Status::InstallFailed(why) if why == "disk full")),
|
||||
];
|
||||
for (n, (file, text, expected)) in cases.into_iter().enumerate() {
|
||||
let root = std::env::temp_dir().join(format!("wordcraft-last-update-{n}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let record = root.join(file);
|
||||
std::fs::create_dir_all(record.parent().unwrap()).unwrap();
|
||||
std::fs::write(&record, text).unwrap();
|
||||
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
app.updates_rt.updater = craft_update::updater::Updater::new(&crate::UPDATE_APP, version)
|
||||
.with_source(std::sync::Arc::new(|| Ok(None)))
|
||||
.with_paths(craft_update::Paths::under(&root))
|
||||
.with_pins_path(root.join("no-pins.toml"));
|
||||
app.set_startup_update_check(true);
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut crate::WordApp, events: Vec<egui::Event>| {
|
||||
let input = egui::RawInput { events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
super::tick(app, ui.ctx());
|
||||
super::show(app, ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(&mut app, Vec::new());
|
||||
let status = app.updates_rt.updater.status();
|
||||
assert!(expected(status), "{file}: {status:?}");
|
||||
assert!(app.updates_rt.manual_window_open, "{file}: the result window opens by itself");
|
||||
assert!(!record.exists(), "{file}: the record is read once");
|
||||
frame(&mut app, Vec::new());
|
||||
assert!(app.updates_rt.manual_window_open, "{file}: the window stays open");
|
||||
let escape =
|
||||
egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
|
||||
frame(&mut app, vec![escape]);
|
||||
assert!(!app.updates_rt.manual_window_open, "{file}: Esc closes the window");
|
||||
assert_eq!(app.updates_rt.updater.status(), &Status::Idle, "{file}: closing clears the status");
|
||||
frame(&mut app, Vec::new());
|
||||
assert!(!app.updates_rt.manual_window_open, "{file}: the window stays closed");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,3 +530,24 @@ fn a_long_combo_list_builds_only_the_rows_in_view() {
|
||||
})
|
||||
.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
/// The tab strip exposes each tab as a button with its selected state, and a click on an
|
||||
/// unselected tab reports its index.
|
||||
#[test]
|
||||
fn dialog_tabs_expose_the_selected_state_and_report_clicks() {
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(300.0, 60.0)).build_ui_state(
|
||||
|ui, st: &mut (usize, Option<usize>)| {
|
||||
if let Some(i) = dialog_tabs(ui, &["One".to_string(), "Two".to_string()], st.0) {
|
||||
st.1 = Some(i);
|
||||
}
|
||||
},
|
||||
(0usize, None),
|
||||
);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.get_by_label("One").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True));
|
||||
assert_eq!(h.get_by_label("Two").accesskit_node().toggled(), Some(egui::accesskit::Toggled::False));
|
||||
h.get_by_label("Two").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().1, Some(1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user