From 4eb9b565e01363a919bd52ca25cd48943d04d5c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 17:54:50 +0000 Subject: [PATCH] Profiles: imported LUT names shown as named, own ids for their folders (#333) #333 passed imported LUT names and folder names through tr(), so a LUT named like a message ("Vivid") was shown translated; they are now shown as the file names them (builtin_label / new profile_label, as for user presets). A LUT folder named like a built-in group ("Film") shared that group's menu widget id and the browser section's open state; both are keyed apart now. Restores the "Amount 0 = the photo as it was" assertion #333 replaced, and adds a What's new line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 --- crates/engine/src/tests_import.rs | 1 + crates/ui-egui/src/i18n.rs | 6 ++++++ crates/ui-egui/src/panels/edit.rs | 15 +++++++------- crates/ui-egui/src/panels/profiles.rs | 28 ++++++++++++++------------- docs/whats-new.md | 2 ++ 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/tests_import.rs b/crates/engine/src/tests_import.rs index ba657701..ce36c447 100644 --- a/crates/engine/src/tests_import.rs +++ b/crates/engine/src/tests_import.rs @@ -544,6 +544,7 @@ fn cube_luts_become_profiles() { mean(&warm, 2) ); s.execute("develop.profile", &json!({"id": id, "amount": 0})).unwrap(); + assert_eq!(s.render_now(photo, 48, 32).unwrap().image.data, base.data, "Amount 0 = the photo as it was"); assert_eq!(s.profile_info(&id), Some(("Warm Test", "Film Looks"))); s.execute("profile.favorite", &json!({"id": id})).unwrap(); assert!(s.profile_favorites.contains(&id)); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 7ed26d56..b95cb630 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -191,6 +191,12 @@ pub fn builtin_label(source: &str, builtin: bool) -> &str { if builtin { tr(source) } else { source } } +/// A profile's name for display: a built-in profile's is translated, an imported LUT's is shown as +/// its file named it (it may happen to match a message, like "Vivid"). +pub fn profile_label<'a>(id: &str, name: &'a str) -> &'a str { + builtin_label(name, lightcraft_engine::presets::profile(id).is_some()) +} + /// An Activity (history) step for display. The step's stored label stays English; generated steps /// are translated around the names they carry, and a user preset's name is kept verbatim. pub fn history_label(source: &str, presets: &[lightcraft_develop::Preset]) -> String { diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index 2d79f5f5..163ae6fb 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -140,7 +140,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { ui.horizontal(|ui| { ui.label(egui::RichText::new(crate::i18n::tr("Profile")).font(t.font(13.0)).color(t.text_dim)); let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color"); - let r = crate::widgets::dropdown(ui, "profile", crate::i18n::tr(name), t.font(15.0), t.text_label); + let r = crate::widgets::dropdown(ui, "profile", crate::i18n::profile_label(&d.profile.id, name), t.font(15.0), t.text_label); egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if crate::widgets::icon_button(ui, "profileBrowser", Icon::ProfileGrid, vec2(28.0, 28.0), false, true, "Browse Profiles").clicked() { @@ -356,7 +356,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) id: &str, name: &str, pick: &mut (Option, Option)| { - let r = ui.selectable_label(id == cur, crate::i18n::tr(name)); + let r = ui.selectable_label(id == cur, crate::i18n::profile_label(id, name)); register(ui.ctx(), format!("profileMenu:{key}:{id}"), r.rect); if r.clicked() { pick.0 = Some(id.to_string()); @@ -393,21 +393,22 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) lut_groups.sort_unstable(); lut_groups.dedup(); for g in &lut_groups { - let r = ui.menu_button(crate::i18n::tr(g), |ui| { + // a folder's name as it is, and its own widget id even when it matches a built-in group + let r = ui.menu_button(g.as_str(), |ui| { ui.set_min_width(170.0); for p in app.session.lut_profiles.iter().filter(|p| &p.group == g) { item(ui, "group", &p.id, &p.name, &mut pick); } }); - register(ui.ctx(), format!("profileMenu:groupMenu:{g}"), r.response.rect); + register(ui.ctx(), format!("profileMenu:lutGroupMenu:{g}"), r.response.rect); } ui.separator(); if let Some((name, _)) = app.session.profile_info(cur) { let fav = app.session.profile_favorites.iter().any(|f| f == cur); let label = if fav { - crate::i18n::tr_format!("Remove “{}” from Favorites", crate::i18n::tr(name)) + crate::i18n::tr_format!("Remove “{}” from Favorites", crate::i18n::profile_label(cur, name)) } else { - crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::tr(name)) + crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::profile_label(cur, name)) }; let r = ui.button(label); register(ui.ctx(), "profileMenu:toggleFavorite", r.rect); @@ -428,7 +429,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) let mut s = d.clone(); s.profile.id = hovered_id.clone(); s.profile.amount = 100.0; - app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(name)), settings: s }); + app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::profile_label(hovered_id, name)), settings: s }); } if let Some(id) = pick.0 { let _ = app.run("develop.profile", json!({"id": id})); diff --git a/crates/ui-egui/src/panels/profiles.rs b/crates/ui-egui/src/panels/profiles.rs index 1ae321f5..03a62451 100644 --- a/crates/ui-egui/src/panels/profiles.rs +++ b/crates/ui-egui/src/panels/profiles.rs @@ -35,13 +35,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let (cr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover()); let name = app.session.profile_info(&d.profile.id).map(|(name, _)| name).unwrap_or("Color"); ui.painter().text(pos2(cr.left() + 24.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Profile"), t.font(13.0), t.text_dim); - ui.painter().text(pos2(cr.left() + 84.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::tr(name), t.font(14.0), t.text_label); + ui.painter().text(pos2(cr.left() + 84.0, cr.center().y), Align2::LEFT_CENTER, crate::i18n::profile_label(&d.profile.id, name), t.font(14.0), t.text_label); if d.profile.id != "lc.color" { super::edit::control(app, ui, &d, "profile.amount", true); ui.add_space(6.0); } divider(ui); - let mut sections: Vec<(&str, &str, Vec<(String, String, String)>)> = Vec::new(); + // (title, cell tag, imported LUT folder, profiles) + let mut sections: Vec<(&str, &str, bool, Vec<(String, String, String)>)> = Vec::new(); let favs: Vec<(String, String, String)> = app .session .profile_favorites @@ -49,10 +50,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { .filter_map(|f| app.session.profile_info(f).map(|(name, group)| (f.clone(), name.to_string(), group.to_string()))) .collect(); if !favs.is_empty() { - sections.push(("Favorites", "fav", favs)); + sections.push(("Favorites", "fav", false, favs)); } for g in profile_groups() { - sections.push((g, "", PROFILES.iter().filter(|p| p.group == g).map(|p| (p.id.to_string(), p.name.to_string(), p.group.to_string())).collect())); + sections.push((g, "", false, PROFILES.iter().filter(|p| p.group == g).map(|p| (p.id.to_string(), p.name.to_string(), p.group.to_string())).collect())); } // imported LUT profile groups let mut lut_groups: Vec = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect(); @@ -66,20 +67,21 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { .filter(|p| &p.group == g) .map(|p| (p.id.clone(), p.name.clone(), p.group.clone())) .collect(); - sections.push((g.as_str(), "", profiles)); + sections.push((g.as_str(), "", true, profiles)); } - for (title, tag, list) in sections { - let open_id = egui::Id::new(("profile-group", title)); + for (title, tag, imported, list) in sections { + // an imported folder named like a built-in group still opens and closes on its own + let open_id = egui::Id::new(("profile-group", imported, title)); let open: bool = ui.data(|m| m.get_temp(open_id)).unwrap_or(true); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 32.0), Sense::click()); - register(ui.ctx(), format!("profileGroup:{title}"), r); + register(ui.ctx(), if imported { format!("profileGroup:lut:{title}") } else { format!("profileGroup:{title}") }, r); paint( ui.painter(), Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(12.0, 12.0)), if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_label, ); - ui.painter().text(pos2(r.left() + 36.0, r.center().y), Align2::LEFT_CENTER, crate::i18n::tr(title), t.semibold(13.0), t.text_label); + ui.painter().text(pos2(r.left() + 36.0, r.center().y), Align2::LEFT_CENTER, crate::i18n::builtin_label(title, !imported), t.semibold(13.0), t.text_label); ui.painter().text(pos2(r.right() - 22.0, r.center().y), Align2::RIGHT_CENTER, list.len().to_string(), t.font(12.0), t.text_dim); if resp.clicked() { ui.data_mut(|m| m.insert_temp(open_id, !open)); @@ -109,7 +111,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { && let Some((p_name, _)) = app.session.profile_info(&saved_profile_id) { let s = with_profile_id(&d, &saved_profile_id); - app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p_name)), settings: s }); + app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::profile_label(p_id, p_name)), settings: s }); } } else { ui.data_mut(|m| m.remove::<(PhotoId, String)>(last_hover_id)); @@ -158,7 +160,7 @@ fn cell_ui( painter.rect_stroke(thumb, 3.0, Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside); } let name_color = if applied { t.text } else { t.text_label }; - painter.text(pos2(cell.left() + 2.0, thumb.bottom() + 12.0), Align2::LEFT_CENTER, crate::i18n::tr(p_name), t.font(12.0), name_color); + painter.text(pos2(cell.left() + 2.0, thumb.bottom() + 12.0), Align2::LEFT_CENTER, crate::i18n::profile_label(p_id, p_name), t.font(12.0), name_color); // favourite star (always shown when set, on hover otherwise) let fav = app.session.profile_favorites.iter().any(|f| f == p_id); let star = Rect::from_center_size(pos2(thumb.right() - 13.0, thumb.top() + 13.0), vec2(20.0, 20.0)); @@ -169,10 +171,10 @@ fn cell_ui( paint(painter, star.shrink(4.0), if fav { Icon::StarFilled } else { Icon::Star }, if fav || over_star { t.star } else { Color32::WHITE }); } if resp.hovered() && !applied && !over_star { - app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p_name)), settings: s.clone() }); + app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::profile_label(p_id, p_name)), settings: s.clone() }); ui.data_mut(|m| m.insert_temp(egui::Id::new("last-profile-hover"), (id, p_id.to_string()))); } - let resp = resp.on_hover_text(format!("{} ({})", crate::i18n::tr(p_name), crate::i18n::tr(p_group))); + let resp = resp.on_hover_text(format!("{} ({})", crate::i18n::profile_label(p_id, p_name), crate::i18n::builtin_label(p_group, lightcraft_engine::presets::profile(p_id).is_some()))); if resp.clicked() { if resp.interact_pointer_pos().is_some_and(|q| star.contains(q)) { let _ = app.run("profile.favorite", json!({"id": p_id})); diff --git a/docs/whats-new.md b/docs/whats-new.md index 745a8188..5ea9eea8 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -26,6 +26,8 @@ - Luminar looks: `.lmp` files and `.mplumpack` collections import as presets (grouped by collection); the sliders with a counterpart here come along, the rest is listed. - 23 new built-in presets: Portrait, Landscape, Urban, Food, Seasons, Vintage and B&W toners. +- Imported `.cube` LUT profiles appear in the Profile menu and the profile browser, grouped by their folder, and stay + favourites across restarts (issue #328). ### Editing - The Tint slider works the right way round (issues #188, #321): left adds green, right adds magenta, as its track