mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Color panel: Cmd/Ctrl-click and Cmd/Ctrl+Shift-click on the spectrum apply Complement and Invert
The reference app selects the complement of the current fill or stroke on a Cmd/Ctrl-click of the Color panel's color bar and the inverse on a Cmd/Ctrl+Shift-click, and with Alt (Option) also held it changes the nonactive one. In VectorCraft these clicks picked the spectrum's color, and a Cmd/Ctrl+Shift-click cycled the color modes. A Cmd/Ctrl-click on the spectrum now applies Complement and a Cmd/Ctrl+Shift-click applies Invert to the active Fill or Stroke, as the panel menu's items do: a selected gradient stop or freeform point is recolored in place, and a solid color goes through paint.complement or paint.invert. With Alt also held, the click runs the command on the inactive proxy, which stays behind. A Cmd/Ctrl drag still picks colors, and plain, Alt and Shift clicks keep their behavior. The panel menu's Invert and Complement use the same code. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
//! The mode follows the colour's own model (the last mode picked comes back for colours in its
|
||||
//! model and shows the defaults for new art; the app keeps it between sessions). Alt-clicking the
|
||||
//! spectrum or a chip paints the inactive proxy; Shift-clicking the spectrum cycles the modes;
|
||||
//! Cmd/Ctrl-clicking it applies Complement and Cmd/Ctrl+Shift-clicking it applies Invert, both to
|
||||
//! the inactive proxy when Alt is also held;
|
||||
//! Shift-dragging a slider moves the others in tandem; RGB and HSB show the out-of-gamut warning;
|
||||
//! Hide Options leaves just the proxy and the spectrum (the app keeps it between sessions too).
|
||||
//! `ui.colorPanel` picks a mode and shows or hides the options as the panel menu does.
|
||||
@@ -459,6 +461,26 @@ fn set_mode(app: &mut VectorcraftApp, tgt: &Target, mode: Mode) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Invert (`invert`) or Complement, as the panel menu's items do: a gradient stop or freeform point
|
||||
/// is recolored in place, and a solid color or a "?" proxy goes through `paint.invert` or
|
||||
/// `paint.complement` (each selected object keeps its color model). Without a color or a "?"
|
||||
/// proxy nothing changes. With `behind` (Alt held) the command recolors the inactive proxy instead.
|
||||
fn recolor(app: &mut VectorcraftApp, ui: &Ui, tgt: &Target, invert: bool, behind: bool) {
|
||||
let (cmd, f): (&str, fn(&Color) -> Color) =
|
||||
if invert { ("paint.invert", Color::invert_keep_model) } else { ("paint.complement", Color::complement_keep_model) };
|
||||
if behind {
|
||||
app.run(cmd, json!({ "stroke": app.session.fill_active })).ok();
|
||||
return;
|
||||
}
|
||||
match (tgt, tgt.color()) {
|
||||
(Target::Stop { .. } | Target::Point { .. }, Some(c)) => apply(app, tgt, f(&c), Live::Released, false),
|
||||
(_, color) if color.is_some() || super::active_mixed(app, ui.ctx()) => {
|
||||
app.run(cmd, json!({})).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `ui.colorPanel {mode?, options?}`: pick a mode as the panel menu does ([`set_mode`]), and Show
|
||||
/// Options (true) or Hide Options (false). → {mode, options, shown}: the mode picked last (null:
|
||||
/// none yet), whether the options show, and the mode the panel shows the color in (`tint` for a
|
||||
@@ -516,6 +538,10 @@ enum SpectrumHit {
|
||||
Pick(Color, Live),
|
||||
/// Shift-click: the next colour mode.
|
||||
Cycle,
|
||||
/// Cmd/Ctrl-click: Complement, or with Shift also held (`invert`) Invert.
|
||||
Recolor {
|
||||
invert: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// The spectrum ramp: hue across, white to full colour to black down (a grey ramp in Grayscale).
|
||||
@@ -528,7 +554,11 @@ fn spectrum(ui: &mut Ui, mode: Mode, size: egui::Vec2) -> Option<SpectrumHit> {
|
||||
if let Some(p) = resp.hover_pos() {
|
||||
ui.painter().rect_stroke(Rect::from_center_size(p, vec2(5.0, 5.0)), 0.0, Stroke::new(1.0, Color32::WHITE), StrokeKind::Middle);
|
||||
}
|
||||
if resp.clicked() && ui.input(|i| i.modifiers.shift) {
|
||||
let (command, shift) = ui.input(|i| (i.modifiers.command, i.modifiers.shift));
|
||||
if resp.clicked() && command {
|
||||
return Some(SpectrumHit::Recolor { invert: shift });
|
||||
}
|
||||
if resp.clicked() && shift {
|
||||
return Some(SpectrumHit::Cycle);
|
||||
}
|
||||
let (p, phase) = widgets::pointer_phase(&resp)?;
|
||||
@@ -552,6 +582,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
match hit {
|
||||
Some(SpectrumHit::Pick(c, phase)) => apply(app, &tgt, c, phase, alt),
|
||||
Some(SpectrumHit::Cycle) => set_mode(app, &tgt, mode.next()),
|
||||
Some(SpectrumHit::Recolor { invert }) => recolor(app, ui, &tgt, invert, alt),
|
||||
None => {}
|
||||
}
|
||||
return;
|
||||
@@ -683,6 +714,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
match spectrum(ui, mode, vec2(ui.available_width(), 46.0)) {
|
||||
Some(SpectrumHit::Pick(c, phase)) => pick(c, phase, &mut new),
|
||||
Some(SpectrumHit::Cycle) => set_mode(app, &tgt, mode.next()),
|
||||
Some(SpectrumHit::Recolor { invert }) => recolor(app, ui, &tgt, invert, alt),
|
||||
None => {}
|
||||
}
|
||||
if let Some((c, phase)) = behind {
|
||||
@@ -714,20 +746,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
// Solid colours (and a "?" proxy) go through the proxy commands (each selected object keeps
|
||||
// its colour model); a gradient stop is recoloured in place.
|
||||
let recolor = color.is_some() || super::active_mixed(app, ui.ctx());
|
||||
for (label, cmd, f) in [
|
||||
(tl!("Invert"), "paint.invert", Color::invert_keep_model as fn(&Color) -> Color),
|
||||
(tl!("Complement"), "paint.complement", Color::complement_keep_model),
|
||||
] {
|
||||
if menu_item(ui, label, recolor, false) {
|
||||
match (&tgt, color) {
|
||||
(Target::Stop { .. } | Target::Point { .. }, Some(c)) => apply(app, &tgt, f(&c), Live::Released, false),
|
||||
_ => {
|
||||
app.run(cmd, json!({})).ok();
|
||||
}
|
||||
}
|
||||
let enabled = color.is_some() || super::active_mixed(app, ui.ctx());
|
||||
for (label, invert) in [(tl!("Invert"), true), (tl!("Complement"), false)] {
|
||||
if menu_item(ui, label, enabled, false) {
|
||||
recolor(app, ui, &tgt, invert, false);
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
|
||||
@@ -331,6 +331,73 @@ fn shift_click_on_the_spectrum_cycles_modes() {
|
||||
assert!(matches!(crate::panels::current_paints(&app).0.color(), Some(Color::Cmyk { .. })), "CMYK converts the colour");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_click_on_the_spectrum_applies_complement_and_cmd_shift_click_invert() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
let cmd = egui::Modifiers::COMMAND;
|
||||
app.run("paint.setFill", json!({"color": "#336699"})).unwrap();
|
||||
app.run("paint.setStroke", json!({"color": "#123456", "focus": false})).unwrap();
|
||||
assert!(app.session.fill_active);
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd);
|
||||
assert_eq!(paints_hex(&app), ("#996633".into(), "#123456".into()), "Complement of the fill");
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd | egui::Modifiers::SHIFT);
|
||||
assert_eq!(paints_hex(&app), ("#6699cc".into(), "#123456".into()), "Invert of the fill");
|
||||
assert_eq!(app.ui.color_mode, None, "Cmd/Ctrl+Shift-click does not cycle the modes");
|
||||
// With Alt the inactive proxy (the stroke) changes, and the fill stays in front.
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd | egui::Modifiers::ALT);
|
||||
assert_eq!(paints_hex(&app), ("#6699cc".into(), "#563412".into()), "Complement of the stroke");
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd | egui::Modifiers::SHIFT | egui::Modifiers::ALT);
|
||||
assert_eq!(paints_hex(&app), ("#6699cc".into(), "#a9cbed".into()), "Invert of the stroke");
|
||||
assert!(app.session.fill_active, "the fill stays in front");
|
||||
// With the stroke in front, Alt recolors the fill.
|
||||
app.run("paint.toggleActive", json!({})).unwrap();
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd);
|
||||
assert_eq!(paints_hex(&app), ("#6699cc".into(), "#edcba9".into()), "Complement of the stroke in front");
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), cmd | egui::Modifiers::ALT);
|
||||
assert_eq!(paints_hex(&app), ("#cc9966".into(), "#edcba9".into()), "Complement of the fill behind");
|
||||
assert!(!app.session.fill_active, "the stroke stays in front");
|
||||
// A plain click still picks the spectrum's color.
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), egui::Modifiers::NONE);
|
||||
assert_eq!(paints_hex(&app).1, "#ff0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_click_on_the_spectrum_recolors_the_selected_gradient_stop() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#336699"}]);
|
||||
app.run("paint.setFill", json!({"gradient": {"stops": stops, "start": [10, 35], "end": [60, 35]}})).unwrap();
|
||||
app.select_tool("gradient");
|
||||
app.run("gradient.selectStop", json!({"index": 1})).unwrap();
|
||||
click_spectrum(&mut app, &ctx, (0.0, 0.5), egui::Modifiers::COMMAND);
|
||||
let Paint::Gradient(g) = crate::panels::current_paints(&app).0 else { panic!("still a gradient") };
|
||||
let hex: Vec<String> = g.gradient.stops.iter().map(|s| s.color.to_hex()).collect();
|
||||
assert_eq!(hex, ["#ff0000", "#996633"], "only the selected stop changes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_click_on_the_spectrum_with_the_options_hidden() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
app.run("paint.setFill", json!({"color": "#336699"})).unwrap();
|
||||
app.run("ui.colorPanel", json!({"options": false})).unwrap();
|
||||
// The spectrum fills the row right of the proxy.
|
||||
let mut row = Rect::NOTHING;
|
||||
let input = egui::RawInput::default();
|
||||
let mut full = ctx.run_ui(input, |ui| {
|
||||
show(&mut app, ui);
|
||||
row = ui.min_rect();
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
let pos = egui::pos2(row.right() - 4.0, row.center().y);
|
||||
let cmd = egui::Modifiers::COMMAND | egui::Modifiers::SHIFT;
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: cmd };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos), button(true)], cmd);
|
||||
frame(&mut app, &ctx, vec![button(false)], cmd);
|
||||
assert_eq!(paints_hex(&app).0, "#cc9966", "Invert of the fill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_proxy_opens_the_color_picker_for_it() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -849,6 +849,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
|
||||
- **Missing library files:** a persistent swatch or graphic style library panel whose file is gone at startup reopens and stays in the preferences, showing the file's path with Locate Library… (`ui.libraryPanel.locate`), which reads the file from where it is now into the same panel.
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel shows a selected colour in its own model and, with nothing selected, the new-art defaults in the mode picked last, which it keeps between sessions; Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). An ICC CMYK profile converts straight into the RGB working space (not through sRGB, so a wider space such as Wide Gamut RGB keeps colours sRGB can't hold) with the colour settings' black-point compensation, on the canvas and in RGB exports alike (the built-in Generic CMYK compensates both ways, so RGB black separated to a rich black displays as black again). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
A Cmd/Ctrl-click on the Color panel's spectrum applies Complement and a Cmd/Ctrl+Shift-click applies Invert to the active Fill or Stroke (`paint.complement`, `paint.invert`); with Alt also held, the click recolors the inactive one, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) list them.
|
||||
View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value.
|
||||
- **Recolor Artwork layout (#3416):** the Edit chip list uses the modal scroll helper and its actual remaining height; the tab's minimum-height space scrolls with the chips. `tests_recolor` covers 5/20/200/400 colors, window resizing, 100/125/150/200% UI scaling, scrolling to and editing the final chip, Preview, Cancel, Confirm and Undo, alongside the existing #1733 drag regressions. Scores and estimates unchanged.
|
||||
- **Edit Colors parameters:** `edit.colors.saturate` needs a number `intensity`, the value from -100% to 100% that Saturate takes ([Adjusting colors](https://web.archive.org/web/20230128232631/https://helpx.adobe.com/illustrator/using/adjusting-colors.html)), and answers "missing number `intensity`" without one; a number outside the range counts as its nearest end. `edit.colors.adjustBalance` refuses a channel and `color.harmony` refuses `steps` or `amount` that is not a number; both read `null` as a value left out. `tests::editcolors` and `tests::colorguide` cover them.
|
||||
|
||||
Reference in New Issue
Block a user