Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
39f51ca292
commit
381c475edc
|
|
@ -132,6 +132,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
|||
| Date | What landed |
|
||||
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|
||||
|---|---|
|
||||
| 2026-10-11 | File › Print: page preview, page range (all, current, selected, custom 1-3,5), odd/even, copies and collate go into the printed PDF (`file.printPdf`, `ui.print` params); printer, duplex and pages per sheet are still the system dialog's (#641, stage 1) |
|
||||
| 2026-10-11 | A vertically merged table cell (eraser on a horizontal border) lays out, shows and clicks as one cell spanning its rows (#640) |
|
||||
| 2026-10-11 | Word XML files (#548): Word XML Document (Flat OPC `.xml`) opens and saves through the DOCX reader and writer; Word 2003 XML opens (styles, lists, tables, sections, notes, comments, pictures, hyperlinks) |
|
||||
| 2026-10-11 | Font fallback indexes which characters each installed font maps, reading each font file once (#605): text in a script no installed font covers no longer re-reads every system font per distinct character (200 such characters: 13 s → 0.2 s) |
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
CommandSpec::new("file.exportPng", "Export Page as PNG", "File › Export", export_png)
|
||||
.params(r#"{"path": string, "page"?: n (1-based), "scale"?: px per pt}"#)
|
||||
.pure(),
|
||||
CommandSpec::new("file.printPdf", "Print to PDF", "File › Print", crate::print::print_pdf_to_file)
|
||||
.params(
|
||||
r#"{"path": string, "range"?: "all" | "current" | "selection" (the pages the selection touches) | "custom", "pages"?: "1-3,5" (implies custom), "parity"?: "all" | "odd" | "even", "copies"?: 1-999, "collate"?: bool (default true)}"#,
|
||||
)
|
||||
.pure(),
|
||||
CommandSpec::new("file.print", "Print", "File", |s, _| {
|
||||
s.ui_requests.push(json!({"open": "print"}));
|
||||
sel_result(s)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub mod io;
|
|||
mod io_ext;
|
||||
pub mod keymap;
|
||||
pub mod math_gallery;
|
||||
pub mod print;
|
||||
pub mod ribbon;
|
||||
pub mod sample;
|
||||
mod session;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
//! Print settings (#641): which pages go out, how many copies, as a PDF.
|
||||
//!
|
||||
//! WordCraft prints through a PDF (the host opens it in the system's print flow), so the settings
|
||||
//! that can be delivered are the ones that shape the PDF: the page range, odd/even pages, copies
|
||||
//! and collation. Printer choice, duplex and pages per sheet belong to the OS print dialog.
|
||||
//!
|
||||
//! Page ranges and copies come from users and control channels, so they are bounded: a range
|
||||
//! text is at most [`MAX_SPEC_CHARS`] long, copies are clamped to [`MAX_COPIES`], and the
|
||||
//! resulting page list never exceeds [`MAX_PRINT_PAGES`].
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use wordcraft_layout::DocLayout;
|
||||
|
||||
use crate::{CmdError, Session, p};
|
||||
|
||||
/// Longest page-range text accepted.
|
||||
pub const MAX_SPEC_CHARS: usize = 500;
|
||||
/// Most copies one print job asks for.
|
||||
pub const MAX_COPIES: u32 = 999;
|
||||
/// Most pages (counting copies) one print job holds.
|
||||
pub const MAX_PRINT_PAGES: usize = 5000;
|
||||
|
||||
/// Parses a custom page range like `1-3, 5, 8-` into 0-based page indexes, in the order typed
|
||||
/// (Word prints them in document order; so does this: sorted, without repeats). Pages are
|
||||
/// 1-based in the text; `8-` means "to the last page" and `-3` "from the first". An empty text,
|
||||
/// page 0, a page past `total`, a reversed range or anything else is an error.
|
||||
pub fn parse_page_range(spec: &str, total: usize) -> Result<Vec<usize>, String> {
|
||||
if spec.chars().count() > MAX_SPEC_CHARS {
|
||||
return Err("the page range is too long".into());
|
||||
}
|
||||
if total == 0 {
|
||||
return Err("the document has no pages".into());
|
||||
}
|
||||
let mut keep = vec![false; total];
|
||||
let mut any = false;
|
||||
for item in spec.split([',', ';']) {
|
||||
let item: String = item.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
if item.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let page = |t: &str| -> Result<usize, String> {
|
||||
let n: usize = t.parse().map_err(|_| format!("`{item}` isn't a page or a range like 2-4"))?;
|
||||
if n == 0 || n > total {
|
||||
return Err(format!("page {n} doesn't exist; the document has {total} page{}", if total == 1 { "" } else { "s" }));
|
||||
}
|
||||
Ok(n)
|
||||
};
|
||||
let (from, to) = match item.split_once('-') {
|
||||
None => {
|
||||
let n = page(&item)?;
|
||||
(n, n)
|
||||
}
|
||||
Some(("", "")) => return Err("`-` isn't a page or a range like 2-4".into()),
|
||||
Some((a, b)) => {
|
||||
let from = if a.is_empty() { 1 } else { page(a)? };
|
||||
let to = if b.is_empty() { total } else { page(b)? };
|
||||
if from > to {
|
||||
return Err(format!("`{item}` runs backwards"));
|
||||
}
|
||||
(from, to)
|
||||
}
|
||||
};
|
||||
for flag in keep.iter_mut().take(to).skip(from - 1) {
|
||||
*flag = true;
|
||||
}
|
||||
any = true;
|
||||
}
|
||||
if !any {
|
||||
return Err("type the pages to print, like 1-3, 5".into());
|
||||
}
|
||||
Ok(keep.iter().enumerate().filter(|(_, k)| **k).map(|(i, _)| i).collect())
|
||||
}
|
||||
|
||||
/// What a print job holds: the pages (0-based, copies included, in print order).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PrintPlan {
|
||||
/// Page indexes in print order, repeated for copies.
|
||||
pub pages: Vec<usize>,
|
||||
/// Distinct pages chosen (before copies).
|
||||
pub chosen: usize,
|
||||
pub copies: u32,
|
||||
}
|
||||
|
||||
/// Works out the print plan for `v` against a layout of `total` pages whose caret and selection
|
||||
/// sit on pages `current` (first, last). Params:
|
||||
/// `range` (`all` | `current` | `selection` | `custom`; `custom` is implied by `pages`),
|
||||
/// `pages` (text like `1-3,5`), `parity` (`all` | `odd` | `even`), `copies` (1–999),
|
||||
/// `collate` (default true: 1,2,3 1,2,3; false: 1,1,2,2,3,3).
|
||||
pub fn plan(v: &Value, total: usize, current: (usize, usize)) -> Result<PrintPlan, CmdError> {
|
||||
let bad = |m: String| CmdError::Params(m);
|
||||
let pages_text = p::str(v, "pages").filter(|t| !t.trim().is_empty());
|
||||
let range = p::str(v, "range").unwrap_or(if pages_text.is_some() { "custom" } else { "all" });
|
||||
let last = total.saturating_sub(1);
|
||||
let mut pages: Vec<usize> = match range {
|
||||
"all" => (0..total).collect(),
|
||||
"current" => vec![current.0.min(last)],
|
||||
"selection" => (current.0.min(last)..=current.1.min(last).max(current.0.min(last))).collect(),
|
||||
"custom" => parse_page_range(pages_text.unwrap_or(""), total).map_err(bad)?,
|
||||
other => return Err(bad(format!("unknown range `{other}`; use all, current, selection or custom"))),
|
||||
};
|
||||
match p::str(v, "parity").unwrap_or("all") {
|
||||
"all" => {}
|
||||
// Page numbers are 1-based: the odd ones are indexes 0, 2, 4…
|
||||
"odd" => pages.retain(|i| i % 2 == 0),
|
||||
"even" => pages.retain(|i| i % 2 == 1),
|
||||
other => return Err(bad(format!("unknown parity `{other}`; use all, odd or even"))),
|
||||
}
|
||||
if pages.is_empty() {
|
||||
return Err(bad("no pages to print with these settings".into()));
|
||||
}
|
||||
let copies = p::u64(v, "copies").unwrap_or(1).clamp(1, u64::from(MAX_COPIES)) as u32;
|
||||
let chosen = pages.len();
|
||||
let total_out = chosen.checked_mul(copies as usize).filter(|n| *n <= MAX_PRINT_PAGES);
|
||||
let Some(total_out) = total_out else {
|
||||
return Err(bad(format!("{chosen} pages × {copies} copies is more than the {MAX_PRINT_PAGES} pages one print job can hold")));
|
||||
};
|
||||
let mut out = Vec::with_capacity(total_out);
|
||||
if p::bool(v, "collate").unwrap_or(true) {
|
||||
for _ in 0..copies {
|
||||
out.extend_from_slice(&pages);
|
||||
}
|
||||
} else {
|
||||
for &pg in &pages {
|
||||
out.extend(std::iter::repeat_n(pg, copies as usize));
|
||||
}
|
||||
}
|
||||
Ok(PrintPlan { pages: out, chosen, copies })
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The pages the caret (or selection) is on, in the output layout: (first, last).
|
||||
pub fn current_pages(&self, lay: &DocLayout) -> (usize, usize) {
|
||||
let (a, b) = self.sel.ordered();
|
||||
let first = lay.caret_on(&a, self.page_hint).map_or(0, |c| c.page);
|
||||
let last = lay.caret_on(&b, self.page_hint).map_or(first, |c| c.page);
|
||||
(first, last.max(first))
|
||||
}
|
||||
|
||||
/// The document as the PDF of a print job `v` describes (see [`plan`]), with the number of
|
||||
/// distinct pages chosen and copies. Does not touch the document.
|
||||
pub fn print_pdf(&self, v: &Value) -> Result<(Vec<u8>, PrintPlan), CmdError> {
|
||||
let lay = self.export_layout();
|
||||
let plan = plan(v, lay.pages.len(), self.current_pages(&lay))?;
|
||||
let opts = wordcraft_pdf::PdfOptions {
|
||||
pages: Some(plan.pages.clone()),
|
||||
// Repeated pages would repeat the structure tree's entries too.
|
||||
tagged: plan.copies <= 1,
|
||||
..Default::default()
|
||||
};
|
||||
let bytes = wordcraft_pdf::export_layout(&self.doc, &lay, &opts).map_err(|e| CmdError::Failed(e.to_string()))?;
|
||||
Ok((bytes, plan))
|
||||
}
|
||||
}
|
||||
|
||||
/// `file.printPdf`: write the print job's PDF to `path` (no dialogs).
|
||||
pub(crate) fn print_pdf_to_file(s: &mut Session, v: &Value) -> Result<Value, CmdError> {
|
||||
let path = p::req_str(v, "path")?;
|
||||
let path = if path.to_ascii_lowercase().ends_with(".pdf") { path.to_string() } else { format!("{path}.pdf") };
|
||||
let (bytes, plan) = s.print_pdf(v)?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::fs::write(&path, &bytes).map_err(|e| CmdError::Failed(format!("{path}: {e}")))?;
|
||||
Ok(json!({"path": path, "pages": plan.pages.len(), "chosen": plan.chosen, "copies": plan.copies, "bytes": bytes.len()}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ranges_parse_and_hostile_ones_are_errors() {
|
||||
assert_eq!(parse_page_range("1-3, 5", 6), Ok(vec![0, 1, 2, 4]));
|
||||
assert_eq!(parse_page_range("5,1-2,2", 6), Ok(vec![0, 1, 4]), "document order, no repeats");
|
||||
assert_eq!(parse_page_range("4-", 6), Ok(vec![3, 4, 5]));
|
||||
assert_eq!(parse_page_range("-2", 6), Ok(vec![0, 1]));
|
||||
for bad in ["", " , ", "0", "7", "3-1", "a", "1-2-3", "99999999999999999999999", "-", "1..3", &"1,".repeat(400)] {
|
||||
assert!(parse_page_range(bad, 6).is_err(), "`{bad}` is refused");
|
||||
}
|
||||
assert!(parse_page_range("1", 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plans_apply_range_parity_copies_and_caps() {
|
||||
let p = |v: Value| plan(&v, 6, (2, 3)).map(|p| p.pages);
|
||||
assert_eq!(p(json!({})), Ok(vec![0, 1, 2, 3, 4, 5]));
|
||||
assert_eq!(p(json!({"range": "current"})), Ok(vec![2]));
|
||||
assert_eq!(p(json!({"range": "selection"})), Ok(vec![2, 3]));
|
||||
assert_eq!(p(json!({"pages": "2-4", "copies": 2})), Ok(vec![1, 2, 3, 1, 2, 3]));
|
||||
assert_eq!(p(json!({"pages": "2-3", "copies": 2, "collate": false})), Ok(vec![1, 1, 2, 2]));
|
||||
assert_eq!(p(json!({"parity": "even"})), Ok(vec![1, 3, 5]));
|
||||
assert!(p(json!({"range": "custom"})).is_err(), "custom needs pages");
|
||||
assert!(p(json!({"range": "everything"})).is_err());
|
||||
assert!(p(json!({"range": "current", "parity": "even"})).is_err(), "page 3 is odd: nothing left");
|
||||
// Hostile copies are clamped; a job that would be too big is refused, not allocated.
|
||||
assert_eq!(p(json!({"range": "current", "copies": 1e12})).map(|v| v.len()), Ok(MAX_COPIES as usize));
|
||||
assert!(plan(&json!({"copies": 999}), 600, (0, 0)).is_err());
|
||||
assert_eq!(p(json!({"copies": -5, "range": "current"})), Ok(vec![2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_pdf_writes_only_the_chosen_pages() {
|
||||
let mut s = Session::new(crate::sample::sample_document());
|
||||
let pages = s.export_layout().pages.len();
|
||||
let (all, _) = s.print_pdf(&json!({})).unwrap();
|
||||
let (one, plan) = s.print_pdf(&json!({"range": "current", "copies": 2})).unwrap();
|
||||
assert!(all.starts_with(b"%PDF") && one.starts_with(b"%PDF"));
|
||||
assert_eq!(plan.pages.len(), 2);
|
||||
if pages > 2 {
|
||||
assert!(one.len() < all.len());
|
||||
}
|
||||
assert!(s.print_pdf(&json!({"pages": "9999"})).is_err());
|
||||
let r = s.run("file.printPdf", &json!({})).is_err();
|
||||
assert!(r, "printPdf needs a path");
|
||||
}
|
||||
}
|
||||
|
|
@ -331,39 +331,15 @@ fn info_page(app: &mut WordApp, ui: &mut Ui) {
|
|||
}
|
||||
|
||||
fn export_page(app: &mut WordApp, ui: &mut Ui) {
|
||||
let is_print = app.ui.backstage_page == "print";
|
||||
heading(ui, if is_print { "Print" } else { "Export" });
|
||||
if is_print && app.services.print.is_some() {
|
||||
ui.label(tl!("Send the document straight to the system print dialog, or save a copy in another format below."));
|
||||
ui.add_space(12.0);
|
||||
if ui.add(egui::Button::new(egui::RichText::new(tl!("Print")).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
|
||||
let _ = app.run("ui.print", json!({}));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
ui.add_space(12.0);
|
||||
ui.label(tl!("Or save a copy in another format:"));
|
||||
} else {
|
||||
ui.label(tl!("Save a copy in another format. Printing goes through a PDF you can print from any viewer."));
|
||||
if app.ui.backstage_page == "print" {
|
||||
heading(ui, "Print");
|
||||
crate::print_page::show(app, ui);
|
||||
return;
|
||||
}
|
||||
heading(ui, "Export");
|
||||
ui.label(tl!("Save a copy in another format. Printing goes through a PDF you can print from any viewer."));
|
||||
ui.add_space(12.0);
|
||||
for (label, ext) in [
|
||||
("PDF document (*.pdf)", "pdf"),
|
||||
("Word document (*.docx)", "docx"),
|
||||
("Word XML Document (*.xml)", "xml"),
|
||||
("OpenDocument Text (*.odt)", "odt"),
|
||||
("Rich Text Format (*.rtf)", "rtf"),
|
||||
("Web page (*.html)", "html"),
|
||||
("Markdown (*.md)", "md"),
|
||||
("LaTeX (*.tex)", "tex"),
|
||||
("Plain text (*.txt)", "txt"),
|
||||
("Page image (*.png)", "png"),
|
||||
] {
|
||||
if ui.add(egui::Button::new(egui::RichText::new(tl!(label)).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
|
||||
app.export_dialog(ext);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
crate::print_page::export_formats(app, ui);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ pub(crate) enum AfterPick {
|
|||
SaveAs(AfterSave),
|
||||
/// Save a copy there in this format (`pdf`, `docx`, `png`…).
|
||||
Export { ext: String },
|
||||
/// Write the PDF of these print settings there (File › Print › Save these pages as PDF).
|
||||
PrintPdf { settings: Value },
|
||||
/// Load the picked CSV/TSV/text file or spreadsheet as the mail-merge recipients (#240, #334).
|
||||
Recipients,
|
||||
/// Hand the picked path to the open dialog (`WordApp::dialog_pick`): Templates and Add-ins,
|
||||
|
|
@ -151,6 +153,16 @@ impl WordApp {
|
|||
}
|
||||
r
|
||||
}
|
||||
AfterPick::PrintPdf { settings } => {
|
||||
let Some(path) = picked else { return Ok(json!({"cancelled": true})) };
|
||||
let mut params = settings;
|
||||
params["path"] = json!(path);
|
||||
let r = self.run("file.printPdf", params);
|
||||
if r.is_ok() {
|
||||
self.status(crate::i18n::fmt(tl!("Exported {path}"), &[("path", &path)]));
|
||||
}
|
||||
r
|
||||
}
|
||||
AfterPick::SaveAs(after) => {
|
||||
let saved = picked.is_some_and(|path| {
|
||||
self.run("file.save", json!({"path": path})).is_ok_and(|v| v.get("saved").and_then(Value::as_bool) == Some(true))
|
||||
|
|
@ -191,6 +203,14 @@ impl WordApp {
|
|||
let _ = self.ask_file(FileDialogRequest::Save { name }, AfterPick::Export { ext: ext.to_string() });
|
||||
}
|
||||
|
||||
/// File › Print › Save these pages as PDF: ask where, then write the pages the print
|
||||
/// settings choose.
|
||||
pub(crate) fn print_pdf_dialog(&mut self) {
|
||||
let name = format!("{}.pdf", self.title_stem());
|
||||
let settings = self.ui.print.params();
|
||||
let _ = self.ask_file(FileDialogRequest::Save { name }, AfterPick::PrintPdf { settings });
|
||||
}
|
||||
|
||||
pub(crate) fn open_dialog(&mut self) {
|
||||
if let Some(f) = &self.services.open_async {
|
||||
f("document");
|
||||
|
|
|
|||
|
|
@ -1696,6 +1696,21 @@
|
|||
Continuous numbering Numeración continua
|
||||
Apply Aplicar
|
||||
Convert Notes Convertir notas
|
||||
All pages Todas las páginas
|
||||
Current page Página actual
|
||||
Selected pages Páginas seleccionadas
|
||||
Custom range Intervalo personalizado
|
||||
All pages in the range Todas las páginas del intervalo
|
||||
Odd pages only Solo páginas impares
|
||||
Even pages only Solo páginas pares
|
||||
Copies Copias
|
||||
Collate copies Intercalar copias
|
||||
Pages like 1-3, 5 Páginas como 1-3, 5
|
||||
{count} of {total} pages will print Se imprimirán {count} de {total} páginas
|
||||
Save these pages as PDF… Guardar estas páginas como PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. La impresora, la impresión a doble cara y las páginas por hoja se eligen en ese cuadro de diálogo.
|
||||
Previous Page Página anterior
|
||||
Print to PDF Imprimir en PDF
|
||||
Add Shape Agregar forma
|
||||
Change Colors Cambiar colores
|
||||
Change Layout Cambiar diseño
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1657,6 +1657,21 @@
|
|||
Continuous numbering Pidev nummerdus
|
||||
Apply Rakenda
|
||||
Convert Notes Teisenda märkused
|
||||
All pages Kõik leheküljed
|
||||
Current page Praegune lehekülg
|
||||
Selected pages Valitud leheküljed
|
||||
Custom range Kohandatud vahemik
|
||||
All pages in the range Kõik vahemiku leheküljed
|
||||
Odd pages only Ainult paaritud leheküljed
|
||||
Even pages only Ainult paarisleheküljed
|
||||
Copies Koopiad
|
||||
Collate copies Järjesta koopiad
|
||||
Pages like 1-3, 5 Leheküljed, nt 1-3, 5
|
||||
{count} of {total} pages will print Prinditakse {count} lehekülge {total}-st
|
||||
Save these pages as PDF… Salvesta need leheküljed PDF-ina…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Printer, kahepoolne printimine ja lehekülgi lehe kohta valitakse selles dialoogis.
|
||||
Previous Page Eelmine lehekülg
|
||||
Print to PDF Prindi PDF-i
|
||||
Add Shape Lisa kujund
|
||||
Change Colors Muuda värve
|
||||
Change Layout Muuda paigutust
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -1680,6 +1680,21 @@
|
|||
Continuous numbering 通し番号
|
||||
Apply 適用
|
||||
Convert Notes 注の変換
|
||||
All pages すべてのページ
|
||||
Current page 現在のページ
|
||||
Selected pages 選択したページ
|
||||
Custom range ページ範囲を指定
|
||||
All pages in the range 範囲内のすべてのページ
|
||||
Odd pages only 奇数ページのみ
|
||||
Even pages only 偶数ページのみ
|
||||
Copies 部数
|
||||
Collate copies 部単位で印刷
|
||||
Pages like 1-3, 5 ページ指定(例: 1-3, 5)
|
||||
{count} of {total} pages will print {total} ページ中 {count} ページを印刷します
|
||||
Save these pages as PDF… このページを PDF として保存…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. プリンター、両面印刷、1 枚あたりのページ数は、そのダイアログで選択します。
|
||||
Previous Page 前のページ
|
||||
Print to PDF PDF に印刷
|
||||
Add Shape 図形の追加
|
||||
Change Colors 色の変更
|
||||
Change Layout レイアウトの変更
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -1692,6 +1692,21 @@
|
|||
Continuous numbering Numeração contínua
|
||||
Apply Aplicar
|
||||
Convert Notes Converter notas
|
||||
All pages Todas as páginas
|
||||
Current page Página atual
|
||||
Selected pages Páginas selecionadas
|
||||
Custom range Intervalo personalizado
|
||||
All pages in the range Todas as páginas do intervalo
|
||||
Odd pages only Somente páginas ímpares
|
||||
Even pages only Somente páginas pares
|
||||
Copies Cópias
|
||||
Collate copies Agrupar cópias
|
||||
Pages like 1-3, 5 Páginas como 1-3, 5
|
||||
{count} of {total} pages will print {count} de {total} páginas serão impressas
|
||||
Save these pages as PDF… Salvar estas páginas como PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. A impressora, a impressão frente e verso e as páginas por folha são escolhidas nessa caixa de diálogo.
|
||||
Previous Page Página anterior
|
||||
Print to PDF Imprimir em PDF
|
||||
Add Shape Adicionar forma
|
||||
Change Colors Alterar cores
|
||||
Change Layout Alterar layout
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -2125,6 +2125,21 @@ preview Title Пример
|
|||
Continuous numbering Сквозная нумерация
|
||||
Apply Применить
|
||||
Convert Notes Преобразовать сноски
|
||||
All pages Все страницы
|
||||
Current page Текущая страница
|
||||
Selected pages Выбранные страницы
|
||||
Custom range Свой диапазон
|
||||
All pages in the range Все страницы диапазона
|
||||
Odd pages only Только нечётные страницы
|
||||
Even pages only Только чётные страницы
|
||||
Copies Копии
|
||||
Collate copies Разобрать по копиям
|
||||
Pages like 1-3, 5 Страницы, например 1-3, 5
|
||||
{count} of {total} pages will print Будет напечатано страниц: {count} из {total}
|
||||
Save these pages as PDF… Сохранить эти страницы как PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Принтер, двусторонняя печать и число страниц на листе выбираются в этом диалоге.
|
||||
Previous Page Предыдущая страница
|
||||
Print to PDF Печать в PDF
|
||||
|
||||
# Word XML Document (#548).
|
||||
Word XML Document (*.xml) Документ Word XML (*.xml)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -1690,6 +1690,21 @@
|
|||
Continuous numbering Neprekidna numeracija
|
||||
Apply Primeni
|
||||
Convert Notes Pretvori napomene
|
||||
All pages Sve stranice
|
||||
Current page Trenutna stranica
|
||||
Selected pages Izabrane stranice
|
||||
Custom range Prilagođeni opseg
|
||||
All pages in the range Sve stranice u opsegu
|
||||
Odd pages only Samo neparne stranice
|
||||
Even pages only Samo parne stranice
|
||||
Copies Kopije
|
||||
Collate copies Složi kopije
|
||||
Pages like 1-3, 5 Stranice poput 1-3, 5
|
||||
{count} of {total} pages will print Štampaće se {count} od {total} stranica
|
||||
Save these pages as PDF… Sačuvaj ove stranice kao PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Štampač, obostrana štampa i broj stranica po listu biraju se u tom dijalogu.
|
||||
Previous Page Prethodna stranica
|
||||
Print to PDF Štampaj u PDF
|
||||
Add Shape Dodaj oblik
|
||||
Change Colors Promeni boje
|
||||
Change Layout Promeni raspored
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1694,6 +1694,21 @@
|
|||
Continuous numbering Непрекидна нумерација
|
||||
Apply Примени
|
||||
Convert Notes Претвори напомене
|
||||
All pages Све странице
|
||||
Current page Тренутна страница
|
||||
Selected pages Изабране странице
|
||||
Custom range Прилагођени опсег
|
||||
All pages in the range Све странице у опсегу
|
||||
Odd pages only Само непарне странице
|
||||
Even pages only Само парне странице
|
||||
Copies Копије
|
||||
Collate copies Сложи копије
|
||||
Pages like 1-3, 5 Странице попут 1-3, 5
|
||||
{count} of {total} pages will print Штампаће се {count} од {total} страница
|
||||
Save these pages as PDF… Сачувај ове странице као PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Штампач, обострана штампа и број страница по листу бирају се у том дијалогу.
|
||||
Previous Page Претходна страница
|
||||
Print to PDF Штампај у PDF
|
||||
Add Shape Додај облик
|
||||
Change Colors Промени боје
|
||||
Change Layout Промени распоред
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
|
|
@ -1747,6 +1747,21 @@
|
|||
Continuous numbering Sürekli numaralandırma
|
||||
Apply Uygula
|
||||
Convert Notes Notları Dönüştür
|
||||
All pages Tüm sayfalar
|
||||
Current page Geçerli sayfa
|
||||
Selected pages Seçili sayfalar
|
||||
Custom range Özel aralık
|
||||
All pages in the range Aralıktaki tüm sayfalar
|
||||
Odd pages only Yalnızca tek sayfalar
|
||||
Even pages only Yalnızca çift sayfalar
|
||||
Copies Kopya
|
||||
Collate copies Kopyaları harmanla
|
||||
Pages like 1-3, 5 Sayfalar, örn. 1-3, 5
|
||||
{count} of {total} pages will print {total} sayfadan {count} sayfa yazdırılacak
|
||||
Save these pages as PDF… Bu sayfaları PDF olarak kaydet…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Yazıcı, çift taraflı yazdırma ve yaprak başına sayfa sayısı o iletişim kutusunda seçilir.
|
||||
Previous Page Önceki sayfa
|
||||
Print to PDF PDF'ye yazdır
|
||||
|
||||
# Word XML Document (#548).
|
||||
Word XML Document (*.xml) Word XML Belgesi (*.xml)
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 18.
|
|
|
@ -1692,6 +1692,21 @@
|
|||
Continuous numbering Наскрізна нумерація
|
||||
Apply Застосувати
|
||||
Convert Notes Перетворити примітки
|
||||
All pages Усі сторінки
|
||||
Current page Поточна сторінка
|
||||
Selected pages Вибрані сторінки
|
||||
Custom range Власний діапазон
|
||||
All pages in the range Усі сторінки діапазону
|
||||
Odd pages only Лише непарні сторінки
|
||||
Even pages only Лише парні сторінки
|
||||
Copies Копії
|
||||
Collate copies Розсортувати копії
|
||||
Pages like 1-3, 5 Сторінки, наприклад 1-3, 5
|
||||
{count} of {total} pages will print Буде надруковано {count} зі сторінок: {total}
|
||||
Save these pages as PDF… Зберегти ці сторінки як PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. Принтер, двобічний друк і кількість сторінок на аркуші вибирають у цьому діалозі.
|
||||
Previous Page Попередня сторінка
|
||||
Print to PDF Друк у PDF
|
||||
Add Shape Додати фігуру
|
||||
Change Colors Змінити кольори
|
||||
Change Layout Змінити макет
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
|
|
@ -1692,6 +1692,21 @@
|
|||
Continuous numbering 连续编号
|
||||
Apply 应用
|
||||
Convert Notes 转换注释
|
||||
All pages 所有页面
|
||||
Current page 当前页
|
||||
Selected pages 所选页面
|
||||
Custom range 自定义范围
|
||||
All pages in the range 范围内的所有页面
|
||||
Odd pages only 仅奇数页
|
||||
Even pages only 仅偶数页
|
||||
Copies 份数
|
||||
Collate copies 逐份打印
|
||||
Pages like 1-3, 5 页码,如 1-3, 5
|
||||
{count} of {total} pages will print 将打印 {total} 页中的 {count} 页
|
||||
Save these pages as PDF… 将这些页面另存为 PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. 打印机、双面打印和每张页数在该对话框中选择。
|
||||
Previous Page 上一页
|
||||
Print to PDF 打印到 PDF
|
||||
Add Shape 添加形状
|
||||
Change Colors 更改颜色
|
||||
Change Layout 更改布局
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -1681,6 +1681,21 @@
|
|||
Continuous numbering 連續編號
|
||||
Apply 套用
|
||||
Convert Notes 轉換附註
|
||||
All pages 所有頁面
|
||||
Current page 目前頁面
|
||||
Selected pages 所選頁面
|
||||
Custom range 自訂範圍
|
||||
All pages in the range 範圍內的所有頁面
|
||||
Odd pages only 僅奇數頁
|
||||
Even pages only 僅偶數頁
|
||||
Copies 份數
|
||||
Collate copies 逐份列印
|
||||
Pages like 1-3, 5 頁碼,如 1-3, 5
|
||||
{count} of {total} pages will print 將列印 {total} 頁中的 {count} 頁
|
||||
Save these pages as PDF… 將這些頁面另存為 PDF…
|
||||
The printer, two-sided printing and pages per sheet are chosen in that dialog. 印表機、雙面列印和每張頁數在該對話方塊中選擇。
|
||||
Previous Page 上一頁
|
||||
Print to PDF 列印為 PDF
|
||||
Add Shape 新增圖案
|
||||
Change Colors 變更色彩
|
||||
Change Layout 變更版面配置
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
|
@ -50,6 +50,7 @@ pub mod options;
|
|||
pub mod panes;
|
||||
pub mod paste_picture;
|
||||
pub mod previews;
|
||||
pub mod print_page;
|
||||
pub mod read_aloud;
|
||||
pub mod ribbon;
|
||||
pub mod scroll;
|
||||
|
|
@ -108,6 +109,9 @@ pub struct UiState {
|
|||
pub tab: String,
|
||||
pub backstage: bool,
|
||||
pub backstage_page: String,
|
||||
/// File › Print settings (#641); not saved.
|
||||
#[serde(skip)]
|
||||
pub print: print_page::PrintSettings,
|
||||
pub ribbon_collapsed: bool,
|
||||
pub recent: Vec<String>,
|
||||
/// Symbols inserted from the Symbol dialog, most recent first (#321).
|
||||
|
|
@ -185,6 +189,7 @@ impl Default for UiState {
|
|||
tab: "Home".into(),
|
||||
backstage: false,
|
||||
backstage_page: "home".into(),
|
||||
print: print_page::PrintSettings::default(),
|
||||
ribbon_collapsed: false,
|
||||
recent: Vec::new(),
|
||||
recent_symbols: Vec::new(),
|
||||
|
|
@ -815,7 +820,7 @@ impl WordApp {
|
|||
// Send the document to the system print flow (web, desktop). `file.print` opens the Print
|
||||
// page; this is the button on it, and the one programmatic call that opens system UI
|
||||
// here, like `ui.openFileDialog` — only when the host can print.
|
||||
"ui.print" => return Some(self.print_to_system()),
|
||||
"ui.print" => return Some(self.print_to_system(p)),
|
||||
"ui.discord" => {
|
||||
self.canvas.open_url = Some("https://discord.gg/artcraft".into());
|
||||
json!({})
|
||||
|
|
@ -834,8 +839,10 @@ impl WordApp {
|
|||
}
|
||||
|
||||
/// `ui.print`: export the document to PDF in memory and hand it to the host's print hook.
|
||||
/// `p` holds the print settings (`range`, `pages`, `parity`, `copies`, `collate`; see
|
||||
/// `file.printPdf`); without any, every page prints once.
|
||||
/// An error when the host can't print (File › Print then only saves a PDF).
|
||||
fn print_to_system(&mut self) -> Result<Value, String> {
|
||||
fn print_to_system(&mut self, p: &Value) -> Result<Value, String> {
|
||||
if self.services.print.is_none() {
|
||||
return Err("printing to the system print dialog isn't available here; export a PDF instead".into());
|
||||
}
|
||||
|
|
@ -845,13 +852,12 @@ impl WordApp {
|
|||
{
|
||||
log::warn!("updating fields before printing: {e}");
|
||||
}
|
||||
let without_colour = (!self.ui.print_backgrounds && self.session.doc.settings.page_color.is_some()).then(|| {
|
||||
let mut d = self.session.doc.clone();
|
||||
d.settings.page_color = None;
|
||||
d
|
||||
});
|
||||
let result = wordcraft_engine::io::save_bytes("document.pdf", without_colour.as_ref().unwrap_or(&self.session.doc))
|
||||
.and_then(|bytes| self.services.print.as_ref().map_or(Ok(()), |print| print(&bytes)).map(|()| bytes.len()));
|
||||
let colour = if self.ui.print_backgrounds { None } else { self.session.doc.settings.page_color.take() };
|
||||
let pdf = self.session.print_pdf(p).map_err(|e| e.to_string());
|
||||
if colour.is_some() {
|
||||
self.session.doc.settings.page_color = colour;
|
||||
}
|
||||
let result = pdf.and_then(|(bytes, _)| self.services.print.as_ref().map_or(Ok(()), |print| print(&bytes)).map(|()| bytes.len()));
|
||||
match result {
|
||||
Ok(len) => {
|
||||
self.status(tl!("Opening print dialog…"));
|
||||
|
|
@ -1924,6 +1930,23 @@ mod tests {
|
|||
assert!(a.status_msg.as_ref().is_some_and(|(m, _)| m.contains("blocked by the browser")), "the status bar says why");
|
||||
}
|
||||
|
||||
/// The print settings reach the hook: bad ones print nothing, `copies` repeats the pages.
|
||||
#[test]
|
||||
fn ui_print_honours_the_print_settings() {
|
||||
let mut a = typed();
|
||||
let sizes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let seen = sizes.clone();
|
||||
a.services.print = Some(Box::new(move |bytes| {
|
||||
seen.borrow_mut().push(bytes.len());
|
||||
Ok(())
|
||||
}));
|
||||
assert!(a.run("ui.print", json!({"pages": "7-9"})).is_err(), "page 7 doesn't exist");
|
||||
assert!(a.run("ui.print", json!({"range": "nope"})).is_err());
|
||||
assert!(sizes.borrow().is_empty(), "nothing was sent");
|
||||
a.run("ui.print", json!({"range": "current", "copies": 3})).unwrap();
|
||||
assert_eq!(sizes.borrow().len(), 1);
|
||||
}
|
||||
|
||||
/// The Print page shows the Print button only when the host can print, and the button
|
||||
/// prints through `ui.print`.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
//! File › Print (#641): the print settings and a preview of the pages.
|
||||
//!
|
||||
//! WordCraft prints through a PDF that the host opens in the system's print flow, so the settings
|
||||
//! here are the ones that shape that PDF: which pages, odd/even, copies and collation. They go to
|
||||
//! `ui.print` / `file.printPdf` as params (see `wordcraft_engine::print`). The printer, two-sided
|
||||
//! printing and pages per sheet are chosen in the system print dialog that opens next.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Rect, Sense, Stroke, TextureHandle, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use wordcraft_layout::DocLayout;
|
||||
|
||||
use crate::theme::{Tokens, medium, regular};
|
||||
use crate::{WordApp, icons};
|
||||
|
||||
/// The Print page's settings. Not saved with the document or the UI state.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrintSettings {
|
||||
/// `all`, `current`, `selection` or `custom`.
|
||||
pub range: String,
|
||||
/// The custom range text, like `1-3, 5`.
|
||||
pub pages: String,
|
||||
/// `all`, `odd` or `even`.
|
||||
pub parity: String,
|
||||
pub copies: u32,
|
||||
pub collate: bool,
|
||||
/// The page shown in the preview (0-based).
|
||||
pub preview: usize,
|
||||
}
|
||||
|
||||
impl Default for PrintSettings {
|
||||
fn default() -> Self {
|
||||
PrintSettings { range: "all".into(), pages: String::new(), parity: "all".into(), copies: 1, collate: true, preview: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintSettings {
|
||||
/// The params of `ui.print` and `file.printPdf` for these settings.
|
||||
pub fn params(&self) -> Value {
|
||||
let mut v = json!({"range": self.range, "parity": self.parity, "copies": self.copies, "collate": self.collate});
|
||||
if self.range == "custom" {
|
||||
v["pages"] = json!(self.pages);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
const RANGES: [(&str, &str); 4] = [("all", "All pages"), ("current", "Current page"), ("selection", "Selected pages"), ("custom", "Custom range")];
|
||||
const PARITIES: [(&str, &str); 3] = [("all", "All pages in the range"), ("odd", "Odd pages only"), ("even", "Even pages only")];
|
||||
|
||||
/// A choice list over `(id, label)` pairs; true when the choice changed.
|
||||
fn choice(ui: &mut Ui, salt: &str, current: &mut String, items: &[(&str, &str)]) -> bool {
|
||||
let shown = items.iter().find(|(id, _)| *id == current.as_str()).map_or("", |(_, l)| *l);
|
||||
let mut changed = false;
|
||||
egui::ComboBox::from_id_salt(salt).width(260.0).selected_text(tl!(shown)).show_ui(ui, |ui| {
|
||||
for (id, label) in items {
|
||||
if ui.selectable_label(current == id, tl!(label)).clicked() && current != id {
|
||||
*current = (*id).to_string();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
changed
|
||||
}
|
||||
|
||||
/// The output layout, kept while the document is unchanged (laying out every frame would be slow).
|
||||
fn layout_of(app: &WordApp, ctx: &egui::Context) -> Arc<DocLayout> {
|
||||
let id = egui::Id::new("print_page_layout");
|
||||
let rev = app.session.rev();
|
||||
if let Some((r, l)) = ctx.data(|d| d.get_temp::<(u64, Arc<DocLayout>)>(id))
|
||||
&& r == rev
|
||||
{
|
||||
return l;
|
||||
}
|
||||
let l = app.session.export_layout();
|
||||
ctx.data_mut(|d| d.insert_temp(id, (rev, l.clone())));
|
||||
l
|
||||
}
|
||||
|
||||
/// The texture of one page, re-rendered only when the document, page or size changes.
|
||||
fn page_texture(app: &WordApp, ctx: &egui::Context, lay: &DocLayout, page: usize, width: f32) -> Option<TextureHandle> {
|
||||
let id = egui::Id::new("print_page_texture");
|
||||
let key = (app.session.rev(), page, width as u32, (ctx.pixels_per_point() * 100.0) as u32);
|
||||
if let Some((k, t)) = ctx.data(|d| d.get_temp::<((u64, usize, u32, u32), TextureHandle)>(id))
|
||||
&& k == key
|
||||
{
|
||||
return Some(t);
|
||||
}
|
||||
let pg = lay.pages.get(page)?;
|
||||
let scale = (width / pg.w.max(1.0) * ctx.pixels_per_point()).clamp(0.05, 4.0);
|
||||
let img = wordcraft_render::render_page(&app.session.doc, pg, scale, &Default::default());
|
||||
let color = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_straight());
|
||||
let tex = ctx.load_texture("print_page_preview", color, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, tex.clone())));
|
||||
Some(tex)
|
||||
}
|
||||
|
||||
/// The Print page: settings on the left, the preview on the right.
|
||||
pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ctx = ui.ctx().clone();
|
||||
let lay = layout_of(app, &ctx);
|
||||
let total = lay.pages.len();
|
||||
let current = app.session.current_pages(&lay);
|
||||
app.ui.print.preview = app.ui.print.preview.min(total.saturating_sub(1));
|
||||
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(380.0);
|
||||
ui.spacing_mut().item_spacing.y = 8.0;
|
||||
let hook = app.services.print.is_some();
|
||||
if hook {
|
||||
ui.label(tl!("Send the document straight to the system print dialog, or save a copy in another format below."));
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("The printer, two-sided printing and pages per sheet are chosen in that dialog.")).color(t.text_dim),
|
||||
);
|
||||
} else {
|
||||
ui.label(tl!("Save a copy in another format. Printing goes through a PDF you can print from any viewer."));
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
if hook && ui.add(egui::Button::new(egui::RichText::new(tl!("Print")).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
|
||||
let params = app.ui.print.params();
|
||||
let _ = app.run("ui.print", params);
|
||||
}
|
||||
if ui
|
||||
.add(egui::Button::new(egui::RichText::new(tl!("Save these pages as PDF…")).font(medium(13.5))).min_size(vec2(320.0, 34.0)))
|
||||
.clicked()
|
||||
{
|
||||
app.print_pdf_dialog();
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label(egui::RichText::new(tl!("Copies")).font(medium(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
let mut n = app.ui.print.copies;
|
||||
ui.add(egui::DragValue::new(&mut n).range(1..=wordcraft_engine::print::MAX_COPIES).speed(0.2));
|
||||
app.ui.print.copies = n.clamp(1, wordcraft_engine::print::MAX_COPIES);
|
||||
if app.ui.print.copies > 1 {
|
||||
ui.checkbox(&mut app.ui.print.collate, tl!("Collate copies"));
|
||||
}
|
||||
});
|
||||
|
||||
ui.label(egui::RichText::new(tl!("Pages")).font(medium(13.0)));
|
||||
if choice(ui, "print_range", &mut app.ui.print.range, &RANGES) {
|
||||
// Show the first page that will print.
|
||||
app.ui.print.preview = if app.ui.print.range == "all" { 0 } else { current.0.min(total.saturating_sub(1)) };
|
||||
}
|
||||
if app.ui.print.range == "custom" {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut app.ui.print.pages)
|
||||
.hint_text(tl!("Pages like 1-3, 5"))
|
||||
.desired_width(260.0)
|
||||
.char_limit(wordcraft_engine::print::MAX_SPEC_CHARS),
|
||||
);
|
||||
}
|
||||
choice(ui, "print_parity", &mut app.ui.print.parity, &PARITIES);
|
||||
|
||||
match wordcraft_engine::print::plan(&app.ui.print.params(), total, current) {
|
||||
Ok(plan) => {
|
||||
let msg = crate::i18n::fmt(
|
||||
tl!("{count} of {total} pages will print"),
|
||||
&[("count", &plan.chosen.to_string()), ("total", &total.to_string())],
|
||||
);
|
||||
ui.label(egui::RichText::new(msg).color(t.text_dim));
|
||||
}
|
||||
Err(e) => {
|
||||
ui.label(egui::RichText::new(e.to_string()).color(t.red));
|
||||
}
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(4.0);
|
||||
if hook {
|
||||
ui.label(tl!("Or save a copy in another format:"));
|
||||
}
|
||||
export_formats(app, ui);
|
||||
});
|
||||
ui.add_space(24.0);
|
||||
ui.vertical(|ui| preview(app, ui, &lay, total));
|
||||
});
|
||||
}
|
||||
|
||||
/// The preview: one page with Previous/Next.
|
||||
fn preview(app: &mut WordApp, ui: &mut Ui, lay: &DocLayout, total: usize) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ctx = ui.ctx().clone();
|
||||
let width = 340.0;
|
||||
let page = app.ui.print.preview;
|
||||
let aspect = lay.pages.get(page).map_or(11.0 / 8.5, |p| p.h.max(1.0) / p.w.max(1.0)).clamp(0.2, 5.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(width, width * aspect), Sense::hover());
|
||||
ui.painter().rect_filled(r.translate(vec2(2.0, 3.0)), 0.0, t.page_shadow);
|
||||
ui.painter().rect(r, 0.0, egui::Color32::WHITE, Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
|
||||
if let Some(tex) = page_texture(app, &ctx, lay, page, width) {
|
||||
ui.painter().image(tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), egui::Color32::WHITE);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
let nav = |ui: &mut Ui, icon: &str, tip: &str, enabled: bool| {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 26.0), Sense::click());
|
||||
let col = if enabled { t.icon } else { t.text_disabled };
|
||||
if enabled && resp.hovered() {
|
||||
ui.painter().rect_filled(r, 3.0, t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), r.shrink(4.0), icon, col, col);
|
||||
enabled && resp.on_hover_text(tip).clicked()
|
||||
};
|
||||
if nav(ui, "chevronLeft", tl!("Previous Page"), page > 0) {
|
||||
app.ui.print.preview = page - 1;
|
||||
}
|
||||
let label = crate::i18n::fmt(tl!("Page {page} of {pages}"), &[("page", &(page + 1).to_string()), ("pages", &total.to_string())]);
|
||||
ui.label(egui::RichText::new(label).font(regular(13.0)));
|
||||
if nav(ui, "chevronRight", tl!("Next Page"), page + 1 < total) {
|
||||
app.ui.print.preview = page + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The "save a copy" formats (File › Export, and below the settings on the Print page).
|
||||
pub fn export_formats(app: &mut WordApp, ui: &mut Ui) {
|
||||
for (label, ext) in [
|
||||
("PDF document (*.pdf)", "pdf"),
|
||||
("Word document (*.docx)", "docx"),
|
||||
("Word XML Document (*.xml)", "xml"),
|
||||
("OpenDocument Text (*.odt)", "odt"),
|
||||
("Rich Text Format (*.rtf)", "rtf"),
|
||||
("Web page (*.html)", "html"),
|
||||
("Markdown (*.md)", "md"),
|
||||
("LaTeX (*.tex)", "tex"),
|
||||
("Plain text (*.txt)", "txt"),
|
||||
("Page image (*.png)", "png"),
|
||||
] {
|
||||
if ui.add(egui::Button::new(egui::RichText::new(tl!(label)).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
|
||||
app.export_dialog(ext);
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes.
|
|||
| 3 | **Crashes and freezes in the field**: startup crash on Intel UHD Graphics (#170) (lag on GPUs without a DirectX 12 driver fixed, #316), freezes (#77, #59 Thai fonts, #31 font selector), no writes without admin on Windows 11 (#218), broken shape tool (#142), images can't be moved (#82) | B | 62 open issues, ~15 of them crashes, hangs or data-path failures | Anyone hit loses work or trust; the never-crash standard covers panics, not hangs or GPU driver faults | 20–30 | [target-app-parity.md](target-app-parity.md) (Stability) |
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options ignored; no page-by-page comparison harness | B | [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 5 | **Content controls** landed (#381): `w:sdt` kept, shown and edited (Developer tab, Design Mode, Properties). Move tracking landed (#400): `w:moveFrom`/`w:moveTo` with their named ranges round-trip, a tracked cut and paste of a paragraph or sentence records a move, either end accepts/rejects both, green double strike/underline markup and "Moved from/to" balloons; still missing: drag-and-drop moves (no text drag yet) and move options in Track Changes Options. Formatting revisions landed (#41): `w:rPrChange`/`w:pPrChange`/`w:tblPrChange`/`w:trPrChange`/`w:tcPrChange`/`w:sectPrChange`/`w:numberingChange` round-trip, are recorded while tracking, accepted/rejected and shown in balloons; still missing: tracked style-definition changes and table/section changes made in WordCraft | B | Issues #41, #400; source grep | Legal and editorial review workflows; templates with form controls | 5–8 | [file-format-parity.md](file-format-parity.md) |
|
||||
| 6 | **No native printing** on desktop: File › Print opens the PDF in the system viewer, which prints (#286); no print dialog of our own (printer, copies, ranges); web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
|
||||
| 6 | **No native printing** on desktop: File › Print opens the PDF in the system viewer, which prints (#286); File › Print has a preview, page range, odd/even, copies and collate (#641); printer choice, duplex and pages per sheet are still the system's; web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
|
||||
| 7 | **Dialog depth**: 54 modal dialogs vs Word's ~100 | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs` | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
|
||||
| 8 | **Proofing is English only**; no language per run, no dictionaries for other languages | | `crates/proof` has one dictionary; Word ships 120 proofing tools; issues #25, #40, #100 | Every non-English writer | 25–45 | [localization-parity.md](localization-parity.md) |
|
||||
| 9 | **Password-protected files** can't be opened or saved | | #55; `docbin` detects and refuses RC4/XOR | Corporate and legal users | 6–10 | [file-format-parity.md](file-format-parity.md) |
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
|
|||
| Modal dialogs | **54** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Templates and Add-ins, Organizer, Compare and Combine, AutoCorrect Options, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402); Line Numbers, Hyphenation with Manual Hyphenation, Language, Envelopes, Labels (#407)) plus panes and menus for the rest | ~100 (Options panes…) | ~45% | 11–19 |
|
||||
| Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation, Styles, Comments, Thesaurus, Restrict Editing, Accessibility (grouped findings, jump-to, fixes) and Alt Text (#386), Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 |
|
||||
| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); right-click selects a picture, shape or text box and offers Wrap Text (styles, Move with Text / Fix Position on Page, #513); tables partial | rich per context | 60% | 3–5 |
|
||||
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485) | | 68% | 4–6 |
|
||||
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268), Options in Word's panes with working settings (#485), Print page with preview, page range and copies (#641) | | 68% | 4–6 |
|
||||
| Rulers: indents, tabs, margins, table columns | indents draggable (one undo, #62); tabs; table column drag pending (#49) | ✅ | 70% | 2–3 |
|
||||
| On-canvas objects: select, drag, resize handles, nudge | ✅ tested (#228) | ✅ | 75% | — |
|
||||
| Rotation handle, crop handles on canvas, alignment guides, smart guides | rotation handle (Shift: 15° steps), turned frames resize along their axes (#332); crop handles and guides missing | ✅ | 25% | 3–5 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue