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
@@ -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)); } });@@ -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() {