Gradient Map: pick presets from the gradient library (#1780)
The Gradient Map preset picker lists the session's gradient library by group with a swatch per preset, in the Properties panel and the dialog, as Photoshop's gradient picker does. Picking one writes its colour stops as one undoable edit. Fixes #1588. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
616f206c5d
commit
347a0f9fc9
|
|
@ -182,7 +182,14 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, fields: &mut Map<String,
|
|||
Some(id) if adjust_editors::needs_histogram(&kind) => Some(tone::histograms(app, HistSource::Layer(id), adjust_editors::space_of(&values))),
|
||||
_ => None,
|
||||
};
|
||||
let cx = EditorCx { mem: dialog_mem(&kind), hist, gray, swatches: adjust_editors::swatches(app), dialog: true };
|
||||
let cx = EditorCx {
|
||||
mem: dialog_mem(&kind),
|
||||
hist,
|
||||
gray,
|
||||
swatches: adjust_editors::swatches(app),
|
||||
gradients: adjust_editors::gradient_presets(app, &kind),
|
||||
dialog: true,
|
||||
};
|
||||
let e = adjust_editors::editor(ui, &kind, &mut values, &cx);
|
||||
if e.changed
|
||||
&& let Value::Object(v) = values
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ use egui::{Color32, Pos2, Rect, RichText, Sense, Stroke, StrokeKind, pos2, vec2}
|
|||
use photocraft_doc::adjust::{HueRange, ToneSpace};
|
||||
use photocraft_doc::{Adjustment, LayerId};
|
||||
use photocraft_engine::adjust_params::{self, HUE_RANGES, PHOTO_FILTERS};
|
||||
use photocraft_engine::presets::Group;
|
||||
use photocraft_engine::presets::gradients::GradientPreset;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::PhotocraftApp;
|
||||
|
|
@ -79,6 +81,8 @@ pub struct EditorCx {
|
|||
pub gray: bool,
|
||||
/// Current foreground and background colours (Gradient Map preset).
|
||||
pub swatches: [[f32; 3]; 2],
|
||||
/// The session's gradient preset library (Gradient Map's picker; empty for other kinds).
|
||||
pub gradients: Vec<Group<GradientPreset>>,
|
||||
/// Hosted by an Image › Adjustments dialog (false: the Properties panel). Photoshop's dialog
|
||||
/// sliders follow the mouse wheel; the panel's don't.
|
||||
pub dialog: bool,
|
||||
|
|
@ -1109,22 +1113,34 @@ fn gradient_map(ui: &mut egui::Ui, v: &mut Value, cx: &EditorCx) -> Edit {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let mut e = Edit::default();
|
||||
let mut stops = read_stops(v);
|
||||
let presets: [(&str, Vec<Stop>); 5] = [
|
||||
(tl!("Black, White"), vec![(0.0, [0.0; 3]), (1.0, [1.0; 3])]),
|
||||
(tl!("Foreground to Background"), vec![(0.0, cx.swatches[0]), (1.0, cx.swatches[1])]),
|
||||
(tl!("Violet, Orange"), vec![(0.0, [0.161, 0.039, 0.349]), (1.0, [1.0, 0.486, 0.0])]),
|
||||
(tl!("Blue, Red, Yellow"), vec![(0.0, [0.039, 0.0, 0.698]), (0.5, [1.0, 0.0, 0.0]), (1.0, [1.0, 0.988, 0.0])]),
|
||||
(tl!("Copper"), vec![(0.0, [0.592, 0.275, 0.102]), (0.4, [0.984, 0.847, 0.773]), (0.7, [0.424, 0.180, 0.086]), (1.0, [0.937, 0.859, 0.804])]),
|
||||
];
|
||||
let mut preset = usize::MAX;
|
||||
// Photoshop's gradient picker: the same library as the Gradient tool and Gradients panel
|
||||
// (built-in groups and the user's own), Foreground/Background resolved to the current colours.
|
||||
let [fg, bg] = cx.swatches.map(|c| [c[0], c[1], c[2], 1.0]);
|
||||
ui.horizontal(|ui| {
|
||||
label(ui, tl!("Preset:"));
|
||||
let mut opts: Vec<(usize, &str)> = vec![(usize::MAX, tl!("Custom"))];
|
||||
opts.extend(presets.iter().enumerate().map(|(i, p)| (i, p.0)));
|
||||
if widgets::dropdown(ui, &format!("{:?}-gm-preset", cx.mem), &mut preset, &opts, 190.0)
|
||||
&& let Some(p) = presets.get(preset)
|
||||
{
|
||||
stops = p.1.clone();
|
||||
let mut picked = None;
|
||||
egui::ComboBox::from_id_salt(cx.mem.with("gm-preset")).selected_text(tl!("Custom")).width(190.0).height(420.0).icon(widgets::chevron_icon).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for g in &cx.gradients {
|
||||
ui.label(RichText::new(&g.name).color(t.text_faint));
|
||||
for p in &g.items {
|
||||
ui.horizontal(|ui| {
|
||||
// A map has no transparency: colour stops only, shown opaque.
|
||||
let rgba: Vec<(f32, [f32; 4])> = p.resolve(fg, bg).into_iter().map(|(at, c)| (at, [c[0], c[1], c[2], 1.0])).collect();
|
||||
let (r, swatch) = ui.allocate_exact_size(vec2(48.0, 16.0), Sense::click());
|
||||
crate::preset_panels::paint_gradient(ui, r, &rgba);
|
||||
// Both, not short-circuited: the name is drawn either way.
|
||||
if swatch.clicked() | ui.selectable_label(false, &p.name).clicked() {
|
||||
picked = Some(rgba.iter().map(|(at, c)| (*at, [c[0], c[1], c[2]])).collect::<Vec<Stop>>());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
if let Some(p) = picked {
|
||||
stops = p;
|
||||
write_stops(v, &stops);
|
||||
e.add(Edit::discrete(true));
|
||||
}
|
||||
|
|
@ -1202,6 +1218,11 @@ pub fn is_gray(mode: photocraft_doc::ColorMode) -> bool {
|
|||
matches!(mode, photocraft_doc::ColorMode::Grayscale | photocraft_doc::ColorMode::Duotone | photocraft_doc::ColorMode::Bitmap)
|
||||
}
|
||||
|
||||
/// The gradient preset library for a `kind` editor's picker (Gradient Map only; empty otherwise).
|
||||
pub fn gradient_presets(app: &PhotocraftApp, kind: &str) -> Vec<Group<GradientPreset>> {
|
||||
if kind == "gradientMap" { app.session.presets.gradients.clone() } else { Vec::new() }
|
||||
}
|
||||
|
||||
pub fn swatches(app: &PhotocraftApp) -> [[f32; 3]; 2] {
|
||||
let c = |x: [f32; 4]| [x[0], x[1], x[2]];
|
||||
[c(app.session.tools.foreground), c(app.session.tools.background)]
|
||||
|
|
@ -1313,7 +1334,7 @@ pub fn layer_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, id: LayerId, adj
|
|||
};
|
||||
let gray = app.session.active().is_some_and(|s| is_gray(s.doc.mode));
|
||||
let hist = needs_histogram(kind).then(|| tone::histograms(app, HistSource::BelowLayer(id), space_of(&values)));
|
||||
let cx = EditorCx { mem: layer_mem(id), hist, gray, swatches: swatches(app), dialog: false };
|
||||
let cx = EditorCx { mem: layer_mem(id), hist, gray, swatches: swatches(app), gradients: gradient_presets(app, kind), dialog: false };
|
||||
let e = editor(ui, kind, &mut values, &cx);
|
||||
if e.changed {
|
||||
app.live_adjust = Some((id, values.clone()));
|
||||
|
|
|
|||
|
|
@ -396,3 +396,39 @@ fn color_balance_properties_history_and_reset_like_photoshop() {
|
|||
assert_eq!(cb(&h), ([0.0; 3], [0.0; 3], [0.0; 3], false), "third Reset: still the defaults");
|
||||
assert_eq!(steps(&h), before, "the edit and the Resets share one history step");
|
||||
}
|
||||
|
||||
fn map_stops(a: &Adjustment) -> Vec<(f32, [u8; 3])> {
|
||||
let q = |v: f32| (v * 255.0).round() as u8;
|
||||
match a {
|
||||
Adjustment::GradientMap { stops, .. } => stops.iter().map(|(t, c)| (*t, [q(c[0]), q(c[1]), q(c[2])])).collect(),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue #1588: the Gradient Map preset picker offers the session's gradient library (built-in
|
||||
/// groups beyond Basics and the user's own saved gradients), and picking one writes its stops as
|
||||
/// one history step.
|
||||
#[test]
|
||||
fn gradient_map_picker_offers_the_gradient_library() {
|
||||
use photocraft_engine::presets::{Group, gradients};
|
||||
let mut h = app_harness("gradientMap", "rgb", 8);
|
||||
// A short library so every row fits the popup: Basics, Blues and a user-saved group.
|
||||
let mut lib: Vec<_> = gradients::builtin().into_iter().take(2).collect();
|
||||
lib.push(Group::new("Mine", vec![gradients::GradientPreset::new("My Teal", &["#008080", "#ffee00"])]));
|
||||
h.state_mut().session.presets.gradients = lib;
|
||||
h.run_steps(2);
|
||||
let steps = |h: &Harness<'_, PhotocraftApp>| h.state().session.active().unwrap().history.past_len();
|
||||
for (name, want) in
|
||||
[("Blue 03", vec![(0.0, [0x00, 0xb4, 0xd8]), (1.0, [0x03, 0x04, 0x5e])]), ("My Teal", vec![(0.0, [0x00, 0x80, 0x80]), (1.0, [0xff, 0xee, 0x00])])]
|
||||
{
|
||||
let before = steps(&h);
|
||||
// The picker sits right of its "Preset:" label.
|
||||
let preset = label_rect(&h, "Preset:").expect("the preset picker");
|
||||
click(&mut h, Pos2::new(preset.right() + 60.0, preset.center().y));
|
||||
let item = h.query_by_label(name).unwrap_or_else(|| panic!("`{name}` in the picker")).rect();
|
||||
click(&mut h, item.center());
|
||||
h.run_steps(4);
|
||||
assert_eq!(map_stops(&layer(&h).1), want, "{name}");
|
||||
assert_eq!(steps(&h), before + 1, "{name}: one history step");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue