Run cargo fmt over the develop shortcuts and the star-range filter
Benchmarks / CPU and I/O (per commit) (push) Successful in 2m8s
Benchmarks / Frame budget (on demand) (push) Skipped
Traceability / Requirement traces (push) Successful in 33s
Build and test / Android (aarch64) (push) Successful in 14m21s
Build and test / android-image (push) Successful in 1s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / Desktop (Linux) (push) Successful in 47m57s
Build and test / windows-image (push) Successful in 1s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / Layer separation (push) Successful in 51s
Build and test / Windows (x86_64, cross) (push) Successful in 34m3s
Build and test / Publish the release (push) Successful in 1m3s

bddf325 and 00c028c went in unformatted, so the Desktop job's
`cargo fmt --check` step failed on master (run 1693) and the release job
that needs it was skipped. Whitespace only.
This commit is contained in:
2026-09-24 20:05:02 -04:00
parent 00c028c8c8
commit 031315bdb6
4 changed files with 232 additions and 145 deletions
File diff suppressed because one or more lines are too long
+31 -27
View File
@@ -1958,37 +1958,41 @@ fn wire_settings_screen(
{ {
let (weak, ctl, library, start) = (weak.clone(), ctl.clone(), library.clone(), start); let (weak, ctl, library, start) = (weak.clone(), ctl.clone(), library.clone(), start);
window.global::<ExportOptions>().on_browse_open_picker(move || { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
// Opens on the library root rather than on whatever the .on_browse_open_picker(move || {
// destination field happens to contain: a half-typed path let Some(w) = weak.upgrade() else { return };
// would list nothing and look like a broken picker. // Opens on the library root rather than on whatever the
ctl.browser.replace(Some(launch::FolderBrowser { // destination field happens to contain: a half-typed path
path: String::new(), // would list nothing and look like a broken picker.
entries: Vec::new(), ctl.browser.replace(Some(launch::FolderBrowser {
loading: true, path: String::new(),
})); entries: Vec::new(),
start(&ctl, &weak, &library, String::new()); loading: true,
settings_ui::render(&w, &ctl); }));
}); start(&ctl, &weak, &library, String::new());
settings_ui::render(&w, &ctl);
});
} }
{ {
let (weak, ctl, library, start) = (weak.clone(), ctl.clone(), library.clone(), start); let (weak, ctl, library, start) = (weak.clone(), ctl.clone(), library.clone(), start);
window.global::<ExportOptions>().on_browse_into(move |name| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
let path = { .on_browse_into(move |name| {
let mut browser = ctl.browser.borrow_mut(); let Some(w) = weak.upgrade() else { return };
let Some(b) = browser.as_mut() else { return }; let path = {
let path = b.child_path(&name); let mut browser = ctl.browser.borrow_mut();
b.path = path.clone(); let Some(b) = browser.as_mut() else { return };
b.entries.clear(); let path = b.child_path(&name);
b.loading = true; b.path = path.clone();
path b.entries.clear();
}; b.loading = true;
start(&ctl, &weak, &library, path); path
settings_ui::render(&w, &ctl); };
}); start(&ctl, &weak, &library, path);
settings_ui::render(&w, &ctl);
});
} }
{ {
+5 -1
View File
@@ -1032,7 +1032,11 @@ mod tests {
#[test] #[test]
fn a_hovered_photograph_outside_the_selection_is_judged_alone() { fn a_hovered_photograph_outside_the_selection_is_judged_alone() {
let (a, b, c) = (dr_types::ImageId(1), dr_types::ImageId(2), dr_types::ImageId(3)); let (a, b, c) = (
dr_types::ImageId(1),
dr_types::ImageId(2),
dr_types::ImageId(3),
);
assert_eq!(judged_images(vec![a, b], Some(c)), vec![c]); assert_eq!(judged_images(vec![a, b], Some(c)), vec![c]);
} }
+172 -93
View File
@@ -245,19 +245,33 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
// Choice rows are sent as labels plus the selected index rather than as a // Choice rows are sent as labels plus the selected index rather than as a
// model of structs: the page draws a row of chips from them and nothing // model of structs: the page draws a row of chips from them and nothing
// else, so a label and an index is the whole of what it needs. // else, so a label and an index is the whole of what it needs.
window.global::<ExportOptions>().set_format_labels(labels(ExportFormat::ALL.iter().map(|f| f.label()))); window
window.global::<ExportOptions>().set_format_selected(index_of(&ExportFormat::ALL, &s.export.format)); .global::<ExportOptions>()
.set_format_labels(labels(ExportFormat::ALL.iter().map(|f| f.label())));
window
.global::<ExportOptions>()
.set_format_selected(index_of(&ExportFormat::ALL, &s.export.format));
window.global::<ExportOptions>().set_quality(s.export.quality as i32); window
.global::<ExportOptions>()
.set_quality(s.export.quality as i32);
// Disabled rather than hidden for a lossless format: a control that // Disabled rather than hidden for a lossless format: a control that
// vanishes when PNG is picked reads as a bug, where a greyed one explains // vanishes when PNG is picked reads as a bug, where a greyed one explains
// itself. // itself.
window.global::<ExportOptions>().set_quality_enabled(s.export.format.is_lossy()); window
.global::<ExportOptions>()
.set_quality_enabled(s.export.format.is_lossy());
window.global::<ExportOptions>().set_colour_labels(labels(ColourSpace::ALL.iter().map(|c| c.label()))); window
window.global::<ExportOptions>().set_colour_selected(index_of(&ColourSpace::ALL, &s.export.colour_space)); .global::<ExportOptions>()
.set_colour_labels(labels(ColourSpace::ALL.iter().map(|c| c.label())));
window
.global::<ExportOptions>()
.set_colour_selected(index_of(&ColourSpace::ALL, &s.export.colour_space));
window.global::<ExportOptions>().set_sizing_labels(labels(SizingMode::CHOICES.iter().map(|m| m.label()))); window
.global::<ExportOptions>()
.set_sizing_labels(labels(SizingMode::CHOICES.iter().map(|m| m.label())));
// Compared by variant, not by equality: `LongEdge(900)` after the user // Compared by variant, not by equality: `LongEdge(900)` after the user
// typed their own number is still the "Long edge" choice, and equality // typed their own number is still the "Long edge" choice, and equality
// against `CHOICES` would light nothing. // against `CHOICES` would light nothing.
@@ -267,10 +281,14 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
.position(|m| m.same_mode(s.export.sizing)) .position(|m| m.same_mode(s.export.sizing))
.unwrap_or(0) as i32, .unwrap_or(0) as i32,
); );
window.global::<ExportOptions>().set_sizing_value(s.export.sizing.value().unwrap_or(0) as i32); window
.global::<ExportOptions>()
.set_sizing_value(s.export.sizing.value().unwrap_or(0) as i32);
// `Original` carries no number, so the field beside the chips has nothing // `Original` carries no number, so the field beside the chips has nothing
// to edit and is hidden rather than shown holding a meaningless zero. // to edit and is hidden rather than shown holding a meaningless zero.
window.global::<ExportOptions>().set_sizing_has_value(s.export.sizing.value().is_some()); window
.global::<ExportOptions>()
.set_sizing_has_value(s.export.sizing.value().is_some());
// TRACES: FR-EXP-3 // TRACES: FR-EXP-3
// The unit carries the axis for the box modes, so that two numeric fields // The unit carries the axis for the box modes, so that two numeric fields
// one above the other are not two anonymous numbers. It reads "3840 px // one above the other are not two anonymous numbers. It reads "3840 px
@@ -297,14 +315,20 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
} }
.into(), .into(),
); );
window.global::<ExportOptions>().set_sizing_height(s.export.sizing.height().unwrap_or(0) as i32); window
window.global::<ExportOptions>().set_sizing_has_height(s.export.sizing.height().is_some()); .global::<ExportOptions>()
.set_sizing_height(s.export.sizing.height().unwrap_or(0) as i32);
window
.global::<ExportOptions>()
.set_sizing_has_height(s.export.sizing.height().is_some());
// TRACES: FR-EXP-3 // TRACES: FR-EXP-3
// The panels, offered as buttons. Lit only on an exact match, and -1 — // The panels, offered as buttons. Lit only on an exact match, and -1 —
// no chip lit — where the user has typed their own numbers: a preset that // no chip lit — where the user has typed their own numbers: a preset that
// stayed selected while the fields said something else would be the page // stayed selected while the fields said something else would be the page
// claiming the export is 4K when it is not. // claiming the export is 4K when it is not.
window.global::<ExportOptions>().set_screen_labels(labels(ScreenSize::ALL.iter().map(|p| p.label))); window
.global::<ExportOptions>()
.set_screen_labels(labels(ScreenSize::ALL.iter().map(|p| p.label)));
window.global::<ExportOptions>().set_screen_selected( window.global::<ExportOptions>().set_screen_selected(
ScreenSize::ALL ScreenSize::ALL
.iter() .iter()
@@ -314,37 +338,63 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
}) })
.map_or(-1, |i| i as i32), .map_or(-1, |i| i as i32),
); );
window.global::<ExportOptions>().set_allow_upscaling(s.export.allow_upscaling); window
.global::<ExportOptions>()
.set_allow_upscaling(s.export.allow_upscaling);
window.global::<ExportOptions>().set_sharpening_labels(labels(OutputSharpening::ALL.iter().map(|x| x.label()))); window
window.global::<ExportOptions>().set_sharpening_selected(index_of(&OutputSharpening::ALL, &s.export.sharpening)); .global::<ExportOptions>()
.set_sharpening_labels(labels(OutputSharpening::ALL.iter().map(|x| x.label())));
window
.global::<ExportOptions>()
.set_sharpening_selected(index_of(&OutputSharpening::ALL, &s.export.sharpening));
window.global::<ExportOptions>().set_filename_template(s.export.filename_template.clone().into()); window
window.global::<ExportOptions>().set_collision_labels(labels(CollisionPolicy::ALL.iter().map(|c| c.label()))); .global::<ExportOptions>()
window.global::<ExportOptions>().set_collision_selected(index_of(&CollisionPolicy::ALL, &s.export.collision)); .set_filename_template(s.export.filename_template.clone().into());
window.global::<ExportOptions>().set_strip_location(s.export.strip_location); window
.global::<ExportOptions>()
.set_collision_labels(labels(CollisionPolicy::ALL.iter().map(|c| c.label())));
window
.global::<ExportOptions>()
.set_collision_selected(index_of(&CollisionPolicy::ALL, &s.export.collision));
window
.global::<ExportOptions>()
.set_strip_location(s.export.strip_location);
// `available()`, not `ALL`: Android cannot write to a device folder, and // `available()`, not `ALL`: Android cannot write to a device folder, and
// offering a target that fails at the last step is what made export look // offering a target that fails at the last step is what made export look
// broken there rather than absent. // broken there rather than absent.
let targets = ExportTarget::available(); let targets = ExportTarget::available();
window.global::<ExportOptions>().set_target_labels(labels(targets.iter().map(|t| t.label()))); window
window.global::<ExportOptions>().set_target_selected(index_of(targets, &s.export.target)); .global::<ExportOptions>()
window.global::<ExportOptions>().set_destination(s.export.active_destination().into()); .set_target_labels(labels(targets.iter().map(|t| t.label())));
window
.global::<ExportOptions>()
.set_target_selected(index_of(targets, &s.export.target));
window
.global::<ExportOptions>()
.set_destination(s.export.active_destination().into());
// Derived from the target itself, never from its position in a list whose // Derived from the target itself, never from its position in a list whose
// length differs by platform. // length differs by platform.
window.global::<ExportOptions>().set_browse_available(s.export.target.is_remote()); window
.global::<ExportOptions>()
.set_browse_available(s.export.target.is_remote());
// The field means different things either side of the choice, and a // The field means different things either side of the choice, and a
// placeholder saying which is cheaper than a paragraph under it. // placeholder saying which is cheaper than a paragraph under it.
// The placeholder names what an empty field *means*, which differs by // The placeholder names what an empty field *means*, which differs by
// target: on a filesystem it is a question, on the server it is the // target: on a filesystem it is a question, on the server it is the
// library root. // library root.
window.global::<ExportOptions>().set_destination_hint(s.export.destination_label().into()); window
.global::<ExportOptions>()
.set_destination_hint(s.export.destination_label().into());
// --- the remote folder picker -------------------------------------- // --- the remote folder picker --------------------------------------
{ {
let browser = controller.browser.borrow(); let browser = controller.browser.borrow();
window.global::<ExportOptions>().set_browse_open(browser.is_some()); window
.global::<ExportOptions>()
.set_browse_open(browser.is_some());
match browser.as_ref() { match browser.as_ref() {
Some(b) => { Some(b) => {
// The root is shown as a word rather than as an empty string, // The root is shown as a word rather than as an empty string,
@@ -357,19 +407,28 @@ pub fn render(window: &AppWindow, controller: &SettingsController) {
} }
.into(), .into(),
); );
window.global::<ExportOptions>().set_browse_loading(b.loading); window
window.global::<ExportOptions>().set_browse_at_root(b.parent_path().is_none()); .global::<ExportOptions>()
window.global::<ExportOptions>().set_browse_entries(slint::ModelRc::new(slint::VecModel::from( .set_browse_loading(b.loading);
b.entries window
.iter() .global::<ExportOptions>()
.map(|e| slint::SharedString::from(e.as_str())) .set_browse_at_root(b.parent_path().is_none());
.collect::<Vec<_>>(), window
))); .global::<ExportOptions>()
.set_browse_entries(slint::ModelRc::new(slint::VecModel::from(
b.entries
.iter()
.map(|e| slint::SharedString::from(e.as_str()))
.collect::<Vec<_>>(),
)));
} }
None => { None => {
window.global::<ExportOptions>().set_browse_entries(slint::ModelRc::new(slint::VecModel::from( window
Vec::<slint::SharedString>::new(), .global::<ExportOptions>()
))); .set_browse_entries(slint::ModelRc::new(slint::VecModel::from(Vec::<
slint::SharedString,
>::new(
))));
window.global::<ExportOptions>().set_browse_loading(false); window.global::<ExportOptions>().set_browse_loading(false);
} }
} }
@@ -647,13 +706,15 @@ fn wire_export(window: &AppWindow, controller: &Rc<SettingsController>) {
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_quality_changed(move |q| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
// Cast before clamping: a negative from the control would wrap to a .on_quality_changed(move |q| {
// large `u8` and land on 100 instead of the floor. let Some(w) = weak.upgrade() else { return };
ctl.edit(|s| s.export.quality = q.clamp(1, 100) as u8); // Cast before clamping: a negative from the control would wrap to a
render(&w, &ctl); // large `u8` and land on 100 instead of the floor.
}); ctl.edit(|s| s.export.quality = q.clamp(1, 100) as u8);
render(&w, &ctl);
});
} }
{ {
@@ -715,27 +776,33 @@ fn wire_export(window: &AppWindow, controller: &Rc<SettingsController>) {
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_sizing_value_changed(move |text| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
match text.trim().parse::<u32>() { .on_sizing_value_changed(move |text| {
Ok(v) if v > 0 => ctl.edit(|s| s.export.sizing = s.export.sizing.with_value(v)), let Some(w) = weak.upgrade() else { return };
_ => log::debug!("ignoring an unusable export size: {text:?}"), match text.trim().parse::<u32>() {
} Ok(v) if v > 0 => ctl.edit(|s| s.export.sizing = s.export.sizing.with_value(v)),
render(&w, &ctl); _ => log::debug!("ignoring an unusable export size: {text:?}"),
}); }
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_sizing_height_changed(move |text| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
match text.trim().parse::<u32>() { .on_sizing_height_changed(move |text| {
Ok(v) if v > 0 => ctl.edit(|s| s.export.sizing = s.export.sizing.with_height(v)), let Some(w) = weak.upgrade() else { return };
_ => log::debug!("ignoring an unusable export height: {text:?}"), match text.trim().parse::<u32>() {
} Ok(v) if v > 0 => {
render(&w, &ctl); ctl.edit(|s| s.export.sizing = s.export.sizing.with_height(v))
}); }
_ => log::debug!("ignoring an unusable export height: {text:?}"),
}
render(&w, &ctl);
});
} }
{ {
@@ -761,65 +828,77 @@ fn wire_export(window: &AppWindow, controller: &Rc<SettingsController>) {
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_upscaling_toggled(move |on| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
ctl.edit(|s| s.export.allow_upscaling = on); .on_upscaling_toggled(move |on| {
render(&w, &ctl); let Some(w) = weak.upgrade() else { return };
}); ctl.edit(|s| s.export.allow_upscaling = on);
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_sharpening_picked(move |i| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
if let Some(x) = OutputSharpening::ALL.get(i as usize).copied() { .on_sharpening_picked(move |i| {
ctl.edit(|s| s.export.sharpening = x); let Some(w) = weak.upgrade() else { return };
} if let Some(x) = OutputSharpening::ALL.get(i as usize).copied() {
render(&w, &ctl); ctl.edit(|s| s.export.sharpening = x);
}); }
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_template_changed(move |text| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
ctl.edit(|s| s.export.filename_template = text.to_string()); .on_template_changed(move |text| {
render(&w, &ctl); let Some(w) = weak.upgrade() else { return };
}); ctl.edit(|s| s.export.filename_template = text.to_string());
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_collision_picked(move |i| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
if let Some(c) = CollisionPolicy::ALL.get(i as usize).copied() { .on_collision_picked(move |i| {
ctl.edit(|s| s.export.collision = c); let Some(w) = weak.upgrade() else { return };
} if let Some(c) = CollisionPolicy::ALL.get(i as usize).copied() {
render(&w, &ctl); ctl.edit(|s| s.export.collision = c);
}); }
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_strip_location_toggled(move |on| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
ctl.edit(|s| s.export.strip_location = on); .on_strip_location_toggled(move |on| {
render(&w, &ctl); let Some(w) = weak.upgrade() else { return };
}); ctl.edit(|s| s.export.strip_location = on);
render(&w, &ctl);
});
} }
{ {
let weak = window.as_weak(); let weak = window.as_weak();
let ctl = controller.clone(); let ctl = controller.clone();
window.global::<ExportOptions>().on_destination_changed(move |text| { window
let Some(w) = weak.upgrade() else { return }; .global::<ExportOptions>()
ctl.edit(|s| s.export.set_active_destination(text.to_string())); .on_destination_changed(move |text| {
render(&w, &ctl); let Some(w) = weak.upgrade() else { return };
}); ctl.edit(|s| s.export.set_active_destination(text.to_string()));
render(&w, &ctl);
});
} }
{ {