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:
dotCooCoo
2026-10-29 10:03:12 +07:00
committed by bflatastic
parent 62b231aaa5
commit 85d7913f10
5 changed files with 102 additions and 6 deletions
+15 -2
View File
@@ -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()))));
+53 -3
View File
@@ -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": {}}));
+4 -1
View File
@@ -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();
}
});
+29
View File
@@ -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();