collective

Browse / github.com/storytold/photocraft / patch

Print: accept fractional print sizes (#9704)

Print dialog doesn't allow fractional sizes (like Photoshop does) I am often printing images at very specific dimensions especially (example use case: when I'm creating card proxies for various card games I play). In Photoshop I will often print images at (as an example): 2.5x3.5 inches so they fit in a card sleeve. I do this by updating the size settings in the print dialog. In PhotoCraft, this doesn't work as it doesn't accept the decimal places in the print dialog. The only workaround I have is to switch the "position and size" to millimeters and specify the card sizes in millimeters instead (so I don't need fractionals), but this is obviously less than

Status
open
Score
0.00
Adopters
0 other operators
Author
NaufalDzz (imported from pull request #9829, open)
Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035
Size
+50 −16 in 2 files
Review
pending
Shared
2026-10-28
Id
7c2eb15e8f20edc20633c66c

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/dialogs/file_ui/print.rs crates/ui-egui/src/dialogs/file_ui/tests.rs

diff --git a/crates/ui-egui/src/dialogs/file_ui/print.rs b/crates/ui-egui/src/dialogs/file_ui/print.rs--- a/crates/ui-egui/src/dialogs/file_ui/print.rs+++ b/crates/ui-egui/src/dialogs/file_ui/print.rs@@ -141,20 +141,16 @@ fn unit_pt(units: &str) -> f64 {     } } -/// A number field in `units` for a value kept in points; true when edited.+/// A nonnegative length field (paper and print size) in `units` for a value kept in points; true+/// when edited. fn length_field(ui: &mut egui::Ui, label: &str, pt: &mut f64, units: &str) -> bool {-    length_field_prec(ui, label, pt, units, 0)-}--/// Edits a point-valued length in the selected units, with the requested decimal precision.-/// Retains the nonnegative 0..=10000 display-unit range used by existing size fields.-fn length_field_prec(ui: &mut egui::Ui, label: &str, pt: &mut f64, units: &str, decimals: u32) -> bool {-    length_field_range(ui, label, pt, units, decimals, 0.0..=10000.0)+    length_field_range(ui, label, pt, units, 0.0..=10000.0) }  /// Edits a length stored in print points (72 per inch), returning true for a finite edit. /// `range` is in the displayed `units`; a signed range permits negative Top/Left offsets.-fn length_field_range(ui: &mut egui::Ui, label: &str, pt: &mut f64, units: &str, decimals: u32, range: std::ops::RangeInclusive<f32>) -> bool {+/// Shows the unit's own decimals, as Photoshop does, so 2.5 in or 6.35 cm are kept (#9704).+fn length_field_range(ui: &mut egui::Ui, label: &str, pt: &mut f64, units: &str, range: std::ops::RangeInclusive<f32>) -> bool {     let t = Tokens::get(ui.ctx());     let k = unit_pt(units);     let mut v = (*pt / k) as f32;@@ -162,6 +158,7 @@ fn length_field_range(ui: &mut egui::Ui, label: &str, pt: &mut f64, units: &str,     ui.horizontal(|ui| {         ui.label(egui::RichText::new(label).color(t.text_dim));         let before = v;+        let decimals = crate::widgets::unit_decimals(units);         crate::widgets::value_field_prec(ui, &mut v, range, units, 70.0, decimals).accessible_name(tl!(&label));         if (v - before).abs() > 1e-4 && v.is_finite() {             *pt = f64::from(v) * k;@@ -208,8 +205,8 @@ fn print_paper_controls(ui: &mut egui::Ui, f: &mut Map<String, Value>) {         let units = s(f, "units", "cm");         let changed = ui             .horizontal(|ui| {-                let wc = length_field_prec(ui, tl!("Paper Width"), &mut w, &units, 2);-                let hc = length_field_prec(ui, tl!("Paper Height"), &mut h, &units, 2);+                let wc = length_field(ui, tl!("Paper Width"), &mut w, &units);+                let hc = length_field(ui, tl!("Paper Height"), &mut h, &units);                 wc || hc             })             .inner;@@ -535,7 +532,7 @@ pub(super) fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map                         let mut pt =                             centered_layout.map_or_else(|| n(f, key, 0.0) * 72.0, |l| if key == "top" { l.paper.1 - l.rect.1 - l.rect.3 } else { l.rect.0 });                         let limit = (photocraft_engine::print_cmds::MAX_POSITION_INCHES * 72.0 / unit_pt(&units)) as f32;-                        if length_field_range(ui, label, &mut pt, &units, 2, -limit..=limit) {+                        if length_field_range(ui, label, &mut pt, &units, -limit..=limit) {                             let limit = photocraft_engine::print_cmds::MAX_POSITION_INCHES;                             f.insert(key.into(), json!((pt / 72.0).clamp(-limit, limit)));                         }@@ -549,12 +546,14 @@ pub(super) fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map                 let (mut w2, mut h2) = (w, h);                 let fit = b(f, "scaleToFit", false);                 if !fit {-                    // Two decimals: Height and Width set fractional scales.+                    // Written only when edited: the exact scale Height or Width set must survive+                    // the field's rounded display.                     ui.horizontal(|ui| {                         ui.label(egui::RichText::new(tl!("Scale")).color(t.text_dim));                         let mut v = n(f, "scale", 100.0) as f32;-                        crate::widgets::value_field(ui, &mut v, 1.0..=1000.0, "%", 64.0).accessible_name(tl!("Scale"));-                        f.insert("scale".into(), json!((f64::from(v) * 100.0).round() / 100.0));+                        if crate::widgets::value_field(ui, &mut v, 1.0..=1000.0, "%", 64.0).accessible_name(tl!("Scale")).changed() {+                            f.insert("scale".into(), json!((f64::from(v) * 100.0).round() / 100.0));+                        }                     });                 }                 ui.horizontal(|ui| {@@ -568,7 +567,8 @@ pub(super) fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map                         None                     };                     if let Some(k) = k.filter(|k| k.is_finite() && *k > 0.0) {-                        f.insert("scale".into(), json!(((l.scale * k * 100.0) * 100.0).round() / 100.0));+                        // Unrounded, so the typed size prints exactly.+                        f.insert("scale".into(), json!(l.scale * k * 100.0));                         f.insert("scaleToFit".into(), json!(false));                     }                 });diff --git a/crates/ui-egui/src/dialogs/file_ui/tests.rs b/crates/ui-egui/src/dialogs/file_ui/tests.rs--- a/crates/ui-egui/src/dialogs/file_ui/tests.rs+++ b/crates/ui-egui/src/dialogs/file_ui/tests.rs@@ -504,6 +504,40 @@ fn print_dialog_does_not_inherit_scripted_send_override() {     } } +/// #9704: Width and Height take a fractional size, such as a 2.5 in card, and keep it as typed.+#[test]+fn print_size_keeps_a_typed_fractional_width() {+    use egui::accesskit::Role;+    use egui_kittest::{Harness, kittest::Queryable};+    let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());+    // 30 × 15 in: wide enough that a scale rounded to 0.01 % would move 2.5 in to 2.499 in.+    app.run("file.new", json!({"width": 3000, "height": 1500, "resolution": 100})).unwrap();+    let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state(|ui, app| crate::dialogs::show(app, ui.ctx()), app);+    PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);+    // Opened once the fonts are set up: the harness draws a frame as it is built.+    let id = open_print(h.state_mut());+    let f = &mut h.state_mut().ui.dialog_mut(id).unwrap().fields;+    f.insert("units".into(), json!("in"));+    f.insert("dryRun".into(), json!(true));+    h.run_steps(3);+    h.get_by_role_and_label(Role::SpinButton, "Width").click();+    h.run_steps(1);+    for c in "2.5".chars() {+        h.event(egui::Event::Text(c.to_string()));+        h.run_steps(1);+    }+    // Tab leaves the field (Enter would confirm the dialog).+    h.key_press(egui::Key::Tab);+    h.run_steps(3);+    assert_eq!(h.get_by_role_and_label(Role::SpinButton, "Width").value().as_deref(), Some("2.5"));+    let app = h.state_mut();+    let doc = app.session.active().unwrap().doc.clone();+    let f = app.ui.dialog_mut(id).unwrap().fields.clone();+    let l = photocraft_engine::print_cmds::layout(&doc, &print_params(&f), "file.print").unwrap();+    assert!((l.rect.2 - 2.5 * 72.0).abs() < 1e-6, "printed width {} pt", l.rect.2);+    assert!((l.rect.3 - 1.25 * 72.0).abs() < 1e-6, "printed height {} pt", l.rect.3);+}+ /// The Orientation buttons show the driver paper's shape: landscape when it is wider than tall. #[test] fn orientation_follows_the_driver_paper() {