fix(print): read papers in background, restore properties handler, and pass dialog owner

This commit is contained in:
Combiler01 committed 2026-11-07 10:30:49 +05:30
1 parent 04fa9910ca
commit 25b6280ac6
9 files changed
+247 -87

No files matched your search

+1 -1
View File
@@ -43,4 +43,4 @@ pub use settings::{
#[cfg(test)]
use sheets::text_direction;
pub use sheets::{LABEL_MARGIN, MARGIN, Placement, Sheet, layout, matching_paper, text_reads_up};
pub use spool::{open_printer_preferences, printer_default_paper, printer_papers, show_printer_properties};
pub use spool::{open_printer_preferences, printer_default_paper, printer_paper_info, printer_papers, show_printer_properties};
+29 -23
View File
@@ -240,34 +240,47 @@ pub fn printer_papers(_printer: &str) -> Vec<Paper> {
Vec::new()
}
/// The default paper size configured for `printer` in the printer properties / DEVMODE.
/// Returns `Some((name, (width, height)))` if readable.
#[cfg(windows)]
pub fn printer_default_paper(printer: &str) -> Option<Paper> {
let settings = craft_print::rgb::default_settings(printer).ok()?;
let page = craft_print::rgb::page(printer, Some(&settings)).ok()?;
let w_pts = (page.paper.0 * 100.0).round() / 100.0;
let h_pts = (page.paper.1 * 100.0).round() / 100.0;
let papers = printer_papers(printer);
fn resolve_paper_name(papers: &[Paper], paper_pts: (f64, f64)) -> Paper {
let w_pts = (paper_pts.0 * 100.0).round() / 100.0;
let h_pts = (paper_pts.1 * 100.0).round() / 100.0;
let name = papers
.iter()
.find(|(_, (w, h))| ((*w - w_pts).abs() <= 1.0 && (*h - h_pts).abs() <= 1.0) || ((*w - h_pts).abs() <= 1.0 && (*h - w_pts).abs() <= 1.0))
.map(|(n, _)| n.clone())
.unwrap_or_default();
Some((name, (w_pts, h_pts)))
(name, (w_pts, h_pts))
}
#[cfg(not(windows))]
pub fn printer_default_paper(_printer: &str) -> Option<Paper> {
None
/// The default paper size configured for `printer` in the printer properties / DEVMODE.
/// Returns `Some((name, (width, height)))` if readable.
pub fn printer_default_paper(printer: &str) -> Option<Paper> {
printer_paper_info(printer).1
}
/// Reads the supported paper sizes and the default paper size configured for `printer`.
pub fn printer_paper_info(printer: &str) -> (Vec<Paper>, Option<Paper>) {
let papers = printer_papers(printer);
#[cfg(windows)]
let default_paper = {
let settings = craft_print::rgb::default_settings(printer).ok();
let page = settings.as_ref().and_then(|s| craft_print::rgb::page(printer, Some(s)).ok());
page.map(|p| resolve_paper_name(&papers, p.paper))
};
#[cfg(not(windows))]
let default_paper = {
let _ = &papers;
None
};
(papers, default_paper)
}
/// Shows the printer driver's Print Settings / DocumentProperties dialog (Windows).
/// If the user clicks OK, returns the updated paper size.
pub fn show_printer_properties(printer: &str) -> Result<Option<Paper>, crate::PrintError> {
pub fn show_printer_properties(printer: &str, parent: Option<isize>) -> Result<Option<Paper>, crate::PrintError> {
#[cfg(windows)]
{
let devmode = match craft_print::rgb::settings_dialog(printer, None, None) {
let devmode = match craft_print::rgb::settings_dialog(printer, None, parent) {
Ok(Some(dm)) => dm,
Ok(None) => return Ok(None),
Err(_) => {
@@ -276,19 +289,12 @@ pub fn show_printer_properties(printer: &str) -> Result<Option<Paper>, crate::Pr
}
};
let page = craft_print::rgb::page(printer, Some(&devmode)).map_err(|e| crate::PrintError::Spool(e.to_string()))?;
let w_pts = (page.paper.0 * 100.0).round() / 100.0;
let h_pts = (page.paper.1 * 100.0).round() / 100.0;
let papers = printer_papers(printer);
let name = papers
.iter()
.find(|(_, (w, h))| ((*w - w_pts).abs() <= 1.0 && (*h - h_pts).abs() <= 1.0) || ((*w - h_pts).abs() <= 1.0 && (*h - w_pts).abs() <= 1.0))
.map(|(n, _)| n.clone())
.unwrap_or_default();
Ok(Some((name, (w_pts, h_pts))))
Ok(Some(resolve_paper_name(&papers, page.paper)))
}
#[cfg(not(windows))]
{
let _ = printer;
let _ = (printer, parent);
Ok(None)
}
}
+1
View File
@@ -114,6 +114,7 @@ impl PdfCraftApp {
printer_source: None,
options_source: None,
default_paper_source: None,
printer_papers_source: None,
print_tasks: print_ui::PrintTasks::default(),
palette_open: false,
palette_query: String::new(),
+1 -1
View File
@@ -1325,7 +1325,7 @@ fn dialog_print(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending) ->
let view = &mut app.views[i];
view.set_print_grayscale(app.print_draft.grayscale);
view.queue_print_previews(&rasters);
let (go, cancel) = crate::print_ui::body(ui, &mut app.print_draft, &t, &sizes, &labels, app.default_paper_source.as_ref(), &mut |p| {
let (go, cancel) = crate::print_ui::body(ui, &mut app.print_draft, &t, &sizes, &labels, &mut |p| {
view.need_thumbnail(p, true);
view.page_preview(p)
});
+6 -1
View File
@@ -58,7 +58,10 @@ mod editing;
mod home;
mod hotbar;
mod panels;
pub use print_ui::{DefaultPaperSource, Handling as PrintHandling, OptionsSource, PrintDraft, PrinterSource, Which as PrintWhich, system_printers};
pub use print_ui::{
DefaultPaperSource, Handling as PrintHandling, OptionsSource, PrintDraft, PrinterPapersSource, PrinterSource, Which as PrintWhich,
system_printers,
};
pub use redact_ui::{HiddenDraft, PagesDraft as RedactPagesDraft, RedactPrefs, SearchDraft as RedactSearchDraft, SearchMode as RedactSearchMode};
pub mod i18n;
pub mod theme;
@@ -343,6 +346,8 @@ pub struct PdfCraftApp {
pub options_source: Option<OptionsSource>,
/// Where a printer's configured default paper comes from: DEVMODE / driver settings unless a test stands in.
pub default_paper_source: Option<DefaultPaperSource>,
/// Where a printer's supported paper sizes come from: driver capabilities unless a test stands in.
pub printer_papers_source: Option<PrinterPapersSource>,
// A browser has no background print work: the type is empty there.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) print_tasks: print_ui::PrintTasks,
+108 -14
View File
@@ -37,11 +37,20 @@ pub type OptionsSource = std::sync::Arc<dyn Fn(&str) -> Vec<spool::PrinterOption
/// unless a test stands in.
pub type DefaultPaperSource = std::sync::Arc<dyn Fn(&str) -> Option<(String, (f64, f64))> + Send + Sync>;
/// Where a printer's supported paper sizes come from: the spooler driver capabilities
/// unless a test stands in.
pub type PrinterPapersSource = std::sync::Arc<dyn Fn(&str) -> Vec<spool::Paper> + Send + Sync>;
/// The system spooler's printers, as the desktop app's [`PrinterSource`].
pub fn system_printers() -> PrinterSource {
std::sync::Arc::new(spool::printers)
}
#[cfg(not(target_arch = "wasm32"))]
type PaperInfo = (Vec<spool::Paper>, Option<spool::Paper>);
#[cfg(not(target_arch = "wasm32"))]
type PendingPaperInfo = (String, std::sync::mpsc::Receiver<PaperInfo>);
/// The Print dialog's work in the background: the printer list being read, and the jobs being
/// spooled, oldest first.
#[derive(Default)]
@@ -56,6 +65,12 @@ pub(crate) struct PrintTasks {
/// The driver options being read, per printer (`read_options_in_background`).
#[cfg(not(target_arch = "wasm32"))]
options: Vec<(String, std::sync::mpsc::Receiver<Vec<spool::PrinterOption>>)>,
/// The papers and default paper being read in the background, per printer (`read_papers_in_background`).
#[cfg(not(target_arch = "wasm32"))]
papers: Vec<PendingPaperInfo>,
/// Cached papers and default paper per printer so switching printers avoids re-querying.
#[cfg(not(target_arch = "wasm32"))]
papers_cache: std::collections::HashMap<String, PaperInfo>,
/// The browser print being drawn, a sheet a frame (`web_print`).
#[cfg(target_arch = "wasm32")]
web_job: Option<crate::web_print::WebPrintJob>,
@@ -168,24 +183,20 @@ impl PdfCraftApp {
printers_loading,
printer_picked: picked,
units: self.units,
printer,
printer: printer.clone(),
current_page: current,
sheet: 0,
selected,
which,
..keep
};
self.print_draft.update_printer_papers();
if !self.print_draft.paper_overridden {
self.auto_select_default_paper();
#[cfg(not(target_arch = "wasm32"))]
if let Some(p) = printer {
self.read_papers_in_background(p);
}
self.dialog = Some(crate::Dialog::Print);
}
pub(crate) fn auto_select_default_paper(&mut self) {
self.print_draft.auto_select_default_paper_with(self.default_paper_source.as_ref());
}
/// Label printing: find each printed page's printed area (a quick low-resolution render, as
/// Fit Visible does) and which way its text runs. Only the pages being printed are read, at
/// most [`LABEL_CROP_PAGES`] of them (the rest print whole); read again when they change.
@@ -286,12 +297,93 @@ impl PdfCraftApp {
}
}
/// Read `printer`'s papers and default paper off the GUI thread (#5861); `poll_print` picks them up.
/// A read already under way for the same printer is left to finish.
#[cfg(not(target_arch = "wasm32"))]
fn read_papers_in_background(&mut self, printer: String) {
if let Some((papers, default_paper)) = self.print_tasks.papers_cache.get(&printer).cloned() {
self.receive_papers(printer, papers, default_paper);
return;
}
if self.print_tasks.papers.iter().any(|(p, _)| *p == printer) {
return;
}
self.print_draft.papers_loading = Some(printer.clone());
let papers_source = self.printer_papers_source.clone();
let default_source = self.default_paper_source.clone();
let (tx, rx) = std::sync::mpsc::channel();
let wake = WakeOnExit(self.ctx.clone());
let name = printer.clone();
let spawned = std::thread::Builder::new().name("printer papers".into()).spawn(move || {
let _wake = wake;
let tx = tx;
let info = match (&papers_source, &default_source) {
(None, None) => spool::printer_paper_info(&name),
(papers_s, def_s) => {
let papers = match papers_s {
Some(s) => s(&name),
None => spool::printer_papers(&name),
};
let def = match def_s {
Some(s) => s(&name),
None => spool::printer_default_paper(&name),
};
(papers, def)
}
};
let _ = tx.send(info);
});
match spawned {
Ok(_) => self.print_tasks.papers.push((printer, rx)),
Err(_) => {
let d = &mut self.print_draft;
if d.papers_loading.as_deref() == Some(printer.as_str()) {
d.papers_loading = None;
}
}
}
}
/// The papers and default paper read for `printer` arrived, or the read died.
#[cfg(not(target_arch = "wasm32"))]
fn receive_papers(&mut self, printer: String, papers: Vec<spool::Paper>, default_paper: Option<spool::Paper>) {
self.print_tasks.papers_cache.insert(printer.clone(), (papers.clone(), default_paper.clone()));
let d = &mut self.print_draft;
if d.papers_loading.as_deref() == Some(printer.as_str()) {
d.papers_loading = None;
}
if d.printer.as_deref() == Some(printer.as_str()) {
d.apply_printer_papers(papers);
if !d.paper_overridden {
d.apply_default_paper(default_paper);
}
}
}
/// Pick up a printer list, driver options or a spooled job that finished (each frame).
pub(crate) fn poll_print(&mut self) {
#[cfg(target_arch = "wasm32")]
self.poll_web_print();
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(printer) = self.print_draft.read_papers_for.take() {
self.read_papers_in_background(printer);
}
let mut arrived_papers = Vec::new();
self.print_tasks.papers.retain(|(printer, rx)| match rx.try_recv() {
Ok((papers, def)) => {
arrived_papers.push((printer.clone(), papers, def));
false
}
Err(std::sync::mpsc::TryRecvError::Empty) => true,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
arrived_papers.push((printer.clone(), Vec::new(), None));
false
}
});
for (printer, papers, def) in arrived_papers {
self.receive_papers(printer, papers, def);
}
if let Some(printer) = self.print_draft.read_options_for.take() {
self.read_options_in_background(printer);
}
@@ -322,16 +414,18 @@ impl PdfCraftApp {
self.print_tasks.printers = None;
let d = &mut self.print_draft;
// A picked printer that is no longer listed falls back to the default too.
if !d.printer_picked || !offered(d.printer.as_deref(), &printers) {
let changed = if !d.printer_picked || !offered(d.printer.as_deref(), &printers) {
d.printer_picked = false;
d.printer = default_printer(&printers);
d.update_printer_papers();
if !d.paper_overridden {
d.auto_select_default_paper_with(self.default_paper_source.as_ref());
}
}
true
} else {
false
};
d.printers = printers;
d.printers_loading = false;
if changed && let Some(p) = d.printer.clone() {
self.read_papers_in_background(p);
}
}
}
let mut finished = Vec::new();
+8 -13
View File
@@ -87,7 +87,6 @@ pub(crate) fn body(
t: &Tokens,
sizes: &[(f64, f64)],
labels: &[String],
default_paper_source: Option<&super::DefaultPaperSource>,
thumb: &mut dyn FnMut(usize) -> Option<egui::TextureId>,
) -> (bool, bool) {
ui.set_width(820.0);
@@ -142,14 +141,7 @@ pub(crate) fn body(
&& let Some(p) = d.printer.clone()
{
if spool::HAS_PRINTER_PREFERENCES {
d.driver_error = None;
match spool::show_printer_properties(&p) {
Ok(Some((name, size))) => {
d.apply_driver_paper(name, size);
}
Ok(None) => {}
Err(e) => d.driver_error = Some(e.to_string()),
}
d.driver_error = spool::open_printer_preferences(&p).err().map(|e| e.to_string());
} else {
if d.driver_options.as_ref().is_none_or(|(read_for, _)| *read_for != p) && d.driver_options_loading.as_deref() != Some(p.as_str()) {
// Read off the GUI thread (#1818): `poll_print` starts it next frame.
@@ -166,9 +158,8 @@ pub(crate) fn body(
d.printer_picked = true;
d.show_driver_options = false;
d.driver_error = None;
d.update_printer_papers();
if !d.paper_overridden {
d.auto_select_default_paper_with(default_paper_source);
if let Some(p) = d.printer.clone() {
d.read_papers_for = Some(p);
}
}
ui.end_row();
@@ -239,7 +230,11 @@ pub(crate) fn body(
&& let Some(p) = d.printer.clone()
{
d.driver_error = None;
match spool::show_printer_properties(&p) {
#[cfg(windows)]
let parent = winsafe::HWND::GetActiveWindow().map(|h| h.ptr() as isize);
#[cfg(not(windows))]
let parent = None;
match spool::show_printer_properties(&p, parent) {
Ok(Some((name, size))) => {
d.apply_driver_paper(name, size);
}
+65 -32
View File
@@ -87,6 +87,10 @@ pub struct PrintDraft {
pub driver_options_loading: Option<String>,
/// Properties… asked for this printer's options; the app starts the read on the next frame.
pub read_options_for: Option<String>,
/// The printer whose papers and default paper are being read in the background.
pub papers_loading: Option<String>,
/// A printer change requested reading papers; the app starts the read on the next frame.
pub read_papers_for: Option<String>,
}
impl Default for PrintDraft {
@@ -137,6 +141,8 @@ impl Default for PrintDraft {
crop_pages: None,
driver_options_loading: None,
read_options_for: None,
papers_loading: None,
read_papers_for: None,
}
}
}
@@ -168,21 +174,33 @@ impl PrintDraft {
/// Refresh `self.papers` with driver-reported media sizes, preserving the current selection.
pub fn update_printer_papers(&mut self) {
self.update_printer_papers_with(None);
}
/// Refresh `self.papers` with driver-reported media sizes from an optional source or the driver.
pub fn update_printer_papers_with(&mut self, source: Option<&super::PrinterPapersSource>) {
let detected = if let Some(ref printer) = self.printer {
if let Some(source) = source { source(printer) } else { spool::printer_papers(printer) }
} else {
Vec::new()
};
self.apply_printer_papers(detected);
}
/// Apply driver-reported media sizes to `self.papers`, preserving the current selection.
pub fn apply_printer_papers(&mut self, detected: Vec<(String, (f64, f64))>) {
let before_label = self.paper_before_label.and_then(|idx| self.papers.get(idx).cloned());
let mut papers = default_papers();
if let Some(ref printer) = self.printer {
let detected = spool::printer_papers(printer);
for (name, size) in detected {
if !(size.0 >= 72.0 && size.1 >= 72.0 && size.0.is_finite() && size.1.is_finite()) {
continue;
}
let norm = (size.0.min(size.1), size.0.max(size.1));
if !papers.iter().any(|(_, s)| {
let s_norm = (s.0.min(s.1), s.0.max(s.1));
(s_norm.0 - norm.0).abs() <= 0.5 && (s_norm.1 - norm.1).abs() <= 0.5
}) {
papers.push((name, size));
}
for (name, size) in detected {
if !(size.0 >= 72.0 && size.1 >= 72.0 && size.0.is_finite() && size.1.is_finite()) {
continue;
}
let norm = (size.0.min(size.1), size.0.max(size.1));
if !papers.iter().any(|(_, s)| {
let s_norm = (s.0.min(s.1), s.0.max(s.1));
(s_norm.0 - norm.0).abs() <= 0.5 && (s_norm.1 - norm.1).abs() <= 0.5
}) {
papers.push((name, size));
}
}
let current_name = self.paper_name().to_string();
@@ -217,27 +235,34 @@ impl PrintDraft {
/// Auto-select the printer's default paper using an optional source (or DEVMODE / driver settings).
pub fn auto_select_default_paper_with(&mut self, source: Option<&super::DefaultPaperSource>) {
if self.handling == Handling::Label {
return;
}
if let Some(ref printer) = self.printer {
let detected = if let Some(source) = source { source(printer) } else { spool::printer_default_paper(printer) };
if let Some((name, size)) = detected {
let overridden = self.paper_overridden;
self.apply_driver_paper(name, size);
self.paper_overridden = overridden;
self.apply_default_paper(detected);
}
}
/// Apply the detected default paper.
pub fn apply_default_paper(&mut self, detected: Option<(String, (f64, f64))>) {
if let Some((name, size)) = detected
&& let Some(pos) = self.find_or_add_paper(name, size)
{
if self.handling == Handling::Label {
if !self.paper_overridden {
self.paper_before_label = Some(pos);
}
} else {
self.paper = pos;
}
}
}
/// Apply paper size and orientation chosen in printer properties or page setup.
pub fn apply_driver_paper(&mut self, name: String, size: (f64, f64)) {
/// Locate `(name, size)` in `self.papers`, or append it if missing. Returns the paper index.
pub fn find_or_add_paper(&mut self, name: String, size: (f64, f64)) -> Option<usize> {
if !(size.0 >= 72.0 && size.1 >= 72.0 && size.0.is_finite() && size.1.is_finite()) {
return;
return None;
}
self.paper_overridden = true;
if let Some(pos) = self.matching_paper(size) {
self.paper = pos;
Some(pos)
} else if !name.is_empty() {
let norm = (size.0.min(size.1), size.0.max(size.1));
if let Some(pos) = self.papers.iter().position(|(n, s)| {
@@ -246,20 +271,28 @@ impl PrintDraft {
(s_norm.0 - norm.0).abs() <= 1.0 && (s_norm.1 - norm.1).abs() <= 1.0
}
}) {
self.paper = pos;
Some(pos)
} else {
self.papers.push((name, size));
self.paper = self.papers.len() - 1;
Some(self.papers.len() - 1)
}
} else {
let custom_name = format!("{:.1} x {:.1} pt", size.0, size.1);
self.papers.push((custom_name, size));
self.paper = self.papers.len() - 1;
Some(self.papers.len() - 1)
}
if size.0 > size.1 {
self.orientation = Orientation::Landscape;
} else if size.1 > size.0 {
self.orientation = Orientation::Portrait;
}
/// Apply paper size and orientation chosen in printer properties or page setup.
pub fn apply_driver_paper(&mut self, name: String, size: (f64, f64)) {
if let Some(pos) = self.find_or_add_paper(name, size) {
self.paper_overridden = true;
self.paper = pos;
if size.0 > size.1 {
self.orientation = Orientation::Landscape;
} else if size.1 > size.0 {
self.orientation = Orientation::Portrait;
}
}
}
+28 -2
View File
@@ -812,10 +812,17 @@ fn paper_choice_survives_cancel_and_settings_restore() {
use std::sync::Arc;
default_h.state_mut().printer_source =
Some(Arc::new(|| vec![spool::Printer { name: "Office Printer".into(), default: true, two_sided: None, description: String::new() }]));
default_h.state_mut().printer_papers_source = Some(Arc::new(|_| vec![("A4".into(), (595.28, 841.89))]));
default_h.state_mut().default_paper_source =
Some(Arc::new(|printer: &str| if printer == "Office Printer" { Some(("A4".into(), (595.28, 841.89))) } else { None }));
assert!(default_h.state_mut().execute("print.dialog"));
default_h.run_steps(3);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while (default_h.state().print_draft.printers_loading || default_h.state().print_draft.papers_loading.is_some())
&& std::time::Instant::now() < deadline
{
default_h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert_eq!(default_h.state().print_draft.paper, a4);
assert!(!default_h.state().print_draft.paper_overridden);
let mut restarted = harness_with_settings(Some(saved));
@@ -894,10 +901,15 @@ fn legacy_settings_restore_unsets_override_and_open_selects_printer_default() {
h.state_mut().printer_source =
Some(Arc::new(|| vec![spool::Printer { name: "Office Printer".into(), default: true, two_sided: None, description: String::new() }]));
h.state_mut().printer_papers_source = Some(Arc::new(|_| vec![("A4".into(), (595.28, 841.89))]));
h.state_mut().default_paper_source =
Some(Arc::new(|printer: &str| if printer == "Office Printer" { Some(("A4".into(), (595.28, 841.89))) } else { None }));
assert!(h.state_mut().execute("print.dialog"));
h.run_steps(3);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while (h.state().print_draft.printers_loading || h.state().print_draft.papers_loading.is_some()) && std::time::Instant::now() < deadline {
h.run_steps(1);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert_eq!(h.state().print_draft.paper, a4);
assert!(!h.state().print_draft.paper_overridden);
}
@@ -910,9 +922,23 @@ fn auto_select_default_paper_leaves_label_stock_unchanged() {
paper: pdfcraft_engine::print::LABEL_PAPER,
..Default::default()
};
let a4 = pdfcraft_engine::print::PAPERS.iter().position(|(name, _)| *name == "A4").unwrap();
let source: pdfcraft_ui_egui::DefaultPaperSource = std::sync::Arc::new(move |_| Some(("A4".into(), (595.28, 841.89))));
draft.auto_select_default_paper_with(Some(&source));
assert_eq!(draft.paper, pdfcraft_engine::print::LABEL_PAPER);
assert_eq!(draft.paper_before_label, Some(a4));
}
#[test]
fn auto_select_default_paper_preserves_user_orientation() {
let mut draft = pdfcraft_ui_egui::PrintDraft {
printer: Some("Office Printer".into()),
orientation: pdfcraft_engine::print::Orientation::Landscape,
..Default::default()
};
let source: pdfcraft_ui_egui::DefaultPaperSource = std::sync::Arc::new(move |_| Some(("A4".into(), (595.28, 841.89))));
draft.auto_select_default_paper_with(Some(&source));
assert_eq!(draft.orientation, pdfcraft_engine::print::Orientation::Landscape);
}
#[test]