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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
This commit is contained in:
Claude 2026-10-08 17:54:50 +00:00
parent ccdd9e45d1
commit 4eb9b565e0
No known key found for this signature in database
5 changed files with 32 additions and 20 deletions

View File

@ -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));

View File

@ -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 {

View File

@ -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<String>, Option<String>)| {
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}));

View File

@ -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<String> = 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}));

View File

@ -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