mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-03 00:40:33 -06:00
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1789: cap font bytes waiting in the web inbox Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
Dmitrii Khlapov
Brandon Thomas
parent
a492189a84
commit
28d6260a81
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
|
||||
|
||||
@@ -156,6 +156,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-15 | Web: typing in Cyrillic or another non-Latin script no longer repeats text. WordCraft now honours the input method's request to erase around the caret before retyping (#1480). Chrome can still move the hidden text field's caret, so later letters may still land out of order. That cause is still open |
|
||||
| 2026-10-15 | Equations: typing `V_dd` or `x^ab` and pressing Space keeps `dd` and `ab` whole in the subscript or superscript, like `V_(dd)`, as Word for Mac does; Linear format shows the bare form |
|
||||
| 2026-10-15 | Plain text pasted from another program keeps the formatting of the paragraph it lands in: Paste inserts it as Keep Text Only does, so every pasted line, empty lines too, becomes a paragraph with that paragraph's direction, style, alignment, indents and list, and the text after the caret keeps them. The lines after the first had default formatting, so pasting several lines into a right-to-left paragraph turned all but the first left-to-right |
|
||||
| 2026-10-14 | Add Font… imports every file selected in the file chooser (#1789), not just the first: the desktop dialog and the web picker are multi-select, each file is added, a bad file is reported ("Added 11 fonts; 1 couldn't be read: X.ttf") without stopping the rest, and one pick adds at most 2,000 (the status line counts the skipped). On the web each font is added and reported on its own, with no single summary line. The built-in file browser still picks one file. |
|
||||
|---|---|
|
||||
| 2026-10-15 | The font family caches are bounded (4096 families for text, 1024 for math, names over 256 bytes aren't remembered), so a document naming thousands of missing fonts can't grow memory; equation fonts are looked up again after a font is added or removed instead of keeping the old choice (#1354) |
|
||||
| 2026-10-15 | AutoCorrect recognizes `-->` as `→` (#2223), respecting replacement settings and undo without changing paragraph-start arrow bullets. |
|
||||
@@ -498,6 +499,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-14 | trivial | Progress log: RTF export writes sections (gap 19) |
|
||||
| 2026-10-15 | trivial | Progress log: multi-letter equation scripts (#1609) |
|
||||
| 2026-10-15 | trivial | Progress log: plain-text paste keeps the paragraph formatting where it lands |
|
||||
| 2026-10-14 | trivial | Progress log: Add Font imports every selected font file (#1789) |
|
||||
| 2026-10-14 | trivial | Progress log: row table border exceptions |
|
||||
| 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks |
|
||||
| 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged |
|
||||
|
||||
@@ -224,13 +224,23 @@ fn services(inbox: Inbox, ctx: egui::Context, dirty: Rc<Cell<bool>>, saves: Asyn
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new().add_filter("Documents", DOC_EXTS)
|
||||
};
|
||||
let many = purpose == "font";
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let Some(file) = dialog.pick_file().await else {
|
||||
return;
|
||||
};
|
||||
let bytes = file.read().await;
|
||||
inbox.lock().unwrap_or_else(|e| e.into_inner()).push((file.file_name(), bytes));
|
||||
ctx.request_repaint();
|
||||
// Add Font takes every selected file (#1789); the others one.
|
||||
let files = if many { dialog.pick_files().await.unwrap_or_default() } else { dialog.pick_file().await.into_iter().collect() };
|
||||
for file in files.into_iter().take(MAX_PICKED_FILES) {
|
||||
let mut bytes = file.read().await;
|
||||
let mut pending = inbox.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Fonts waiting for the app to drain them stay under a byte cap. A file
|
||||
// over it is queued empty, so Add Font reports that file on its own.
|
||||
let held: usize = pending.iter().map(|(_, b)| b.len()).fold(0, usize::saturating_add);
|
||||
if many && held.saturating_add(bytes.len()) > MAX_PENDING_PICKED_BYTES {
|
||||
bytes = Vec::new();
|
||||
}
|
||||
pending.push((file.file_name(), bytes));
|
||||
drop(pending);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
})),
|
||||
// Exports ask for a name; the browser decides where the download goes.
|
||||
@@ -250,6 +260,10 @@ fn services(inbox: Inbox, ctx: egui::Context, dirty: Rc<Cell<bool>>, saves: Asyn
|
||||
}
|
||||
}
|
||||
|
||||
/// Most files one multi-select pick (Add Font) reads.
|
||||
const MAX_PICKED_FILES: usize = 2000;
|
||||
/// Most bytes of picked fonts held in the inbox before the app drains them.
|
||||
const MAX_PENDING_PICKED_BYTES: usize = 256 << 20;
|
||||
/// Most local fonts one Load System Installed Fonts installs (a designer's machine has thousands).
|
||||
const MAX_SYSTEM_FONTS: usize = 500;
|
||||
/// Most font bytes one Load System Installed Fonts reads into memory (wasm memory is small).
|
||||
|
||||
@@ -27,7 +27,7 @@ type Request = (FileDialogRequest, Sender<FileDialogAnswer>);
|
||||
pub type Hook = Box<dyn Fn(FileDialogRequest) -> Receiver<FileDialogAnswer>>;
|
||||
|
||||
/// A picker's answer, waited for off the UI thread.
|
||||
type Picking = Pin<Box<dyn Future<Output = Option<rfd::FileHandle>> + Send>>;
|
||||
type Picking = Pin<Box<dyn Future<Output = Vec<rfd::FileHandle>> + Send>>;
|
||||
|
||||
/// Shows the dialogs the app queued; lives in the eframe app, which has the window.
|
||||
pub struct Launcher {
|
||||
@@ -56,13 +56,18 @@ impl Launcher {
|
||||
// Built here, on the UI thread: macOS shows the panel as a sheet on the window now.
|
||||
let dialog = rfd::AsyncFileDialog::new().set_parent(parent);
|
||||
let picking: Picking = match req {
|
||||
FileDialogRequest::Open { purpose } => Box::pin(open_filters(dialog, &purpose).pick_file()),
|
||||
FileDialogRequest::Open { purpose } => {
|
||||
Box::pin(async move { open_filters(dialog, &purpose).pick_file().await.into_iter().collect() })
|
||||
}
|
||||
FileDialogRequest::OpenMany { purpose } => {
|
||||
Box::pin(async move { open_filters(dialog, &purpose).pick_files().await.unwrap_or_default() })
|
||||
}
|
||||
FileDialogRequest::Save { name } => {
|
||||
let mut dialog = dialog.set_file_name(name.clone());
|
||||
for (label, exts) in save_filters(&name) {
|
||||
dialog = dialog.add_filter(label, &exts);
|
||||
}
|
||||
Box::pin(dialog.save_file())
|
||||
Box::pin(async move { dialog.save_file().await.into_iter().collect() })
|
||||
}
|
||||
};
|
||||
let ctx = self.ctx.clone();
|
||||
@@ -70,7 +75,7 @@ impl Launcher {
|
||||
// The picker only starts when first polled: without a system dialog it never does.
|
||||
let picked = match no_system_dialog() {
|
||||
Some(reason) => Err(reason),
|
||||
None => Ok(pollster::block_on(picking).map(|f| f.path().to_string_lossy().to_string())),
|
||||
None => Ok(pollster::block_on(picking).iter().map(|f| f.path().to_string_lossy().to_string()).collect()),
|
||||
};
|
||||
let _ = answer.send(picked);
|
||||
ctx.request_repaint();
|
||||
|
||||
@@ -147,7 +147,7 @@ pub(crate) struct FileBrowser {
|
||||
impl FileBrowser {
|
||||
pub(crate) fn new(req: FileDialogRequest, after: AfterPick, start: PathBuf, unavailable: bool) -> Self {
|
||||
let (name, filters) = match &req {
|
||||
FileDialogRequest::Open { purpose } => {
|
||||
FileDialogRequest::Open { purpose } | FileDialogRequest::OpenMany { purpose } => {
|
||||
(String::new(), open_filters(purpose).iter().map(|(l, e)| ((*l).to_string(), e.iter().map(|x| (*x).to_string()).collect())).collect())
|
||||
}
|
||||
FileDialogRequest::Save { name } => {
|
||||
@@ -209,7 +209,7 @@ impl FileBrowser {
|
||||
return Step::Stay;
|
||||
}
|
||||
match self.req {
|
||||
FileDialogRequest::Open { .. } => {
|
||||
FileDialogRequest::Open { .. } | FileDialogRequest::OpenMany { .. } => {
|
||||
if path.is_file() {
|
||||
return Step::Pick(path);
|
||||
}
|
||||
@@ -242,14 +242,14 @@ impl FileBrowser {
|
||||
|
||||
fn title(&self) -> &'static str {
|
||||
match self.req {
|
||||
FileDialogRequest::Open { .. } => "Open",
|
||||
FileDialogRequest::Open { .. } | FileDialogRequest::OpenMany { .. } => "Open",
|
||||
FileDialogRequest::Save { .. } => "Save As",
|
||||
}
|
||||
}
|
||||
|
||||
fn ok_label(&self) -> &'static str {
|
||||
match self.req {
|
||||
FileDialogRequest::Open { .. } => "Open",
|
||||
FileDialogRequest::Open { .. } | FileDialogRequest::OpenMany { .. } => "Open",
|
||||
FileDialogRequest::Save { .. } => "Save",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,19 @@ pub enum FileDialogRequest {
|
||||
/// Pick a file to open; `purpose` says what for (`document`, `picture`, `recipients`, `font`), as for
|
||||
/// `pick_open`.
|
||||
Open { purpose: String },
|
||||
/// Pick one or more files (Add Font…); the answer lists every one picked.
|
||||
OpenMany { purpose: String },
|
||||
/// Pick a path to save to, given a suggested file name.
|
||||
Save { name: String },
|
||||
}
|
||||
|
||||
/// The host's answer to a [`FileDialogRequest`]: the picked path, `Ok(None)` when the user
|
||||
/// cancelled, or an error when no system dialog could be shown (the app then shows its own).
|
||||
pub type FileDialogAnswer = Result<Option<String>, String>;
|
||||
/// The host's answer to a [`FileDialogRequest`]: the picked paths (one for everything but
|
||||
/// [`FileDialogRequest::OpenMany`]), empty when the user cancelled, or an error when no system
|
||||
/// dialog could be shown (the app then shows its own).
|
||||
pub type FileDialogAnswer = Result<Vec<String>, String>;
|
||||
|
||||
/// Most font files one Add Font pick tries; the rest of a bigger selection is skipped.
|
||||
const MAX_FONTS_PER_PICK: usize = 2000;
|
||||
|
||||
const PICTURES: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "wmf", "emf"];
|
||||
const DOCUMENTS: &[&str] = &["docx", "docm", "dotx", "dotm", "doc", "dot", "odt", "rtf", "txt", "md", "html", "htm", "tex", "json", "xml"];
|
||||
@@ -60,7 +66,7 @@ pub(crate) enum AfterPick {
|
||||
PrintPdf { settings: Value },
|
||||
/// Load the picked CSV/TSV/text file or spreadsheet as the mail-merge recipients (#240, #334).
|
||||
Recipients,
|
||||
/// Add the picked font file (#582).
|
||||
/// Add every picked font file (#582, #1789).
|
||||
Font,
|
||||
/// Hand the picked path to the open dialog (`WordApp::dialog_pick`): Templates and Add-ins,
|
||||
/// Organizer (#377).
|
||||
@@ -131,7 +137,9 @@ impl WordApp {
|
||||
return self.poll_file_dialog().map_or(Asked::Pending, Asked::Done);
|
||||
}
|
||||
let picked = match &req {
|
||||
FileDialogRequest::Open { purpose } => self.services.pick_open.as_ref().and_then(|f| f(purpose)),
|
||||
FileDialogRequest::Open { purpose } | FileDialogRequest::OpenMany { purpose } => {
|
||||
self.services.pick_open.as_ref().and_then(|f| f(purpose))
|
||||
}
|
||||
FileDialogRequest::Save { name } => self.services.pick_save.as_ref().and_then(|f| f(name)),
|
||||
};
|
||||
Asked::Done(self.finish_pick(after, picked))
|
||||
@@ -144,11 +152,11 @@ impl WordApp {
|
||||
None | Some(Err(TryRecvError::Empty)) => return None,
|
||||
Some(Ok(answer)) => answer,
|
||||
// The host dropped the dialog without an answer: as good as cancelled.
|
||||
Some(Err(TryRecvError::Disconnected)) => Ok(None),
|
||||
Some(Err(TryRecvError::Disconnected)) => Ok(Vec::new()),
|
||||
};
|
||||
let d = self.file_dialog.take()?;
|
||||
match answer {
|
||||
Ok(picked) => Some(self.finish_pick(d.after, picked)),
|
||||
Ok(picked) => Some(self.finish_picks(d.after, picked)),
|
||||
Err(reason) => {
|
||||
log::warn!("no system file dialog ({reason}); showing the built-in file browser");
|
||||
self.dialog_unavailable(d.req, d.after)
|
||||
@@ -178,6 +186,53 @@ impl WordApp {
|
||||
Some(self.finish_pick(after, None))
|
||||
}
|
||||
|
||||
/// Do what the dialog was for with the picked paths (none: the user cancelled). Only Add Font
|
||||
/// takes more than one; the others use the first.
|
||||
fn finish_picks(&mut self, after: AfterPick, picked: Vec<String>) -> Result<Value, String> {
|
||||
match after {
|
||||
AfterPick::Font if !picked.is_empty() => self.add_fonts(picked),
|
||||
after => self.finish_pick(after, picked.into_iter().next()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add each picked font file, going on past the ones that fail, and say how it went in one
|
||||
/// status line (#1789). One file keeps `font.add`'s own result and message.
|
||||
fn add_fonts(&mut self, mut paths: Vec<String>) -> Result<Value, String> {
|
||||
if paths.len() == 1 {
|
||||
return self.run("font.add", json!({"path": paths.remove(0)}));
|
||||
}
|
||||
let skipped = paths.len().saturating_sub(MAX_FONTS_PER_PICK);
|
||||
paths.truncate(MAX_FONTS_PER_PICK);
|
||||
let (mut added, mut failed) = (0usize, Vec::new());
|
||||
for path in paths {
|
||||
match self.run("font.add", json!({"path": path})) {
|
||||
Ok(_) => added += 1,
|
||||
Err(e) => {
|
||||
log::warn!("couldn't add font {path}: {e}");
|
||||
let name = std::path::Path::new(&path).file_name().map_or(path.clone(), |n| n.to_string_lossy().into_owned());
|
||||
failed.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = added.to_string();
|
||||
let mut msg = if added == 1 { tl!("Added 1 font").to_string() } else { crate::i18n::fmt(tl!("Added {n} fonts"), &[("n", &n)]) };
|
||||
if !failed.is_empty() {
|
||||
let count = failed.len().to_string();
|
||||
let mut names = failed.iter().take(3).cloned().collect::<Vec<_>>().join(", ");
|
||||
if failed.len() > 3 {
|
||||
let more = (failed.len() - 3).to_string();
|
||||
names = crate::i18n::fmt(tl!("{names} and {n} more"), &[("names", &names), ("n", &more)]);
|
||||
}
|
||||
msg = crate::i18n::fmt(tl!("{added}; {count} couldn't be read: {names}"), &[("added", &msg), ("count", &count), ("names", &names)]);
|
||||
}
|
||||
if skipped > 0 {
|
||||
let (n, max) = (skipped.to_string(), MAX_FONTS_PER_PICK.to_string());
|
||||
msg = crate::i18n::fmt(tl!("{added}; {n} skipped (limit {max})"), &[("added", &msg), ("n", &n), ("max", &max)]);
|
||||
}
|
||||
self.status(msg);
|
||||
Ok(json!({"added": added, "failed": failed, "skipped": skipped}))
|
||||
}
|
||||
|
||||
/// Do what the dialog was for with the picked path (`None`: the user cancelled).
|
||||
pub(crate) fn finish_pick(&mut self, after: AfterPick, picked: Option<String>) -> Result<Value, String> {
|
||||
match after {
|
||||
@@ -196,7 +251,7 @@ impl WordApp {
|
||||
}
|
||||
AfterPick::Font => {
|
||||
let Some(path) = picked else { return Ok(json!({"cancelled": true})) };
|
||||
self.run("font.add", json!({"path": path}))
|
||||
self.add_fonts(vec![path])
|
||||
}
|
||||
AfterPick::ForDialog => {
|
||||
let picked = picked.filter(|p| !p.is_empty());
|
||||
@@ -315,7 +370,7 @@ impl WordApp {
|
||||
f("font");
|
||||
return;
|
||||
}
|
||||
let _ = self.ask_file(FileDialogRequest::Open { purpose: "font".into() }, AfterPick::Font);
|
||||
let _ = self.ask_file(FileDialogRequest::OpenMany { purpose: "font".into() }, AfterPick::Font);
|
||||
}
|
||||
|
||||
/// Request loading local system installed fonts (web: queryLocalFonts).
|
||||
@@ -360,8 +415,13 @@ mod tests {
|
||||
|
||||
/// Answer the dialog the host is showing (the last one asked for).
|
||||
fn answer(shown: &Shown, picked: Option<&str>) {
|
||||
answer_all(shown, picked.as_slice());
|
||||
}
|
||||
|
||||
/// Answer the dialog with several picked paths (none: cancelled).
|
||||
fn answer_all(shown: &Shown, picked: &[&str]) {
|
||||
let (_, tx) = shown.borrow_mut().pop().unwrap();
|
||||
tx.send(Ok(picked.map(str::to_string))).unwrap();
|
||||
tx.send(Ok(picked.iter().map(|p| p.to_string()).collect())).unwrap();
|
||||
}
|
||||
|
||||
fn typed(a: &mut WordApp) {
|
||||
@@ -619,4 +679,99 @@ mod tests {
|
||||
a.run("font.loadSystem", json!({})).unwrap();
|
||||
assert_eq!(a.status_msg.as_ref().map(|(s, _)| s.as_str()), Some("All system fonts are already available."));
|
||||
}
|
||||
|
||||
/// The bundled face `file` of family `original` renamed `family` (as long as `original`), so it
|
||||
/// is a font the database doesn't have.
|
||||
fn renamed_font(file: &str, original: &str, family: &str) -> Vec<u8> {
|
||||
assert_eq!(family.len(), original.len());
|
||||
let mut data = std::fs::read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts").join(file)).unwrap();
|
||||
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
|
||||
for (from, to) in [(utf16(original), utf16(family)), (original.as_bytes().to_vec(), family.as_bytes().to_vec())] {
|
||||
let mut i = 0;
|
||||
while let Some(at) = data.get(i..).and_then(|d| d.windows(from.len()).position(|w| w == from)) {
|
||||
data[i + at..i + at + to.len()].copy_from_slice(&to);
|
||||
i += at + to.len();
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn font_families(a: &mut WordApp) -> Vec<String> {
|
||||
let list = a.run("font.list", json!({"all": true})).unwrap();
|
||||
list["families"].as_array().unwrap().iter().filter_map(|f| f.as_str().map(str::to_string)).collect()
|
||||
}
|
||||
|
||||
/// #1789: Add Font asks for several files, and every one picked is added; an unreadable file
|
||||
/// is reported and the rest still go in.
|
||||
#[test]
|
||||
fn add_font_imports_every_selected_file() {
|
||||
const ALPHA: &str = "Mfpick Alpha 1";
|
||||
const BRAVO: &str = "Mfpick Bravo 2";
|
||||
let dir = scratch("fonts");
|
||||
let alpha = dir.join("MfpickAlpha1-Regular.ttf");
|
||||
let junk = dir.join("Junk.ttf");
|
||||
let bravo = dir.join("MfpickBravo2-Regular.ttf");
|
||||
std::fs::write(&alpha, renamed_font("JetBrainsMono-Regular.ttf", "JetBrains Mono", ALPHA)).unwrap();
|
||||
std::fs::write(&junk, b"this is not a font").unwrap();
|
||||
std::fs::write(&bravo, renamed_font("JetBrainsMono-Regular.ttf", "JetBrains Mono", BRAVO)).unwrap();
|
||||
|
||||
let (mut a, shown) = async_app();
|
||||
a.pick_font();
|
||||
assert_eq!(shown.borrow()[0].0, FileDialogRequest::OpenMany { purpose: "font".into() }, "Add Font is multi-select");
|
||||
let paths: Vec<String> = [&alpha, &junk, &bravo].iter().map(|p| p.to_string_lossy().into_owned()).collect();
|
||||
answer_all(&shown, &paths.iter().map(String::as_str).collect::<Vec<_>>());
|
||||
let r = a.poll_file_dialog().unwrap().unwrap();
|
||||
assert_eq!(r["added"], 2);
|
||||
assert_eq!(r["failed"].as_array().map(Vec::len), Some(1));
|
||||
let families = font_families(&mut a);
|
||||
assert!(families.iter().any(|f| f == ALPHA) && families.iter().any(|f| f == BRAVO), "both valid fonts were added");
|
||||
let msg = a.status_msg.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
|
||||
assert_eq!(msg, "Added 2 fonts; 1 couldn't be read: Junk.ttf");
|
||||
for f in [ALPHA, BRAVO] {
|
||||
a.run("font.remove", json!({"family": f})).unwrap();
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Only Add Font is multi-select; a cancelled pick adds nothing.
|
||||
#[test]
|
||||
fn only_add_font_asks_for_several_files() {
|
||||
let (mut a, shown) = async_app();
|
||||
a.open_dialog();
|
||||
assert_eq!(shown.borrow()[0].0, FileDialogRequest::Open { purpose: "document".into() });
|
||||
answer(&shown, None);
|
||||
a.poll_file_dialog();
|
||||
a.pick_picture();
|
||||
assert_eq!(shown.borrow()[0].0, FileDialogRequest::Open { purpose: "picture".into() });
|
||||
answer(&shown, None);
|
||||
a.poll_file_dialog();
|
||||
a.pick_font();
|
||||
assert!(matches!(shown.borrow()[0].0, FileDialogRequest::OpenMany { .. }));
|
||||
answer_all(&shown, &[]);
|
||||
assert_eq!(a.poll_file_dialog().unwrap().unwrap(), json!({"cancelled": true}));
|
||||
assert!(!a.file_dialog_open());
|
||||
}
|
||||
|
||||
/// A hostile selection is bounded: only the first MAX_FONTS_PER_PICK are tried.
|
||||
#[test]
|
||||
fn a_huge_font_selection_is_capped() {
|
||||
let (mut a, _) = async_app();
|
||||
let paths: Vec<String> = (0..MAX_FONTS_PER_PICK + 50).map(|i| format!("/nonexistent/f{i}.ttf")).collect();
|
||||
let r = a.add_fonts(paths).unwrap();
|
||||
assert_eq!(r["added"], 0);
|
||||
assert_eq!(r["failed"].as_array().map(Vec::len), Some(MAX_FONTS_PER_PICK));
|
||||
assert_eq!(r["skipped"], 50);
|
||||
let msg = a.status_msg.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
|
||||
assert_eq!(msg, "Added 0 fonts; 2000 couldn't be read: f0.ttf, f1.ttf, f2.ttf and 1997 more; 50 skipped (limit 2000)");
|
||||
}
|
||||
|
||||
/// Past three failures the status line names three and counts the rest.
|
||||
#[test]
|
||||
fn many_failed_fonts_end_with_a_count() {
|
||||
let (mut a, _) = async_app();
|
||||
let paths: Vec<String> = (0..5).map(|i| format!("/nonexistent/f{i}.ttf")).collect();
|
||||
a.add_fonts(paths).unwrap();
|
||||
let msg = a.status_msg.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
|
||||
assert_eq!(msg, "Added 0 fonts; 5 couldn't be read: f0.ttf, f1.ttf, f2.ttf and 2 more");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2645,3 +2645,9 @@
|
||||
All Fonts Всички шрифтове
|
||||
Spike Натрупване
|
||||
Insert Spike Вмъкване на натрупаното
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Добавен 1 шрифт
|
||||
Added {n} fonts Добавени шрифтове: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; не можаха да се прочетат ({count}): {names}
|
||||
{names} and {n} more {names} и още {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; пропуснати: {n} (ограничение {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2648,3 +2648,9 @@ preview Title Ukázka
|
||||
All Fonts Všechna písma
|
||||
Spike Sběrač
|
||||
Insert Spike Vložit ze sběrače
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Přidáno 1 písmo
|
||||
Added {n} fonts Přidaná písma: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; nelze přečíst ({count}): {names}
|
||||
{names} and {n} more {names} a dalších {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; přeskočeno: {n} (limit {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2256,3 +2256,9 @@
|
||||
All Fonts Alle Schriftarten
|
||||
Spike Sammlung
|
||||
Insert Spike Sammlung einfügen
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 Schriftart hinzugefügt
|
||||
Added {n} fonts {n} Schriftarten hinzugefügt
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} nicht lesbar: {names}
|
||||
{names} and {n} more {names} und {n} weitere
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} übersprungen (Limit {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2623,3 +2623,9 @@ preview Title Título
|
||||
All Fonts Todas las fuentes
|
||||
Spike Acumulador
|
||||
Insert Spike Insertar acumulador
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 fuente añadida
|
||||
Added {n} fonts {n} fuentes añadidas
|
||||
{added}; {count} couldn't be read: {names} {added}; no se pudieron leer {count}: {names}
|
||||
{names} and {n} more {names} y {n} más
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} omitidas (límite {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2291,3 +2291,9 @@
|
||||
All Fonts Kõik fondid
|
||||
Spike Koguja
|
||||
Insert Spike Lisa kogujast
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Lisati 1 font
|
||||
Added {n} fonts Lisati {n} fonti
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} ei õnnestunud lugeda: {names}
|
||||
{names} and {n} more {names} ja veel {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} jäeti vahele (piir {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 845 and column 23.
|
@@ -2264,3 +2264,9 @@
|
||||
All Fonts Toutes les polices
|
||||
Spike Pique-notes
|
||||
Insert Spike Insérer le pique-notes
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 police ajoutée
|
||||
Added {n} fonts {n} polices ajoutées
|
||||
{added}; {count} couldn't be read: {names} {added} ; {count} illisibles : {names}
|
||||
{names} and {n} more {names} et {n} autres
|
||||
{added}; {n} skipped (limit {max}) {added} ; {n} ignorées (limite {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2656,3 +2656,9 @@ preview Title Naslov
|
||||
Moved to: Premješteno u:
|
||||
Spike Skupljač
|
||||
Insert Spike Umetni iz skupljača
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Dodan 1 font
|
||||
Added {n} fonts Dodano fontova: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; nije moguće pročitati ({count}): {names}
|
||||
{names} and {n} more {names} i još {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; preskočeno: {n} (ograničenje {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2652,3 +2652,9 @@
|
||||
All Fonts Összes betűtípus
|
||||
Spike Gyűjtő
|
||||
Insert Spike Beszúrás a gyűjtőből
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 betűtípus hozzáadva
|
||||
Added {n} fonts {n} betűtípus hozzáadva
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} nem olvasható: {names}
|
||||
{names} and {n} more {names} és még {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} kihagyva (korlát: {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2665,3 +2665,9 @@ preview Title Esempio
|
||||
All Fonts Tutti i caratteri
|
||||
Spike Raccoglitore
|
||||
Insert Spike Inserisci raccoglitore
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 tipo di carattere aggiunto
|
||||
Added {n} fonts {n} tipi di carattere aggiunti
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} non leggibili: {names}
|
||||
{names} and {n} more {names} e altri {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} saltati (limite {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2257,3 +2257,9 @@
|
||||
All Fonts すべてのフォント
|
||||
Spike スパイク
|
||||
Insert Spike スパイクの挿入
|
||||
# Add Font status (#1789)
|
||||
Added 1 font フォントを 1 件追加しました
|
||||
Added {n} fonts フォントを {n} 件追加しました
|
||||
{added}; {count} couldn't be read: {names} {added}。読み込めなかったもの {count} 件: {names}
|
||||
{names} and {n} more {names} ほか {n} 件
|
||||
{added}; {n} skipped (limit {max}) {added}。{n} 件をスキップしました (上限 {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 923 and column 23.
|
@@ -2510,3 +2510,9 @@ preview Title 제목
|
||||
All Fonts 모든 글꼴
|
||||
Spike 스파이크
|
||||
Insert Spike 스파이크 삽입
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 글꼴 1개를 추가했습니다
|
||||
Added {n} fonts 글꼴 {n}개를 추가했습니다
|
||||
{added}; {count} couldn't be read: {names} {added}; 읽을 수 없는 파일 {count}개: {names}
|
||||
{names} and {n} more {names} 외 {n}개
|
||||
{added}; {n} skipped (limit {max}) {added}; {n}개 건너뜀 (한도 {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 929 and column 23.
|
@@ -2661,3 +2661,9 @@ preview Title Tytuł
|
||||
All Fonts Wszystkie czcionki
|
||||
Spike Zbieracz
|
||||
Insert Spike Wstaw ze zbieracza
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Dodano 1 czcionkę
|
||||
Added {n} fonts Dodano czcionek: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; nie udało się odczytać ({count}): {names}
|
||||
{names} and {n} more {names} i {n} więcej
|
||||
{added}; {n} skipped (limit {max}) {added}; pominięto: {n} (limit {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2269,3 +2269,9 @@
|
||||
All Fonts Todas as fontes
|
||||
Spike Coletor
|
||||
Insert Spike Inserir coletor
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 fonte adicionada
|
||||
Added {n} fonts {n} fontes adicionadas
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} não puderam ser lidas: {names}
|
||||
{names} and {n} more {names} e mais {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} ignoradas (limite de {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2654,3 +2654,9 @@ preview Title Пример
|
||||
All Fonts Все шрифты
|
||||
Spike Копилка
|
||||
Insert Spike Вставить из копилки
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Добавлен 1 шрифт
|
||||
Added {n} fonts Добавлено шрифтов: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; не удалось прочитать ({count}): {names}
|
||||
{names} and {n} more {names} и ещё {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; пропущено: {n} (лимит {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2267,3 +2267,9 @@
|
||||
All Fonts Svi fontovi
|
||||
Spike Sakupljač
|
||||
Insert Spike Umetni iz sakupljača
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Dodat je 1 font
|
||||
Added {n} fonts Dodato fontova: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; nije moguće pročitati ({count}): {names}
|
||||
{names} and {n} more {names} i još {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; preskočeno: {n} (ograničenje {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -2271,3 +2271,9 @@
|
||||
All Fonts Сви фонтови
|
||||
Spike Сакупљач
|
||||
Insert Spike Уметни из сакупљача
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Додат је 1 фонт
|
||||
Added {n} fonts Додато фонтова: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; није могуће прочитати ({count}): {names}
|
||||
{names} and {n} more {names} и још {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; прескочено: {n} (ограничење {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
@@ -2671,3 +2671,9 @@ preview Title Titel
|
||||
Moved to: Flyttat till:
|
||||
Spike Samlare
|
||||
Insert Spike Infoga samlare
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 teckensnitt har lagts till
|
||||
Added {n} fonts {n} teckensnitt har lagts till
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} kunde inte läsas: {names}
|
||||
{names} and {n} more {names} och {n} till
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} hoppades över (gräns {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2653,3 +2653,9 @@ preview Title Örnek
|
||||
All Fonts Tüm Yazı Tipleri
|
||||
Spike Biriktirici
|
||||
Insert Spike Biriktiriciyi Ekle
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 1 yazı tipi eklendi
|
||||
Added {n} fonts {n} yazı tipi eklendi
|
||||
{added}; {count} couldn't be read: {names} {added}; {count} okunamadı: {names}
|
||||
{names} and {n} more {names} ve {n} tane daha
|
||||
{added}; {n} skipped (limit {max}) {added}; {n} atlandı (sınır {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 797 and column 23.
|
@@ -2269,3 +2269,9 @@
|
||||
All Fonts Усі шрифти
|
||||
Spike Накопичувач
|
||||
Insert Spike Вставити з накопичувача
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Додано 1 шрифт
|
||||
Added {n} fonts Додано шрифтів: {n}
|
||||
{added}; {count} couldn't be read: {names} {added}; не вдалося прочитати ({count}): {names}
|
||||
{names} and {n} more {names} та ще {n}
|
||||
{added}; {n} skipped (limit {max}) {added}; пропущено: {n} (ліміт {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
@@ -2696,3 +2696,9 @@ template Letter Thư
|
||||
All Fonts Tất cả phông chữ
|
||||
Spike Bộ gom
|
||||
Insert Spike Chèn bộ gom
|
||||
# Add Font status (#1789)
|
||||
Added 1 font Đã thêm 1 phông chữ
|
||||
Added {n} fonts Đã thêm {n} phông chữ
|
||||
{added}; {count} couldn't be read: {names} {added}; không đọc được {count}: {names}
|
||||
{names} and {n} more {names} và {n} tệp nữa
|
||||
{added}; {n} skipped (limit {max}) {added}; đã bỏ qua {n} (giới hạn {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2269,3 +2269,9 @@
|
||||
All Fonts 所有字体
|
||||
Spike 图文场
|
||||
Insert Spike 插入图文场
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 已添加 1 个字体
|
||||
Added {n} fonts 已添加 {n} 个字体
|
||||
{added}; {count} couldn't be read: {names} {added};{count} 个无法读取:{names}
|
||||
{names} and {n} more {names} 等另外 {n} 个
|
||||
{added}; {n} skipped (limit {max}) {added};已跳过 {n} 个(上限 {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -2258,3 +2258,9 @@
|
||||
All Fonts 所有字型
|
||||
Spike 收集區
|
||||
Insert Spike 插入收集區
|
||||
# Add Font status (#1789)
|
||||
Added 1 font 已新增 1 個字型
|
||||
Added {n} fonts 已新增 {n} 個字型
|
||||
{added}; {count} couldn't be read: {names} {added};{count} 個無法讀取:{names}
|
||||
{names} and {n} more {names} 等另外 {n} 個
|
||||
{added}; {n} skipped (limit {max}) {added};已略過 {n} 個(上限 {max})
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 924 and column 23.
|
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (File › Close keeps the window, #1712; previously trivial: the Spike: Ctrl+F3 and Ctrl+Shift+F3; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (Add Font imports every selected font file, #1789); previously trivial (File › Close keeps the window, #1712; previously trivial: the Spike: Ctrl+F3 and Ctrl+Shift+F3; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
|
||||
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
|
||||
@@ -89,6 +89,7 @@ Find selects the Results tab, including when it prefills selected text. Editing
|
||||
| 2026-10-16 | trivial | The Spike: Ctrl+F3 (⌘F3 on macOS) cuts the selection to the end of the Spike, Ctrl+Shift+F3 (⌘⇧F3) inserts everything collected and empties it; it is the AutoText entry "Spike", so Quick Parts inserts it without emptying it. Per session, not saved between launches |
|
||||
| 2026-10-15 | trivial | Table Layout › Cell Margins opens with current table defaults; unchanged margins and per-cell overrides stay intact until edits are confirmed (#1960) |
|
||||
| 2026-10-15 | trivial | Paste (Ctrl+V, Home › Paste) of plain text from another program: every pasted line takes the formatting of the paragraph it lands in, as with Keep Text Only, so a right-to-left paragraph stays right-to-left after the first line. The lines after the first had default formatting |
|
||||
| 2026-10-14 | trivial | Add Font… takes several files at once (desktop dialog and web picker are multi-select); each is added, a bad file is reported and skipped, at most 2,000 per pick (the status line counts the skipped); on the web each font is added and reported on its own, with no single summary line; the built-in browser still picks one file (#1789) |
|
||||
| 2026-10-13 | trivial | New/Modify Table Style: No Border on a part that inherits its lines hides them; emptying the font box makes the part's font inherited again (#1288) |
|
||||
| 2026-10-14 | trivial | Tab stops edited on the ruler: corner kind selector, click to set, drag to move or off to clear, per-kind markers mirrored for RTL; each gesture one `para.tabs` call and one undo step; Rulers 85% → 90% |
|
||||
| 2026-10-12 | trivial | Tables on the page: one selected cell (Select Cell, or a click along its left edge) is a cell selection when its border is dragged: the border moves in its row only (#962) |
|
||||
|
||||
Reference in New Issue
Block a user