This commit is contained in:
Claude committed 2026-10-08 17:52:46 +00:00
commit ccdd9e45d1
6 files changed
+123 -64

No files matched your search

+14 -14
View File
@@ -317,20 +317,6 @@ impl Session {
journal.snapshot(&self.catalog)?;
}
let mut settings = SettingsLoad::default();
// presets
if let Some(f) = settings.read::<PresetsFile>(files.as_mut(), "presets.json") {
for p in &mut self.presets {
p.favorite = p.builtin && f.favorites.contains(&p.id) || (!p.builtin && p.favorite);
}
for u in f.user {
if !self.presets.iter().any(|p| p.id == u.id) {
self.presets.push(u);
}
}
let known = |id: &String| crate::presets::profile(id).is_some();
self.profile_favorites = f.profile_favorites.into_iter().filter(known).collect();
self.profile_recent = f.profile_recent.into_iter().filter(known).take(crate::presets::RECENT_PROFILES).collect();
}
// preferences
let prefs = settings.read::<PrefsFile>(files.as_mut(), "prefs.json").unwrap_or_default();
self.xmp = prefs.xmp;
@@ -352,6 +338,20 @@ impl Session {
if let Some(d) = &self.smart_previews_dir {
self.media.smart_dir = Some(d.clone());
}
// presets
if let Some(f) = settings.read::<PresetsFile>(files.as_mut(), "presets.json") {
for p in &mut self.presets {
p.favorite = p.builtin && f.favorites.contains(&p.id) || (!p.builtin && p.favorite);
}
for u in f.user {
if !self.presets.iter().any(|p| p.id == u.id) {
self.presets.push(u);
}
}
let known = |id: &String| crate::presets::profile(id).is_some() || self.lut_profiles.iter().any(|p| &p.id == id);
self.profile_favorites = f.profile_favorites.into_iter().filter(known).collect();
self.profile_recent = f.profile_recent.into_iter().filter(known).take(crate::presets::RECENT_PROFILES).collect();
}
// view state
if let Some(v) = settings.read::<ViewFile>(files.as_mut(), "view.json") {
self.source = v.source;
+11
View File
@@ -446,6 +446,17 @@ pub fn profile(id: &str) -> Option<&'static ProfileInfo> {
pub const RECENT_PROFILES: usize = 5;
impl Session {
/// A profile by id, looking up built-in profiles first and imported LUT profiles second.
pub fn profile_info(&self, id: &str) -> Option<(&str, &str)> {
if let Some(p) = profile(id) {
return Some((p.name, p.group));
}
self.lut_profiles
.iter()
.find(|p| p.id == id)
.map(|p| (p.name.as_str(), p.group.as_str()))
}
/// Remember `id` as the most recently applied profile.
pub fn note_profile_used(&mut self, id: &str) {
self.profile_recent.retain(|p| p != id);
+5 -1
View File
@@ -544,13 +544,17 @@ 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));
// the library keeps it
drop(s);
lightcraft_pipeline::lut::unregister(&id);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
assert!(s.lut_profiles.iter().any(|p| p.id == id));
assert!(s.profile_favorites.contains(&id), "imported LUT favorite survives reopen");
assert_eq!(s.profile_info(&id), Some(("Warm Test", "Film Looks")));
assert!(lightcraft_pipeline::lut::get(&id).is_some(), "registered again on open");
s.execute("profile.deleteImported", &json!({"id": id})).unwrap();
assert!(lightcraft_pipeline::lut::get(&id).is_none());
+39 -24
View File
@@ -139,7 +139,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 14, bottom: 14 }).show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(crate::i18n::tr("Profile")).font(t.font(13.0)).color(t.text_dim));
let name = lightcraft_engine::presets::profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
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);
egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
@@ -345,36 +345,37 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
/// The profile dropdown: Favorites, Recent, one submenu per group, then favourite toggle and
/// Browse….
fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
use lightcraft_engine::presets::{PROFILES, profile, profile_groups};
use lightcraft_engine::presets::{PROFILES, profile_groups};
let t = Tokens::get(ui.ctx());
ui.set_min_width(200.0);
let cur = d.profile.id.as_str();
// (clicked, hovered)
let mut pick: (Option<&'static str>, Option<&'static str>) = (None, None);
let item = |ui: &mut egui::Ui,
key: &str,
p: &'static lightcraft_engine::presets::ProfileInfo,
pick: &mut (Option<&'static str>, Option<&'static str>)| {
let r = ui.selectable_label(p.id == cur, crate::i18n::tr(p.name));
register(ui.ctx(), format!("profileMenu:{key}:{}", p.id), r.rect);
let mut pick: (Option<String>, Option<String>) = (None, None);
let mut item = |ui: &mut egui::Ui,
key: &str,
id: &str,
name: &str,
pick: &mut (Option<String>, Option<String>)| {
let r = ui.selectable_label(id == cur, crate::i18n::tr(name));
register(ui.ctx(), format!("profileMenu:{key}:{id}"), r.rect);
if r.clicked() {
pick.0 = Some(p.id);
pick.0 = Some(id.to_string());
}
if r.hovered() {
pick.1 = Some(p.id);
pick.1 = Some(id.to_string());
}
};
let heading = |ui: &mut egui::Ui, s: &str| {
ui.label(egui::RichText::new(crate::i18n::tr(s)).font(t.semibold(11.5)).color(t.text_dim));
};
for (title, key, ids) in [("Favorites", "fav", app.session.profile_favorites.clone()), ("Recent", "recent", app.session.profile_recent.clone())] {
let list: Vec<_> = ids.iter().filter_map(|id| profile(id)).collect();
let list: Vec<_> = ids.iter().filter_map(|id| app.session.profile_info(id).map(|(name, _)| (id.as_str(), name))).collect();
if list.is_empty() {
continue;
}
heading(ui, title);
for p in list {
item(ui, key, p, &mut pick);
for (id, name) in list {
item(ui, key, id, name, &mut pick);
}
ui.separator();
}
@@ -382,23 +383,36 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
let r = ui.menu_button(crate::i18n::tr(g), |ui| {
ui.set_min_width(170.0);
for p in PROFILES.iter().filter(|p| p.group == g) {
item(ui, "group", p, &mut pick);
item(ui, "group", p.id, p.name, &mut pick);
}
});
register(ui.ctx(), format!("profileMenu:groupMenu:{g}"), r.response.rect);
}
// imported LUT profile groups
let mut lut_groups: Vec<String> = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect();
lut_groups.sort_unstable();
lut_groups.dedup();
for g in &lut_groups {
let r = ui.menu_button(crate::i18n::tr(g), |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);
}
ui.separator();
if let Some(p) = profile(cur) {
let fav = app.session.profile_favorites.iter().any(|f| f == p.id);
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(p.name))
crate::i18n::tr_format!("Remove “{}” from Favorites", crate::i18n::tr(name))
} else {
crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::tr(p.name))
crate::i18n::tr_format!("Add “{}” to Favorites", crate::i18n::tr(name))
};
let r = ui.button(label);
register(ui.ctx(), "profileMenu:toggleFavorite", r.rect);
if r.clicked() {
let _ = app.run("profile.favorite", json!({"id": p.id}));
let _ = app.run("profile.favorite", json!({"id": cur}));
}
}
let r = ui.button(crate::i18n::tr("Browse…"));
@@ -407,13 +421,14 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
let _ = app.run("panel.profiles", json!({}));
}
// resting on a profile previews it in the loupe
if let Some(p) = pick.1.and_then(profile)
&& p.id != cur
if let Some(hovered_id) = &pick.1
&& hovered_id != cur
&& let Some((name, _)) = app.session.profile_info(hovered_id)
{
let mut s = d.clone();
s.profile.id = p.id.to_string();
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(p.name)), settings: s });
app.hover_preview = Some(crate::HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(name)), settings: s });
}
if let Some(id) = pick.0 {
let _ = app.run("develop.profile", json!({"id": id}));
+53 -24
View File
@@ -5,7 +5,7 @@
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
use lightcraft_catalog::PhotoId;
use lightcraft_develop::DevelopSettings;
use lightcraft_engine::presets::{PROFILES, ProfileInfo, profile, profile_groups};
use lightcraft_engine::presets::{PROFILES, profile_groups};
use serde_json::json;
use super::presets::cover_uv;
@@ -33,7 +33,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
divider(ui);
// the applied profile and its amount
let (cr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
let name = profile(&d.profile.id).map(|p| p.name).unwrap_or("Color");
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);
if d.profile.id != "lc.color" {
@@ -41,13 +41,32 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
ui.add_space(6.0);
}
divider(ui);
let mut sections: Vec<(&str, &str, Vec<&'static ProfileInfo>)> = Vec::new();
let favs: Vec<&'static ProfileInfo> = app.session.profile_favorites.iter().filter_map(|f| profile(f)).collect();
let mut sections: Vec<(&str, &str, Vec<(String, String, String)>)> = Vec::new();
let favs: Vec<(String, String, String)> = app
.session
.profile_favorites
.iter()
.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));
}
for g in profile_groups() {
sections.push((g, "", PROFILES.iter().filter(|p| p.group == g).collect()));
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()));
}
// imported LUT profile groups
let mut lut_groups: Vec<String> = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect();
lut_groups.sort_unstable();
lut_groups.dedup();
for g in &lut_groups {
let profiles = app
.session
.lut_profiles
.iter()
.filter(|p| &p.group == g)
.map(|p| (p.id.clone(), p.name.clone(), p.group.clone()))
.collect();
sections.push((g.as_str(), "", profiles));
}
for (title, tag, list) in sections {
let open_id = egui::Id::new(("profile-group", title));
@@ -87,34 +106,44 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
if app.hover_preview.is_none()
&& let Some((saved_id, saved_profile_id)) = ui.data(|m| m.get_temp::<(PhotoId, String)>(last_hover_id))
&& saved_id == id
&& let Some(p) = profile(&saved_profile_id)
&& let Some((p_name, _)) = app.session.profile_info(&saved_profile_id)
{
let s = with_profile(&d, p);
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(p.name)), settings: s });
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 });
}
} else {
ui.data_mut(|m| m.remove::<(PhotoId, String)>(last_hover_id));
}
}
/// `d` with profile `p` (at 100 % unless it is the applied one).
fn with_profile(d: &DevelopSettings, p: &ProfileInfo) -> DevelopSettings {
/// `d` with profile `profile_id` (at 100 % unless it is the applied one).
fn with_profile_id(d: &DevelopSettings, profile_id: &str) -> DevelopSettings {
let mut s = d.clone();
if s.profile.id != p.id {
s.profile.id = p.id.to_string();
if s.profile.id != profile_id {
s.profile.id = profile_id.to_string();
s.profile.amount = 100.0;
}
s
}
#[allow(clippy::too_many_arguments)]
fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: PhotoId, tag: &str, p: &'static ProfileInfo, cell: Rect, th: f32) {
fn cell_ui(
app: &mut LightcraftApp,
ui: &mut egui::Ui,
d: &DevelopSettings,
id: PhotoId,
tag: &str,
p: &(String, String, String),
cell: Rect,
th: f32,
) {
let (p_id, p_name, p_group) = (p.0.as_str(), p.1.as_str(), p.2.as_str());
let t = Tokens::get(ui.ctx());
let key = if tag.is_empty() { format!("profileCell:{}", p.id) } else { format!("profileCell:{tag}:{}", p.id) };
let key = if tag.is_empty() { format!("profileCell:{p_id}") } else { format!("profileCell:{tag}:{p_id}") };
let resp = ui.interact(cell, egui::Id::new(&key), Sense::click());
register(ui.ctx(), key, cell);
let thumb = Rect::from_min_size(cell.min, vec2(cell.width(), th));
let s = with_profile(d, p);
let s = with_profile_id(d, p_id);
let painter = ui.painter();
painter.rect_filled(thumb, 3.0, t.canvas);
if let Some(job) = app.session.variant_job(id, &s, THUMB_EDGE)
@@ -122,33 +151,33 @@ fn cell_ui(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id:
{
painter.image(tex.tex.id(), thumb, cover_uv(thumb, tex.size), Color32::WHITE);
}
let applied = d.profile.id == p.id;
let applied = d.profile.id == p_id;
if applied {
painter.rect_stroke(thumb.expand(1.0), 4.0, Stroke::new(2.0, t.pick), egui::StrokeKind::Outside);
} else if resp.hovered() {
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::tr(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 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));
register(ui.ctx(), format!("profileStar:{}{}", if tag.is_empty() { "" } else { "fav:" }, p.id), star);
register(ui.ctx(), format!("profileStar:{}{}", if tag.is_empty() { "" } else { "fav:" }, p_id), star);
let over_star = resp.hover_pos().is_some_and(|q| star.contains(q));
if fav || resp.hovered() {
painter.circle_filled(star.center(), 10.0, Color32::from_black_alpha(110));
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() });
ui.data_mut(|m| m.insert_temp(egui::Id::new("last-profile-hover"), (id, p.id.to_string())));
app.hover_preview = Some(HoverPreview { label: crate::i18n::tr_format!("Profile: {}", crate::i18n::tr(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::tr(p_name), crate::i18n::tr(p_group)));
if resp.clicked() {
if resp.interact_pointer_pos().is_some_and(|q| star.contains(q)) {
let _ = app.run("profile.favorite", json!({"id": p.id}));
let _ = app.run("profile.favorite", json!({"id": p_id}));
} else if !applied {
let _ = app.run("develop.profile", json!({"id": p.id, "amount": 100}));
let _ = app.run("develop.profile", json!({"id": p_id, "amount": 100}));
ui.data_mut(|m| m.remove::<(PhotoId, String)>(egui::Id::new("last-profile-hover")));
}
}
+1 -1
View File
@@ -291,7 +291,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-PROF-LEGACY | Legacy profiles | P2 | ⬜ | | |
| LR-PROF-NONRAW | Profiles for non-raw files | P0 | ✅ | `cmd:develop.profile` | same looks apply to JPEG/TIFF |
| LR-PROF-AMOUNT | Profile amount | P1 | ✅ | `ctl:profile.amount` | |
| LR-PROF-IMPORT | Import profiles | P1 | 🟡 | `cmd:profile.import`, `cmd:profile.deleteImported`, `crates/pipeline/src/lut.rs`, `crates/engine/src/cmd/lut_profiles.rs` | `.cube` 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported |
| LR-PROF-IMPORT | Import profiles | P1 | ✅ | `cmd:profile.import`, `cmd:profile.deleteImported`, `crates/pipeline/src/lut.rs`, `crates/engine/src/cmd/lut_profiles.rs`, `crates/ui-egui/src/panels/edit.rs`, `crates/ui-egui/src/panels/profiles.rs` | `.cube` 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported |
## H. Crop & rotate (CROP)