mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Gradient: Cmd/Ctrl-click on the Gradient Fill box or the Gradient button resets to White, Black
The reference app resets a gradient to black and white when the gradient button in the Tools panel is Cmd/Ctrl-clicked, and to the default black and white linear gradient when the Gradient Fill box in the Gradient panel is Cmd/Ctrl-clicked. In VectorCraft a Cmd/Ctrl-click on either did what a plain click does, and the reset was only in the Gradient panel menu. A Cmd/Ctrl-click on the Gradient panel's thumbnail or on the Tools panel's Gradient button now makes the gradient of the active Fill or Stroke a linear gradient with the White, Black stops, through paint.editGradient, as one undo step. A solid or no paint becomes that gradient, and with nothing selected the default paint changes. A plain click keeps its behavior, and the panel menu's Reset to White, Black is unchanged. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
//! recolours the stop it lands on; a global or spot colour (or a tint of one) stays linked, so
|
||||
//! editing the swatch recolours the stop. The thumbnail menu marks the gradient swatch the
|
||||
//! gradient was applied from. Hide Options leaves the thumbnail, the proxy and the slider.
|
||||
//! Cmd/Ctrl-click on the thumbnail (the Gradient Fill box) or on the Tools panel's Gradient
|
||||
//! button resets the active proxy's gradient to the linear White, Black gradient.
|
||||
//!
|
||||
//! With the Stroke proxy in front, the Stroke buttons lay a linear or radial gradient within the
|
||||
//! stroke (placed on the page like a fill's), along it or across it (angle and aspect ratio then
|
||||
@@ -189,6 +191,13 @@ fn write_stops(app: &mut VectorcraftApp, params: Value, select: Option<usize>, p
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the gradient behind the active proxy linear with the White, Black stops, as one undo
|
||||
/// step (a solid or no paint becomes that gradient). With nothing selected it sets the default.
|
||||
/// Cmd/Ctrl-click on the Gradient Fill box or on the Tools panel's Gradient button runs it.
|
||||
pub(crate) fn reset_to_linear_white_black(app: &mut VectorcraftApp) {
|
||||
edit(app, json!({ "kind": "linear", "stops": stops_json(&Gradient::default().stops) }), Live::Released);
|
||||
}
|
||||
|
||||
/// Save the gradient as a swatch; its stops keep their color models.
|
||||
fn save_to_swatches(app: &mut VectorcraftApp, g: &GradientPaint) {
|
||||
app.run("swatch.new", super::swatch_paint_params(&Paint::Gradient(Box::new(g.clone())))).ok();
|
||||
@@ -444,8 +453,12 @@ fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad:
|
||||
widgets::gradient_chip(ui, r, g);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
let resp = resp.on_hover_text(tl!("Gradient Fill: click to apply, drag onto art"));
|
||||
if resp.clicked() && !is_grad {
|
||||
edit(app, json!({}), Live::Released);
|
||||
if resp.clicked() {
|
||||
if ui.input(|i| i.modifiers.command) {
|
||||
reset_to_linear_white_black(app);
|
||||
} else if !is_grad {
|
||||
edit(app, json!({}), Live::Released);
|
||||
}
|
||||
}
|
||||
// The dragged copy carries no placement: it fits the art it lands on.
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::paint(Paint::Gradient(Box::new(g.clone()))));
|
||||
|
||||
@@ -134,9 +134,14 @@ impl Panel {
|
||||
}
|
||||
|
||||
fn click(&mut self, app: &mut VectorcraftApp, at: Pos2) -> Vec<(String, Pos2)> {
|
||||
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
self.frame(app, vec![Event::PointerMoved(at), b(true)], Modifiers::NONE);
|
||||
self.frame(app, vec![b(false)], Modifiers::NONE);
|
||||
self.click_with(app, at, Modifiers::NONE)
|
||||
}
|
||||
|
||||
/// A click at `at` with `m` held.
|
||||
fn click_with(&mut self, app: &mut VectorcraftApp, at: Pos2, m: Modifiers) -> Vec<(String, Pos2)> {
|
||||
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: m };
|
||||
self.frame(app, vec![Event::PointerMoved(at), b(true)], m);
|
||||
self.frame(app, vec![b(false)], m);
|
||||
self.frame(app, vec![], Modifiers::NONE)
|
||||
}
|
||||
|
||||
@@ -312,6 +317,51 @@ fn the_thumbnail_menu_applies_gradient_swatches_and_saves_one() {
|
||||
assert_eq!(gradient_swatches(&app).len(), before + 1);
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl-click on the Gradient Fill box (the thumbnail) resets the active proxy's gradient to
|
||||
/// the linear White, Black gradient as one undo step; a plain click leaves a gradient as it is.
|
||||
#[test]
|
||||
fn cmd_click_on_the_gradient_fill_box_resets_to_the_linear_white_black_gradient() {
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 0.5, "color": "#ffff00"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
let mut app = app(json!({"gradient": {"kind": "radial", "stops": stops}}));
|
||||
let mut p = Panel::new();
|
||||
p.frame(&mut app, vec![], Modifiers::NONE);
|
||||
let thumb = p.widget("grad-swatches").left_center() - vec2(21.0, 0.0);
|
||||
p.click(&mut app, thumb);
|
||||
assert_eq!(hexes(&app), ["#ff0000", "#ffff00", "#0000ff"], "a plain click keeps the gradient");
|
||||
p.click_with(&mut app, thumb, Modifiers::COMMAND);
|
||||
assert_eq!(fill(&app).gradient.kind, GradientKind::Linear);
|
||||
assert_eq!(hexes(&app), ["#ffffff", "#000000"]);
|
||||
assert_eq!(app.session.last_gradient.gradient.stops.len(), 2, "the Tools panel's Gradient button shows it");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(fill(&app).gradient.kind, GradientKind::Radial, "one undo step");
|
||||
assert_eq!(hexes(&app), ["#ff0000", "#ffff00", "#0000ff"]);
|
||||
// With the Stroke proxy in front, a solid stroke becomes the gradient; the fill stays.
|
||||
app.run("paint.setStroke", json!({"color": "#00ff00"})).unwrap();
|
||||
app.session.fill_active = false;
|
||||
p.click_with(&mut app, thumb, Modifiers::COMMAND);
|
||||
let stroke = match crate::panels::current_paints(&app).1 {
|
||||
Paint::Gradient(g) => *g,
|
||||
s => panic!("expected a gradient stroke, got {s:?}"),
|
||||
};
|
||||
assert_eq!(stroke.gradient.kind, GradientKind::Linear);
|
||||
assert_eq!(stroke.gradient.stops.iter().map(|s| s.color.to_hex()).collect::<Vec<_>>(), ["#ffffff", "#000000"]);
|
||||
assert_eq!(fill(&app).gradient.kind, GradientKind::Radial);
|
||||
}
|
||||
|
||||
/// With the options hidden the Gradient Fill box stays, and Cmd/Ctrl-click on it resets a
|
||||
/// freeform gradient too.
|
||||
#[test]
|
||||
fn cmd_click_on_the_gradient_fill_box_with_the_options_hidden() {
|
||||
let mut app = app(json!({"gradient": {"kind": "freeform"}}));
|
||||
let mut p = Panel::new();
|
||||
set_pstate(&p.ctx, HIDE_OPTIONS, true);
|
||||
p.frame(&mut app, vec![], Modifiers::NONE);
|
||||
let thumb = p.widget("grad-swatches").left_center() - vec2(21.0, 0.0);
|
||||
p.click_with(&mut app, thumb, Modifiers::COMMAND);
|
||||
assert_eq!(fill(&app).gradient.kind, GradientKind::Linear);
|
||||
assert_eq!(hexes(&app), ["#ffffff", "#000000"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_drag_copies_a_stop_and_alt_dropping_on_a_stop_swaps_colours() {
|
||||
let mut app = app(json!({"gradient": {}}));
|
||||
|
||||
@@ -496,7 +496,10 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usiz
|
||||
clicked = Some(cmd);
|
||||
}
|
||||
}
|
||||
if let Some(cmd) = clicked {
|
||||
// Cmd/Ctrl-click on Gradient resets the gradient to White, Black.
|
||||
if clicked == Some("paint.lastGradient") && ui.input(|i| i.modifiers.command) {
|
||||
crate::panels::gradient::reset_to_linear_white_black(app);
|
||||
} else if let Some(cmd) = clicked {
|
||||
app.run(cmd, json!({})).ok();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -610,6 +610,35 @@ fn the_paint_and_drawing_mode_buttons_fit_the_toolbar_width() {
|
||||
assert_eq!(app.session.draw_mode, vectorcraft_engine::DrawMode::Behind);
|
||||
}
|
||||
|
||||
/// A click on the Gradient button applies the last gradient; Cmd/Ctrl-click resets the active
|
||||
/// proxy's gradient to the linear White, Black gradient instead, as one undo step.
|
||||
#[test]
|
||||
fn cmd_click_on_the_gradient_button_resets_to_the_linear_white_black_gradient() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
app.run("paint.setFill", json!({"gradient": {"kind": "radial", "stops": stops}})).unwrap();
|
||||
app.run("paint.setFill", json!({"color": "#336699"})).unwrap();
|
||||
let fill = |app: &VectorcraftApp| match crate::panels::current_paints(app).0 {
|
||||
Paint::Gradient(g) => (g.gradient.kind, g.gradient.stops.iter().map(|s| s.color.to_hex()).collect::<Vec<_>>()),
|
||||
p => panic!("expected a gradient fill, got {p:?}"),
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
frame_in(&mut app, &ctx, 0.0, vec![], 900.0);
|
||||
let gradient = widget_rects(&ctx, vec2(11.0, 11.0))[1].center();
|
||||
click_in(&mut app, &ctx, 1.0, gradient, PointerButton::Primary, 900.0);
|
||||
assert_eq!(fill(&app), (vectorcraft_color::GradientKind::Radial, vec!["#ff0000".into(), "#0000ff".into()]), "the last gradient");
|
||||
let m = egui::Modifiers::COMMAND;
|
||||
let b = |pressed| Event::PointerButton { pos: gradient, button: PointerButton::Primary, pressed, modifiers: m };
|
||||
frame_in(&mut app, &ctx, 2.0, vec![Event::ModifiersChanged(m), Event::PointerMoved(gradient), b(true)], 900.0);
|
||||
frame_in(&mut app, &ctx, 2.05, vec![b(false)], 900.0);
|
||||
frame_in(&mut app, &ctx, 2.1, vec![Event::ModifiersChanged(egui::Modifiers::NONE)], 900.0);
|
||||
assert_eq!(fill(&app), (vectorcraft_color::GradientKind::Linear, vec!["#ffffff".into(), "#000000".into()]));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(fill(&app).0, vectorcraft_color::GradientKind::Radial, "one undo step");
|
||||
}
|
||||
|
||||
/// How many columns of tool buttons `rects` make.
|
||||
fn columns(rects: &[egui::Rect]) -> usize {
|
||||
let mut lefts: Vec<i32> = rects.iter().map(|r| r.left().round() as i32).collect();
|
||||
|
||||
@@ -861,6 +861,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. Windows, Linux and other browsers have no sampler yet; the button is gray there.
|
||||
- **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/it/stroke_visibility.rs`).
|
||||
- **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh.
|
||||
Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)).
|
||||
With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke.
|
||||
- **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. Points in different models mix in display RGB, and a CMYK document stores the result as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own.
|
||||
- **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty more arrowheads, and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance.
|
||||
|
||||
Reference in New Issue
Block a user