Merge pull request #6215 from davidmann4/feat/ui-217-4-columns-unit

Image Size: a Columns width unit from the Column Size preference (#6214)
This commit is contained in:
David Mann
2026-11-10 19:36:23 +01:00
committed by GitHub
8 changed files with 314 additions and 26 deletions
+3
View File
@@ -924,7 +924,10 @@ impl TransparencyAndGamut {
pub struct UnitsAndRulers {
pub rulers: Unit,
pub type_units: TypeUnit,
/// Column Size › Width in points: Image Size and Canvas Size's Columns width unit
/// (`n` columns are `n × column_width + (n − 1) × gutter`).
pub column_width: f64,
/// Column Size › Gutter in points, the space between columns.
pub gutter: f64,
pub print_resolution: f64,
pub screen_resolution: f64,
-2
View File
@@ -40,8 +40,6 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
"workspace.autoShowHiddenPanels",
"workspace.enableFloatingDocumentWindowDocking",
"performance.legacyCompositing",
"unitsAndRulers.columnWidth",
"unitsAndRulers.gutter",
"plugIns.showExtensionPanels",
"plugIns.allowScriptsToConnect",
"type.textEngine",
+130 -16
View File
@@ -13,6 +13,56 @@ use crate::theme::Tokens;
const UNITS: [(&str, &str); 7] =
[("px", "Pixels"), ("percent", "Percent"), ("in", "Inches"), ("cm", "Centimeters"), ("mm", "Millimeters"), ("pt", "Points"), ("pica", "Picas")];
/// Width-only unit of Image Size and Canvas Size (height has no Columns). Image Size measures it
/// in columns of Preferences › Units & Rulers › Column Size; Canvas Size in whole inches (see
/// [`canvas_columns_to_px`]).
const COLUMNS: (&str, &str) = ("col", "Columns");
/// Decimal places a Columns field shows.
const COLUMN_DECIMALS: u32 = 3;
/// Column Size in points, read from the dialog: (width, gutter). Photoshop's 180 pt / 12 pt when
/// missing or degenerate: a width that isn't a positive finite number, or a gutter that isn't a
/// finite number of at least zero (0 then), so the conversion never divides by zero.
fn column_size(f: &Map<String, Value>) -> (f64, f64) {
let w = f.get("__colWidth").and_then(Value::as_f64).filter(|v| v.is_finite() && *v > 0.0).unwrap_or(180.0);
let g = f.get("__colGutter").and_then(Value::as_f64).filter(|v| v.is_finite()).map_or(12.0, |g| g.max(0.0));
(w, g)
}
/// Pixels → columns: `n` columns span `n × width + (n − 1) × gutter` points, so a width of
/// `pt` points is `(pt + gutter) / (width + gutter)` columns. With `relative` (Canvas Size's
/// deltas) the count applies to the magnitude and zero stays zero.
fn px_to_columns(px: f64, res: f64, ppi: f64, (w, g): (f64, f64), relative: bool) -> f64 {
let pt = px / res.max(1.0) * ppi;
if relative {
if pt == 0.0 {
return 0.0;
}
return pt.signum() * (pt.abs() + g) / (w + g);
}
(pt + g) / (w + g)
}
/// Columns → pixels, the inverse of [`px_to_columns`].
fn columns_to_px(n: f64, res: f64, ppi: f64, (w, g): (f64, f64), relative: bool) -> f64 {
let span = |n: f64| n * (w + g) - g;
let pt = if relative { if n == 0.0 { 0.0 } else { n.signum() * span(n.abs()).max(0.0) } } else { span(n) };
pt / ppi * res.max(1.0)
}
/// Canvas Size's Columns, measured in the reference application: one column is one inch at the
/// document resolution, whatever Column Size says, and only whole columns count (1.5 is 2; a
/// 1001 px canvas at 300 ppi shows as 3). Relative deltas the same way.
fn canvas_columns_to_px(n: f64, res: f64) -> f64 {
n.round() * res.max(1.0)
}
/// Pixels → Canvas Size columns, rounded for display; the inverse of [`canvas_columns_to_px`].
fn canvas_px_to_columns(px: f64, res: f64) -> f64 {
(px / res.max(1.0)).round()
}
/// Dialog unit for the Units & Rulers preference.
fn pref_unit(u: photocraft_engine::prefs::Unit) -> &'static str {
use photocraft_engine::prefs::Unit;
@@ -72,6 +122,8 @@ pub fn open(app: &mut PhotocraftApp, command: &str) -> Option<u64> {
let unit = pref_unit(app.session.prefs().units_and_rulers.rulers);
let mut resample = pref_resample(app.session.prefs().general.image_interpolation);
let pt_per_in = app.session.prefs().units_and_rulers.point_size.per_inch();
let column_width = app.session.prefs().units_and_rulers.column_width;
let column_gutter = app.session.prefs().units_and_rulers.gutter;
let d = &app.session.active()?.doc;
let (w, h, res) = (d.size.width as f64, d.size.height as f64, d.resolution_dpi as f64);
let label = photocraft_engine::commands::find(command).map(|c| c.label).unwrap_or("Image Size…");
@@ -84,6 +136,8 @@ pub fn open(app: &mut PhotocraftApp, command: &str) -> Option<u64> {
f.insert("__origRes".into(), json!(res));
f.insert("__unit".into(), json!(unit));
f.insert("__ptPerIn".into(), json!(pt_per_in));
f.insert("__colWidth".into(), json!(column_width));
f.insert("__colGutter".into(), json!(column_gutter));
f.insert("__bytesPerPixel".into(), json!(d.layers.first().and_then(|l| l.surface()).map_or(4, |s| s.format().bytes_per_pixel())));
f.insert("width".into(), json!(w));
f.insert("height".into(), json!(h));
@@ -175,11 +229,34 @@ fn info_row(ui: &mut egui::Ui, k: &str, v: String) {
/// Width / height field in the current unit, writing back pixels. Returns true when edited.
fn dim_field(ui: &mut egui::Ui, f: &mut Map<String, Value>, key: &str, orig_key: &str, id: &str) -> bool {
let unit = f.get("__unit").and_then(Value::as_str).unwrap_or("px").to_string();
let unit = field_unit(f, key);
let res = f.get("resolution").and_then(Value::as_f64).unwrap_or(num(f, "__origRes"));
let orig = num(f, orig_key);
let px = num(f, key);
let ppi = pt_per_in(f);
if unit == COLUMNS.0 && f.get("__command").and_then(Value::as_str) == Some("image.canvasSize") {
let mut v = canvas_px_to_columns(px, res) as f32;
let lim = (300000.0 / res.max(1.0)).ceil() as f32;
let changed = crate::widgets::value_field_prec(ui, &mut v, -lim..=lim, "", 90.0, 0).accessible_name(tl!("Width")).changed();
if changed {
f.insert(key.into(), json!(canvas_columns_to_px(v as f64, res)));
}
let _ = id;
return changed;
}
if unit == COLUMNS.0 {
let cols = column_size(f);
let relative = f.get("relative").and_then(Value::as_bool).unwrap_or(false);
let mut v = px_to_columns(px, res, ppi, cols, relative) as f32;
let lim = px_to_columns(300000.0, res, ppi, cols, false).max(30000.0) as f32;
// No typed-unit conversion: Columns aren't a unit the field parser knows.
let changed = crate::widgets::value_field_prec(ui, &mut v, -lim..=lim, "", 90.0, COLUMN_DECIMALS).accessible_name(tl!("Width")).changed();
if changed {
f.insert(key.into(), json!(columns_to_px(v as f64, res, ppi, cols, relative)));
}
let _ = id;
return changed;
}
let mut v = to_unit(px, &unit, orig, res, ppi) as f32;
// The engine's 300000 px limit in this unit (never below 30000), so a large document's size
// isn't clamped when the field is drawn: egui clamps the shown value to the range, and the
@@ -200,12 +277,41 @@ fn dim_field(ui: &mut egui::Ui, f: &mut Map<String, Value>, key: &str, orig_key:
changed
}
fn unit_dropdown(ui: &mut egui::Ui, f: &mut Map<String, Value>, id: &str) {
let mut unit = f.get("__unit").and_then(Value::as_str).unwrap_or("px").to_string();
let opts: Vec<(String, &str)> = UNITS.iter().map(|(k, l)| (k.to_string(), *l)).collect();
if crate::widgets::dropdown(ui, id, tl!("Units"), &mut unit, &opts, 110.0) {
f.insert("__unit".into(), json!(unit));
/// The unit `key`'s field shows: the dialog's shared `__unit`, except a Width set to Columns
/// (`__unitW`), which height can't follow.
fn field_unit(f: &Map<String, Value>, key: &str) -> String {
let shared = f.get("__unit").and_then(Value::as_str).unwrap_or("px");
if key == "width" && f.get("__unitW").and_then(Value::as_str) == Some(COLUMNS.0) {
return COLUMNS.0.to_string();
}
if shared == COLUMNS.0 { "px".to_string() } else { shared.to_string() }
}
/// Unit menu beside the Width (`key` "width", which also offers Columns) or Height field.
fn unit_dropdown(ui: &mut egui::Ui, f: &mut Map<String, Value>, key: &str, id: &str) {
let mut unit = field_unit(f, key);
let mut opts: Vec<(String, &str)> = UNITS.iter().map(|(k, l)| (k.to_string(), *l)).collect();
if key == "width" {
opts.push((COLUMNS.0.to_string(), COLUMNS.1));
}
if crate::widgets::dropdown(ui, id, tl!("Units"), &mut unit, &opts, 110.0) {
set_unit(f, key, &unit);
}
}
/// Picks `unit` from `key`'s menu: Columns applies to Width alone; any other unit applies to both
/// fields, and on Width it leaves Columns.
fn set_unit(f: &mut Map<String, Value>, key: &str, unit: &str) {
if unit == COLUMNS.0 {
if key == "width" {
f.insert("__unitW".into(), json!(COLUMNS.0));
}
return;
}
if key == "width" {
f.remove("__unitW");
}
f.insert("__unit".into(), json!(unit));
}
fn image_size(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
@@ -228,18 +334,20 @@ fn image_size(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
let mut constrain = !resampling || f.get("__constrain").and_then(Value::as_bool).unwrap_or(true);
// With Resample off, a size in pixels or percent can't change (Photoshop disables those
// fields); a physical size (inches, cm…) changes the resolution instead.
let unit = f.get("__unit").and_then(Value::as_str).unwrap_or("px").to_string();
let editable = resampling || !matches!(unit.as_str(), "px" | "percent");
// Width in Columns is a physical size, so it stays editable while Height (in pixels) isn't.
let editable = |f: &Map<String, Value>, key: &str| resampling || !matches!(field_unit(f, key).as_str(), "px" | "percent");
let w_row = ui.horizontal(|ui| {
label(ui, tl!("Width:"), 90.0);
let editable = editable(f, "width");
let c = ui.add_enabled_ui(editable, |ui| dim_field(ui, f, "width", "__origW", "is-w")).inner;
unit_dropdown(ui, f, "is-unit-w");
unit_dropdown(ui, f, "width", "is-unit-w");
c
});
let h_row = ui.horizontal(|ui| {
label(ui, tl!("Height:"), 90.0);
let editable = editable(f, "height");
let c = ui.add_enabled_ui(editable, |ui| dim_field(ui, f, "height", "__origH", "is-h")).inner;
unit_dropdown(ui, f, "is-unit-h");
unit_dropdown(ui, f, "height", "is-unit-h");
c
});
let (w_changed, h_changed) = (w_row.inner, h_row.inner);
@@ -298,13 +406,19 @@ fn image_size(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
// Without resampling the pixel count is fixed: physical size follows resolution.
f.insert("width".into(), json!(ow));
f.insert("height".into(), json!(oh));
} else if !matches!(unit.as_str(), "px" | "percent") {
} else {
// Resampling in a physical unit keeps the print size: the pixels follow the
// resolution (Photoshop). In pixels or percent the pixel count stays.
// resolution (Photoshop). In pixels or percent the pixel count stays. Each field
// has its own unit (Width can be Columns, a physical unit, beside Height in
// pixels); with Constrain Proportions on, one physical side scales both.
let physical = |key: &str| !matches!(field_unit(f, key).as_str(), "px" | "percent");
let (pw, ph) = (physical("width"), physical("height"));
let old = f.get("resolution").and_then(Value::as_f64).unwrap_or(num(f, "__origRes"));
if old > 0.0 && old.is_finite() {
for key in ["width", "height"] {
f.insert(key.into(), json!((num(f, key) * r / old).max(1.0)));
for (key, scale) in [("width", pw || (constrain && ph)), ("height", ph || (constrain && pw))] {
if scale {
f.insert(key.into(), json!((num(f, key) * r / old).max(1.0)));
}
}
}
}
@@ -369,12 +483,12 @@ fn canvas_size(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
ui.horizontal(|ui| {
label(ui, tl!("Width:"), 90.0);
dim_field(ui, f, "width", "__origW", "cs-w");
unit_dropdown(ui, f, "cs-unit-w");
unit_dropdown(ui, f, "width", "cs-unit-w");
});
ui.horizontal(|ui| {
label(ui, tl!("Height:"), 90.0);
dim_field(ui, f, "height", "__origH", "cs-h");
unit_dropdown(ui, f, "cs-unit-h");
unit_dropdown(ui, f, "height", "cs-unit-h");
});
ui.horizontal(|ui| {
label(ui, "", 90.0);
+165
View File
@@ -32,6 +32,142 @@ fn points_and_picas_use_the_points_per_inch() {
assert_eq!(pt_per_in(&f), 72.27);
}
/// #6214: `n` columns are `n × width + (n − 1) × gutter` points. Photoshop's default Column
/// Size (180 pt, 12 pt gutter) at 300 ppi: 1 column is 2.5 in (750 px), 2 are 372 pt
/// (1550 px), 3 are 564 pt (2350 px).
#[test]
fn columns_convert_both_ways() {
let cols = (180.0, 12.0);
for (n, px) in [(1.0, 750.0), (2.0, 1550.0), (3.0, 2350.0), (0.5, 350.0)] {
assert!((columns_to_px(n, 300.0, 72.0, cols, false) - px).abs() < 1e-9, "{n} columns");
assert!((px_to_columns(px, 300.0, 72.0, cols, false) - n).abs() < 1e-9, "{px} px");
}
// Traditional points (72.27 pt/in) and other resolutions round-trip too.
for (ppi, res, cols) in [(72.27, 72.0, (100.0, 0.0)), (72.0, 150.0, (240.0, 18.0)), (72.0, 1.0, (1.0, 10_000.0))] {
for n in [1.0, 2.0, 4.0, 12.0, 0.25] {
// A relative delta under one column can be shorter than the gutter: it's zero.
for relative in [false, true].into_iter().filter(|r| !*r || n >= 1.0) {
let px = columns_to_px(n, res, ppi, cols, relative);
assert!((px_to_columns(px, res, ppi, cols, relative) - n).abs() < 1e-9, "{n} at {res} ppi, {cols:?}, relative {relative}");
}
}
}
// Canvas Size's relative deltas: zero stays zero, and shrinking mirrors growing.
assert_eq!(columns_to_px(0.0, 300.0, 72.0, cols, true), 0.0);
assert_eq!(px_to_columns(0.0, 300.0, 72.0, cols, true), 0.0);
assert_eq!(columns_to_px(-2.0, 300.0, 72.0, cols, true), -1550.0);
}
/// #6214: a zero, negative, NaN or missing Column Size falls back instead of dividing by zero.
#[test]
fn degenerate_column_size_does_not_panic() {
let mut f = Map::new();
assert_eq!(column_size(&f), (180.0, 12.0));
for (w, g, want) in [
(json!(0.0), json!(0.0), (180.0, 0.0)),
(json!(-5.0), json!(-3.0), (180.0, 0.0)),
(json!(f64::NAN), json!(f64::INFINITY), (180.0, 12.0)),
(json!("x"), json!(null), (180.0, 12.0)),
(json!(90.0), json!(6.0), (90.0, 6.0)),
] {
f.insert("__colWidth".into(), w);
f.insert("__colGutter".into(), g);
let cols = column_size(&f);
assert_eq!(cols, want);
for px in [0.0, 1.0, 750.0, -10.0, 1e12] {
assert!(px_to_columns(px, 300.0, 72.0, cols, false).is_finite());
assert!(px_to_columns(px, 0.0, 72.0, cols, true).is_finite());
}
}
}
/// #6214: the dialogs read Column Size from the preferences; Columns is a Width-only unit and
/// picking another unit on Width leaves it.
#[test]
fn columns_unit_is_width_only_and_follows_the_preference() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", json!({"width": 200, "height": 100})).unwrap();
app.session.execute("prefs.set", json!({"path": "unitsAndRulers.columnWidth", "value": 0})).unwrap();
app.session.execute("prefs.set", json!({"path": "unitsAndRulers.gutter", "value": 6})).unwrap();
for command in ["image.imageSize", "image.canvasSize"] {
let id = open(&mut app, command).unwrap();
let f = &mut app.ui.dialog_mut(id).unwrap().fields;
assert_eq!((f.get("__colWidth"), f.get("__colGutter")), (Some(&json!(0.0)), Some(&json!(6.0))), "{command}");
assert_eq!(column_size(f), (180.0, 6.0), "{command}: a zero width falls back");
f.insert("__unit".into(), json!("in"));
set_unit(f, "width", "col");
assert_eq!((field_unit(f, "width"), field_unit(f, "height")), ("col".to_string(), "in".to_string()), "{command}");
set_unit(f, "height", "col");
assert_eq!(field_unit(f, "height"), "in", "{command}: Height has no Columns");
set_unit(f, "height", "mm");
assert_eq!((field_unit(f, "width"), field_unit(f, "height")), ("col".to_string(), "mm".to_string()), "{command}");
set_unit(f, "width", "pt");
assert_eq!((field_unit(f, "width"), field_unit(f, "height")), ("pt".to_string(), "pt".to_string()), "{command}");
// A stray shared `col` (automation) shows Height in pixels rather than as columns.
f.insert("__unit".into(), json!("col"));
assert_eq!(field_unit(f, "height"), "px", "{command}");
app.ui.close_dialog(id);
}
}
/// #6214: 2 columns typed into Image Size's Width at 300 ppi resize to 1550 px wide (the
/// default 180 pt columns with 12 pt gutters), Height following the proportions; Canvas Size
/// counts whole inches, so its 2 columns are 600 px.
#[test]
fn typed_columns_resize_the_document() {
use egui_kittest::Harness;
for command in ["image.imageSize", "image.canvasSize"] {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", json!({"width": 1000, "height": 500, "resolution": 300})).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]);
let id = open(h.state_mut(), command).unwrap();
set_unit(&mut h.state_mut().ui.dialog_mut(id).unwrap().fields, "width", "col");
h.run_steps(3);
type_width(&mut h, "2");
let f = fields(&h);
let px = if command == "image.imageSize" { 1550.0 } else { 600.0 };
assert!((num(&f, "width") - px).abs() < 1e-6, "{command}: {f:?}");
crate::dialogs::confirm(h.state_mut(), id).unwrap();
let want = if command == "image.imageSize" { photocraft_doc::Size::new(1550, 775) } else { photocraft_doc::Size::new(600, 500) };
assert_eq!(h.state().session.active().unwrap().doc.size, want, "{command}");
}
}
/// Canvas Size's Columns as measured in the reference application: a column is one inch at the
/// document resolution whatever Column Size says, and only whole columns count. At 300 ppi with
/// Column Size 360 pt / 24 pt: a 1001 px canvas shows as 3 columns, typed 1.5 is 2 columns
/// (600 px), and Relative +1 column adds 300 px.
#[test]
fn canvas_size_columns_are_whole_inches() {
use egui_kittest::Harness;
assert_eq!(canvas_px_to_columns(1001.0, 300.0), 3.0);
assert_eq!(canvas_px_to_columns(1001.0, 72.0), 14.0);
assert_eq!(canvas_columns_to_px(1.5, 72.0), 144.0);
assert_eq!(canvas_columns_to_px(-1.0, 300.0), -300.0);
assert!(canvas_px_to_columns(1000.0, 0.0).is_finite());
for (relative, typed, want) in [(false, "1.5", 600), (true, "1", 1301)] {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", json!({"width": 1001, "height": 500, "resolution": 300})).unwrap();
app.session.execute("prefs.set", json!({"path": "unitsAndRulers.columnWidth", "value": 360})).unwrap();
app.session.execute("prefs.set", json!({"path": "unitsAndRulers.gutter", "value": 24})).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]);
let id = open(h.state_mut(), "image.canvasSize").unwrap();
let f = &mut h.state_mut().ui.dialog_mut(id).unwrap().fields;
set_unit(f, "width", "col");
if relative {
f.insert("relative".into(), json!(true));
f.insert("width".into(), json!(0.0));
f.insert("height".into(), json!(0.0));
}
h.run_steps(3);
type_width(&mut h, typed);
crate::dialogs::confirm(h.state_mut(), id).unwrap();
assert_eq!(h.state().session.active().unwrap().doc.size, photocraft_doc::Size::new(want, 500), "relative {relative}");
}
}
#[test]
fn dialog_maps_to_engine_command() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
@@ -309,6 +445,35 @@ fn resample_on_resolution_keeps_the_physical_size() {
}
}
/// #6979 with #6214's per-field units: Width in Columns (physical) beside Height in pixels.
/// A new Resolution with Resample on keeps the print width; linked, the height follows the
/// proportions, unlinked it keeps its pixels. 372 x 300 px at 72 ppi is 2 columns (the default
/// 180 pt columns, 12 pt gutter) wide; at 300 ppi 2 columns are 1550 px.
#[test]
fn resample_on_resolution_with_width_in_columns() {
for (constrain, size) in [(true, (1550.0, 1250.0)), (false, (1550.0, 300.0))] {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", json!({"width": 372, "height": 300, "resolution": 72})).unwrap();
let mut h = egui_kittest::Harness::builder()
.with_size(egui::vec2(1400.0, 900.0))
.build_ui_state(|ui, app: &mut PhotocraftApp| crate::dialogs::show(app, ui.ctx()), app);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
let id = open(h.state_mut(), "image.imageSize").unwrap();
let d = h.state_mut().ui.dialog_mut(id).unwrap();
d.fields.insert("resample".into(), json!("bicubic"));
d.fields.insert("__unit".into(), json!("px"));
d.fields.insert("__constrain".into(), json!(constrain));
set_unit(&mut d.fields, "width", "col");
h.run_steps(3);
type_into(&mut h, 2, "300");
let f = fields(&h);
assert_eq!((num(&f, "width"), num(&f, "height"), num(&f, "resolution")), (size.0, size.1, 300.0), "constrain {constrain}: {f:?}");
crate::dialogs::confirm(h.state_mut(), id).unwrap();
let doc = &h.state().session.active().unwrap().doc;
assert_eq!(doc.size, photocraft_doc::Size::new(size.0 as u32, size.1 as u32), "constrain {constrain}");
}
}
/// #4162: a document wider than 30000 units in a physical unit (40000 px at 72 ppi is 40000 pt)
/// kept its size in the fields and on OK; the fields clamped it to 30000 on open, so OK with
/// no edit resized (Image Size) or cropped (Canvas Size) it.
+5 -1
View File
@@ -34,6 +34,8 @@ pub(super) fn humanize(key: &str) -> String {
"darkTheme" => return "Dark Theme".into(),
"lightTheme" => return "Light Theme".into(),
"automaticallyCheckUpdates" => return "Automatically Check for Updates".into(),
// Units & Rulers › Column Size (in points), as translated for New Guide Layout.
"columnWidth" => return "Column width".into(),
_ => {}
}
// These controls appear only for WebP, so reuse the existing translated labels.
@@ -536,7 +538,9 @@ fn field_rows(ui: &mut egui::Ui, section: &str, obj: &mut Map<String, Value>, ke
}
} else {
let mut x = n.as_f64().unwrap_or(0.0);
ui.add(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.1).max_decimals(3).custom_parser(crate::widgets::parse_num))
// Column Size is stored in points (Image Size's Columns unit, #6214).
let suffix = if matches!(path.as_str(), "unitsAndRulers.columnWidth" | "unitsAndRulers.gutter") { " pt" } else { "" };
ui.add(egui::DragValue::new(&mut x).range(lo..=hi).speed(0.1).max_decimals(3).suffix(suffix).custom_parser(crate::widgets::parse_num))
.labelled_by(value_label.id);
obj.insert(k, json!(x));
}
+5 -2
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [Performance](#performance) | 3 | 22 | 54 | 88 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 50 not run, 4 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 11 | 14 | 2 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 190/258, round trip ≥ 258/258 |
| [Tools](#tools) | 36 | 9 | 0 | |
| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 10 of 199 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 8 of 199 (target 0) |
| [Type](#type) | 5 | 5 | 0 | |
| [Automation](#automation) | 5 | 4 | 2 | 664 of 664 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 30 of 30 crates; panic_hunt present |
@@ -212,9 +212,9 @@ Corpus floors (`crates/io/tests/it/corpus.rs`): the enforced minimums; the corpu
## Workspace and UI
**Settings that do nothing: 10 of 199** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
**Settings that do nothing: 8 of 199** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `performance.legacy_compositing`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `type_.text_engine`, `enhanced_controls.scrubby_slider_acceleration`.
Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `performance.legacy_compositing`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `type_.text_engine`, `enhanced_controls.scrubby_slider_acceleration`.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
+3 -2
View File
File diff suppressed because one or more lines are too long