mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Recolor Artwork: double-click a new color to pick it in the Color Picker, in the model it was picked in (part of #4028)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
//! undo step through `paint.setFill` / `paint.setStroke`, Cancel puts the art back.
|
||||
//! Opened for an artboard (Properties › Artboards › Fill), it shows the color on the artboard's
|
||||
//! paper live, and OK keeps it as one undo step through `artboard.setProps {index, color}`.
|
||||
//! Opened over Recolor Artwork (double-click a row's new color), its OK sets that row's new color
|
||||
//! and goes back to Recolor Artwork, whose preview the art keeps showing.
|
||||
//!
|
||||
//! Fields agents set with `ui.dialog.set`: `hex` (`"00FF00"`) or `color` (any colour the paint
|
||||
//! commands take), `channel` (`hue`, `saturation`, `brightness`, `red`, `green`, `blue`), `webOnly`
|
||||
@@ -240,6 +242,24 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// The field of a Color Picker opened over Recolor Artwork ([`over_recolor`]): the row whose new
|
||||
/// color it picks.
|
||||
const RECOLOR_ROW: &str = "__recolorRow";
|
||||
|
||||
/// The Color Picker over the Recolor Artwork dialog `parent`, on `color`, the new color of row
|
||||
/// `row`: it previews nothing itself, its OK goes back to `parent` with the row's new color set
|
||||
/// ([`super::recolor::with_new_color`]) and its Cancel goes back to `parent` as it was.
|
||||
pub(super) fn over_recolor(parent: &Dialog, color: Color, row: usize) -> Dialog {
|
||||
let mut d = Dialog::new(
|
||||
KIND,
|
||||
json!({"original": color_json(&color), "channel": Channel::default().id(), "webOnly": false, "swatches": false, RECOLOR_ROW: row}),
|
||||
);
|
||||
d.fields.insert(super::PARENT.into(), serde_json::to_value(parent).unwrap_or_default());
|
||||
store(&mut d, &Picked::of(color));
|
||||
d.fields.insert(crate::screen_color::PICKER_ID.into(), json!(crate::screen_color::picker_id()));
|
||||
d
|
||||
}
|
||||
|
||||
/// Stable action width in both modes, including translated labels.
|
||||
fn button_width(ui: &Ui) -> f32 {
|
||||
["OK", "Cancel", "Color Swatches", "Color Models"].into_iter().fold(110.0_f32, |w, label| {
|
||||
@@ -406,6 +426,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> DialogAction {
|
||||
/// Opened for an artboard, the picker shows the color on that artboard's paper, with or without a
|
||||
/// selection.
|
||||
fn preview(app: &mut VectorcraftApp, d: &mut Dialog) {
|
||||
// Over Recolor Artwork the art shows that dialog's preview, which this one must not replace.
|
||||
if d.fields.contains_key(RECOLOR_ROW) {
|
||||
return;
|
||||
}
|
||||
let (cmd, params) = paint_command(app, d);
|
||||
let first = !app.session.in_interaction();
|
||||
let unchanged = d.fields.get(PREVIEWED) == Some(¶ms) || first && d.fields.get("original").and_then(color_value) == Some(picked(d).color);
|
||||
@@ -430,17 +454,23 @@ fn preview(app: &mut VectorcraftApp, d: &mut Dialog) {
|
||||
/// The command that applies the picked colour to the proxy, and its params (as CMYK in a CMYK
|
||||
/// document). Opened for an artboard, the command sets that artboard's background color.
|
||||
fn paint_command(app: &VectorcraftApp, d: &Dialog) -> (&'static str, Value) {
|
||||
let mut c = picked(d).color;
|
||||
if matches!(c, Color::Rgb { .. }) && app.session.active().is_some_and(|s| s.doc.color_mode == ColorMode::Cmyk) {
|
||||
let [cy, m, y, k] = c.to_cmyk_managed(cms::active().settings().intent);
|
||||
c = Color::cmyk(cy, m, y, k);
|
||||
}
|
||||
let c = in_document_model(app, picked(d).color);
|
||||
if let Some(i) = d.fields.get("artboard").and_then(Value::as_u64) {
|
||||
return ("artboard.setProps", json!({ "index": i, "color": color_json(&c) }));
|
||||
}
|
||||
(if d.bool("stroke") { "paint.setStroke" } else { "paint.setFill" }, json!({ "color": color_json(&c) }))
|
||||
}
|
||||
|
||||
/// The picked colour as the document takes it: an RGB colour is CMYK in a CMYK document; CMYK,
|
||||
/// grey and Lab colours stay as they are.
|
||||
fn in_document_model(app: &VectorcraftApp, c: Color) -> Color {
|
||||
if matches!(c, Color::Rgb { .. }) && app.session.active().is_some_and(|s| s.doc.color_mode == ColorMode::Cmyk) {
|
||||
let [cy, m, y, k] = c.to_cmyk_managed(cms::active().settings().intent);
|
||||
return Color::cmyk(cy, m, y, k);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// The New (top) and Original (bottom) chips. Returns true when Original is clicked.
|
||||
fn new_original_chips(ui: &mut Ui, new: &Color, original: &Color) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -593,6 +623,12 @@ fn swatch_list(app: &VectorcraftApp, ui: &mut Ui, current: &Color, side: f32) ->
|
||||
/// OK with the starting color unchanged and nothing previewed closes the picker and leaves the
|
||||
/// artboard as it was: without a background color, or with its color in its own color model.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
if let Some(row) = d.fields.get(RECOLOR_ROW).and_then(Value::as_u64) {
|
||||
let parent = super::parent(d).ok_or("Recolor Artwork is no longer open")?;
|
||||
let c = in_document_model(app, picked(d).color);
|
||||
app.ui.dialog = Some(super::recolor::with_new_color(app, parent, usize::try_from(row).unwrap_or(usize::MAX), c));
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
if d.bool("keep") && !app.session.in_interaction() && d.fields.get("original").and_then(color_value) == Some(picked(d).color) {
|
||||
app.ui.dialog = None;
|
||||
return Ok(Value::Null);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//!
|
||||
//! Assign tab: Colors (Auto, or a count: similar colours merge into rows), Preserve White, Black and
|
||||
//! Grays, the rows (click to select, Shift or Cmd/Ctrl-click to add; a row's arrow excludes or
|
||||
//! includes it; the hex field edits its new colour), Merge and Separate, Randomize order or
|
||||
//! includes it; the hex field edits its new colour, and a double-click on it opens the Color Picker,
|
||||
//! whose OK sets it in the model it was picked in), Merge and Separate, Randomize order or
|
||||
//! saturation and brightness, Reset, the Method and Limit to Library (its folder button imports a
|
||||
//! library file and limits to it; a library's colours apply as its swatches: a spot colour stays a
|
||||
//! spot colour, and joins the document's swatches). Edit tab: a hue and
|
||||
@@ -57,6 +58,8 @@ const WHEEL: f32 = 220.0;
|
||||
const TABLE_H: f32 = 220.0;
|
||||
const TAB_H: f32 = 400.0;
|
||||
const PRESETS: [&str; 5] = ["Custom", "1 Color Job", "2 Color Job", "3 Color Job", "Color Library"];
|
||||
/// The id of row `i`'s new colour chip, with `i`.
|
||||
const NEW_CHIP: &str = "recolor-new";
|
||||
|
||||
/// Open Recolor Artwork (`ui.recolorDialog {colors?, library?, group?}`) on the selected art and,
|
||||
/// with `group`, on that colour group: art is reduced to as many rows as the group has colours,
|
||||
@@ -228,7 +231,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
ui.add_space(4.0);
|
||||
let mut rs = rows(d);
|
||||
let top = ui.cursor().top();
|
||||
let changed = if d.str("tab") == "edit" { edit_tab(ui, d, &mut rs) } else { assign_tab(app, ui, d, &mut rs) };
|
||||
let (changed, pick) = if d.str("tab") == "edit" { (edit_tab(ui, d, &mut rs), None) } else { assign_tab(app, ui, d, &mut rs) };
|
||||
if d.str("tab") != "edit" {
|
||||
ui.add_space((TAB_H - (ui.cursor().top() - top)).max(0.0));
|
||||
}
|
||||
@@ -247,9 +250,23 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
if d.bool("__art") {
|
||||
form::preview(app, ui, d, "Recolor Artwork", CMD, apply_params(d));
|
||||
}
|
||||
if let Some(c) = pick.and_then(|i| Some((i, to_color(rows(d).get(i)?)?))) {
|
||||
*d = super::color_picker::over_recolor(d, c.1, c.0);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `d` with row `i`'s new colour set to `c` (the Color Picker's OK), snapped to Limit to Library.
|
||||
pub(super) fn with_new_color(app: &VectorcraftApp, mut d: Dialog, i: usize, c: Color) -> Dialog {
|
||||
let mut rs = rows(&d);
|
||||
if let Some(r) = rs.get_mut(i) {
|
||||
r["to"] = key(&c);
|
||||
snap(app, &d, &mut rs);
|
||||
d.fields.insert("rows".into(), Value::Array(rs));
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// OK: keep the previewed recolour as one undo step (or apply it, e.g. to a colour group only; or
|
||||
/// save colours opened alone as a new colour group).
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
@@ -292,8 +309,9 @@ fn chip(ui: &Ui, r: Rect, c: Color, dim: bool) {
|
||||
ui.painter().rect_stroke(r, 1.0, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// The Assign tab. Returns true when the rows changed.
|
||||
fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut Vec<Value>) -> bool {
|
||||
/// The Assign tab. Returns true when the rows changed, and the row whose new colour was
|
||||
/// double-clicked.
|
||||
fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut Vec<Value>) -> (bool, Option<usize>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut changed = false;
|
||||
let total: usize = rows.iter().map(|r| r["from"].as_array().map_or(0, Vec::len)).sum();
|
||||
@@ -331,6 +349,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
});
|
||||
let mut sel = selection(d);
|
||||
let mut clicked: Option<usize> = None;
|
||||
let mut pick: Option<usize> = None;
|
||||
widgets::list_box(ui, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("recolor-rows").max_height(TABLE_H).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
@@ -360,12 +379,16 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
row["exclude"] = json!(!off);
|
||||
changed = true;
|
||||
}
|
||||
let (nr, nresp) = ui.allocate_exact_size(vec2(46.0, 20.0), Sense::click());
|
||||
let (nr, _) = ui.allocate_exact_size(vec2(46.0, 20.0), Sense::hover());
|
||||
let nresp = ui.interact(nr, egui::Id::new((NEW_CHIP, i)), Sense::click());
|
||||
let to = to_color(row).unwrap_or_default();
|
||||
chip(ui, nr, to, off);
|
||||
if nresp.clicked() {
|
||||
clicked = Some(i);
|
||||
}
|
||||
if nresp.double_clicked() {
|
||||
pick = Some(i);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let hex = crate::panels::color::hex_digits(&to);
|
||||
if let Some(h) = widgets::hex_field(ui, ("recolor-hex", i), &hex)
|
||||
@@ -468,7 +491,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
changed
|
||||
(changed, pick)
|
||||
}
|
||||
|
||||
/// The arrow between a row's current and new colours: solid when the row is recoloured, a dashed
|
||||
@@ -688,6 +711,88 @@ mod tests {
|
||||
assert_eq!([&r[0]["to"], &r[1]["to"], &r[2]["to"]], ["rgb 255 0 0", "rgb 0 255 0", "rgb 0 0 255"]);
|
||||
}
|
||||
|
||||
/// Runs one frame of the open dialog at `time` and returns row `i`'s new color chip, when the
|
||||
/// Recolor Artwork dialog drew it.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, events: Vec<egui::Event>, i: usize) -> Option<Rect> {
|
||||
let input = egui::RawInput { time: Some(time), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
ctx.read_response(egui::Id::new((NEW_CHIP, i))).map(|r| r.rect)
|
||||
}
|
||||
|
||||
/// Runs frames from `time` until the dialog stops moving (it centres itself as it measures its
|
||||
/// size) and returns row `i`'s new color chip.
|
||||
fn settled(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, i: usize) -> Option<Rect> {
|
||||
let mut last = None;
|
||||
for n in 0..10 {
|
||||
let r = frame(app, ctx, time + f64::from(n) * 0.001, vec![], i);
|
||||
if r.is_some() && r == last {
|
||||
return r;
|
||||
}
|
||||
last = r;
|
||||
}
|
||||
last
|
||||
}
|
||||
|
||||
/// Double-clicks row `i`'s new color chip, as drawn by the last frame at `rect`.
|
||||
fn double_click(app: &mut VectorcraftApp, ctx: &egui::Context, rect: Rect, time: f64, i: usize) {
|
||||
let pos = rect.center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
frame(app, ctx, time, vec![egui::Event::PointerMoved(pos), button(true)], i);
|
||||
frame(app, ctx, time + 0.05, vec![button(false)], i);
|
||||
frame(app, ctx, time + 0.1, vec![button(true)], i);
|
||||
frame(app, ctx, time + 0.15, vec![button(false)], i);
|
||||
}
|
||||
|
||||
/// Double-clicking a row's new color opens the Color Picker on it, over the dialog. OK sets the
|
||||
/// row's new color in the model it was picked in (a CMYK mix stays that mix, not a hex value
|
||||
/// converted back), a color picked as RGB is CMYK in a CMYK document, Cancel leaves the row as
|
||||
/// it was, and nothing is committed until Recolor Artwork's own OK.
|
||||
#[test]
|
||||
fn double_clicking_a_new_color_picks_it_in_its_own_model() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 100, "colorMode": "cmyk"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
app.run("paint.setFill", json!({"color": {"c": 0, "m": 100, "y": 100, "k": 0}})).unwrap();
|
||||
app.run("ui.recolorDialog", json!({})).unwrap();
|
||||
let undo = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len();
|
||||
let before = undo(&app);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&mut app, &ctx, 0.0, vec![], 0);
|
||||
let all = super::rows(app.ui.dialog.as_ref().unwrap());
|
||||
let i = all.iter().position(|r| r["to"] == "cmyk 0 100 100 0").expect("a row for the fill");
|
||||
let rect = settled(&mut app, &ctx, 0.01, i).expect("the row's new color is drawn");
|
||||
|
||||
double_click(&mut app, &ctx, rect, 0.1, i);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.kind, "colorPicker", "a double-click opens the Color Picker");
|
||||
assert_eq!(d.fields.get("color").and_then(color_value), Some(Color::cmyk(0.0, 1.0, 1.0, 0.0)), "on the row's new color");
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND), "Cancel comes back to Recolor Artwork");
|
||||
assert_eq!(super::rows(app.ui.dialog.as_ref().unwrap()), all, "with the rows as they were");
|
||||
|
||||
let rect = settled(&mut app, &ctx, 1.0, i).unwrap();
|
||||
double_click(&mut app, &ctx, rect, 1.1, i);
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
assert_eq!(d.kind, "colorPicker");
|
||||
// 100 50 0 0 is a mix the round trip through sRGB changes.
|
||||
d.fields.insert("color".into(), json!({"c": 100, "m": 50, "y": 0, "k": 0}));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.kind, KIND, "OK comes back to Recolor Artwork");
|
||||
assert_eq!(super::rows(d)[i]["to"], "cmyk 100 50 0 0", "the row keeps the CMYK mix picked");
|
||||
assert_eq!(undo(&app), before, "nothing is committed before Recolor Artwork's OK");
|
||||
|
||||
let rect = settled(&mut app, &ctx, 2.0, i).unwrap();
|
||||
double_click(&mut app, &ctx, rect, 2.1, i);
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
d.fields.insert("color".into(), json!([1.0, 0.0, 0.0]));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let to = super::rows(app.ui.dialog.as_ref().unwrap())[i]["to"].clone();
|
||||
assert!(to.as_str().is_some_and(|k| k.starts_with("cmyk ")), "a color picked as RGB is CMYK in a CMYK document: {to}");
|
||||
}
|
||||
|
||||
/// In a CMYK document colors opened alone are saved as a new color group in their own model.
|
||||
#[test]
|
||||
fn a_new_group_from_colors_keeps_their_model_in_a_cmyk_document() {
|
||||
|
||||
+4
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user