mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 14:33:34 +00:00
Outer Glow: a colour swatch beside Color opens the Color Picker (#5365)
Effect dialogs that take a hex colour (Outer Glow, Inner Glow, Drop Shadow's fields) show a swatch beside the colour field: it follows the field as it is typed, and a double-click opens the Color Picker over the dialog, whose OK fills the field and whose Cancel comes back to the dialog. While the picker is open it previews nothing itself: the effect dialog previews the colour when it returns. The swatch is part of the generic effect parameter grid, so every effect dialog with a colour parameter gets it, and the Color Picker learns to fill one field of a dialog opened over (`over_effect`, a parent like Recolor Artwork's). Test: dialogs::effect::tests::outer_glow_colour_swatch_opens_the_color_picker (swatch drawn on #ffff00, a typed hex recolours it, a double-click opens the picker on it, OK fills Color and the effect applies #00ff00).
This commit is contained in:
@@ -249,6 +249,23 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// The field of a Color Picker opened over a command dialog ([`over_effect`]): the dialog's colour
|
||||
/// field its OK fills.
|
||||
const COLOUR_FIELD: &str = "__colourField";
|
||||
|
||||
/// The Color Picker over the command dialog `parent`, on `colour`, that fills the dialog's `field`
|
||||
/// with its OK and goes back to it, as Cancel does. It previews nothing itself: the dialog's own
|
||||
/// preview shows the colour once it is back.
|
||||
pub(super) fn over_effect(parent: &Dialog, field: &str, colour: Color) -> Dialog {
|
||||
let mut d = Dialog::new(
|
||||
KIND,
|
||||
json!({"original": color_json(&colour), "channel": Channel::default().id(), "webOnly": false, "swatches": false, COLOUR_FIELD: field}),
|
||||
);
|
||||
d.fields.insert(super::PARENT.into(), serde_json::to_value(parent).unwrap_or_default());
|
||||
store(&mut d, &Picked::of(colour));
|
||||
d
|
||||
}
|
||||
|
||||
/// The field of a Color Picker opened over Recolor Artwork ([`over_recolor`]): the row whose new
|
||||
/// color it picks.
|
||||
const RECOLOR_ROW: &str = "__recolorRow";
|
||||
@@ -433,8 +450,9 @@ 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) {
|
||||
// Over Recolor Artwork the art shows that dialog's preview, which this one must not replace;
|
||||
// over a command dialog this picker only fills a field, and the dialog previews it.
|
||||
if d.fields.contains_key(RECOLOR_ROW) || d.fields.contains_key(COLOUR_FIELD) {
|
||||
return;
|
||||
}
|
||||
let (cmd, params) = paint_command(app, d);
|
||||
@@ -632,6 +650,15 @@ 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(field) = d.fields.get(COLOUR_FIELD).and_then(Value::as_str) {
|
||||
let mut parent = super::parent(d).ok_or("the dialog it was opened over is no longer open")?;
|
||||
parent.fields.insert(field.to_string(), json!(picked(d).color.to_hex()));
|
||||
// The dialog previews the new colour when it comes back: end this picker's interaction, as
|
||||
// Cancel does, so the dialog starts its preview afresh.
|
||||
let _ = app.session.cancel_interaction();
|
||||
app.ui.dialog = Some(parent);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -985,4 +985,60 @@ mod tests {
|
||||
assert_eq!((¶ms["texture"], ¶ms["textureImage"]), (&json!("loaded"), &json!(stored)));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
/// Effect > Stylize > Outer Glow: a swatch beside Color shows the effect's colour, typing a
|
||||
/// hex recolours it, and a double-click opens the Color Picker over the dialog; its OK fills
|
||||
/// Color, and the effect applies that colour.
|
||||
#[test]
|
||||
fn outer_glow_colour_swatch_opens_the_color_picker() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("effect.dialog", json!({"effect": "stylize.outerGlow"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
crate::theme::apply(&ctx, Default::default());
|
||||
let frame = |app: &mut VectorcraftApp, time: f64, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { time: Some(time), events, ..Default::default() }, |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
};
|
||||
for n in 0..6 {
|
||||
frame(&mut app, f64::from(n) * 0.01, vec![]);
|
||||
}
|
||||
let rect = ctx.read_response(form::colour_swatch_id("color")).expect("the colour swatch is drawn").rect;
|
||||
// The colour of the swatch drawn at `rect`.
|
||||
let swatch = |out: &egui::FullOutput| {
|
||||
out.shapes.iter().find_map(|c| match &c.shape {
|
||||
egui::Shape::Rect(r) if r.rect == rect => Some(r.fill),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let yellow = egui::Color32::from_rgb(255, 255, 0);
|
||||
assert_eq!(swatch(&frame(&mut app, 0.2, vec![])), Some(yellow), "the swatch shows the effect's #ffff00");
|
||||
// Typing a colour recolours the swatch.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff0000"));
|
||||
assert_eq!(swatch(&frame(&mut app, 0.25, vec![])), Some(egui::Color32::from_rgb(255, 0, 0)), "typing a hex recolours the swatch");
|
||||
// A double-click opens the Color Picker on the field's colour.
|
||||
let pos = rect.center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||||
frame(&mut app, 0.3, vec![egui::Event::PointerMoved(pos), button(true)]);
|
||||
frame(&mut app, 0.35, vec![button(false)]);
|
||||
frame(&mut app, 0.4, vec![button(true)]);
|
||||
frame(&mut app, 0.45, vec![button(false)]);
|
||||
frame(&mut app, 0.5, vec![]);
|
||||
let d = app.ui.dialog.as_ref().expect("a dialog");
|
||||
assert_eq!(d.kind, "colorPicker", "a double-click opens the Color Picker over the effect dialog");
|
||||
assert_eq!(d.fields.get("color").and_then(vectorcraft_engine::cmd::color_value), Some(Color::rgb(1.0, 0.0, 0.0)), "on the field's colour");
|
||||
// Its OK fills the effect's Color, and the effect applies it.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#00ff00"));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
let d = app.ui.dialog.as_ref().expect("the effect dialog is back");
|
||||
assert_eq!((d.kind.as_str(), d.fields.get("color")), ("effect", Some(&json!("#00ff00"))), "OK fills Color and comes back");
|
||||
frame(&mut app, 0.6, vec![]);
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
let st = app.session.doc().unwrap();
|
||||
let fx = &st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0];
|
||||
assert_eq!(fx.params.get("color"), Some(&json!("#00ff00")), "Outer Glow takes the picked colour");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_doc::color::Color;
|
||||
use vectorcraft_plugins::{Manifest, Param, ParamSpec};
|
||||
|
||||
use crate::state::Dialog;
|
||||
@@ -168,6 +169,26 @@ fn choices(kind: &str, key: &str) -> Vec<(&'static str, &'static str)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The id of the colour swatch beside the field `key` (a test clicks it; [`colour_swatch`] draws it).
|
||||
pub(super) fn colour_swatch_id(key: &str) -> egui::Id {
|
||||
egui::Id::unique(("fx-colour-swatch", key))
|
||||
}
|
||||
|
||||
/// The colour swatch beside the colour field `key`, showing `colour`: a double-click asks for the
|
||||
/// Color Picker over this dialog, whose OK fills the field ([`super::color_picker::over_effect`]).
|
||||
/// Returns whether the picker was asked for.
|
||||
fn colour_swatch(ui: &mut egui::Ui, key: &str, colour: Color) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, _) = ui.allocate_exact_size(egui::vec2(20.0, 18.0), egui::Sense::hover());
|
||||
let resp = ui.interact(rect, colour_swatch_id(key), egui::Sense::click());
|
||||
let [r, g, b, _] = colour.to_rgba8(1.0);
|
||||
ui.painter().rect_filled(rect, 2.0, egui::Color32::from_rgb(r, g, b));
|
||||
ui.painter().rect_stroke(rect, 2.0, egui::Stroke::new(1.0, t.input_border), egui::StrokeKind::Inside);
|
||||
if resp.hovered() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
|
||||
}
|
||||
resp.double_clicked()
|
||||
}
|
||||
/// A text field `width` wide bound to `d.fields[key]`. Returns true when it changed.
|
||||
pub(super) fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> bool {
|
||||
text_edit(ui, d, key, width).changed()
|
||||
@@ -403,6 +424,7 @@ pub(super) fn param_fields(
|
||||
) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut changed = false;
|
||||
let mut pick: Option<(String, Color)> = None;
|
||||
egui::Grid::new("fxgrid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
let mut keys: Vec<(String, Value)> =
|
||||
d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
@@ -480,18 +502,42 @@ pub(super) fn param_fields(
|
||||
}
|
||||
}
|
||||
Value::String(mut s) => {
|
||||
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(140.0)).changed() {
|
||||
d.fields.insert(k.clone(), json!(s));
|
||||
changed = true;
|
||||
// A hex colour (#rrggbb or #rgb) shows its swatch beside the field; the swatch's
|
||||
// double-click opens the Color Picker over this dialog.
|
||||
let mut colour = None;
|
||||
let mut ask = false;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(140.0)).changed() {
|
||||
d.fields.insert(k.clone(), json!(s));
|
||||
changed = true;
|
||||
}
|
||||
colour = crate::panels::color::parse_hex(&s);
|
||||
if let Some(c) = colour {
|
||||
ask = colour_swatch(ui, &k, c);
|
||||
}
|
||||
});
|
||||
if ask && let Some(c) = colour {
|
||||
pick = Some((k.clone(), c));
|
||||
}
|
||||
}
|
||||
// A color object shows as its hex; typing in the field stores the typed text.
|
||||
other => match color_object(&other) {
|
||||
Some(c) => {
|
||||
let mut s = c.to_hex();
|
||||
if ui.add(egui::TextEdit::singleline(&mut s).desired_width(140.0)).changed() {
|
||||
d.fields.insert(k.clone(), json!(s));
|
||||
changed = true;
|
||||
let mut colour = None;
|
||||
let mut ask = false;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(egui::TextEdit::singleline(&mut s).desired_width(140.0)).changed() {
|
||||
d.fields.insert(k.clone(), json!(s));
|
||||
changed = true;
|
||||
}
|
||||
colour = crate::panels::color::parse_hex(&s);
|
||||
if let Some(c) = colour {
|
||||
ask = colour_swatch(ui, &k, c);
|
||||
}
|
||||
});
|
||||
if ask && let Some(c) = colour {
|
||||
pick = Some((k.clone(), c));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -503,6 +549,9 @@ pub(super) fn param_fields(
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if let Some((key, colour)) = pick {
|
||||
*d = super::color_picker::over_effect(d, &key, colour);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user