diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 52d5a028c..6fd6ea3cc 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -188,7 +188,7 @@ pub fn specs() -> Vec { "Gradient", [], None, - "{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}|{gray}|[r,g,b] | swatch: name (its colour, as dropped on the Gradient panel), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), ids?} → {swatch, kind, stops} — edits the fill's gradient (an unnamed gradient unless it equals a gradient swatch)", + "{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}(0..100)|[r,g,b](0..255)|{model: rgb|cmyk|gray, …}(0..1) | swatch: name (its colour, as dropped on the Gradient panel), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), ids?} → {swatch, kind, stops} — edits the fill's gradient (an unnamed gradient unless it equals a gradient swatch)", super::has_selection, apply_gradient ), @@ -1006,10 +1006,6 @@ pub fn parse_color(v: &Value) -> Option { return Some(Color::gray(if gray > 1.0 { gray / 100.0 } else { gray } as f32)); } let g = |k: &str| o.get(k).and_then(Value::as_f64); - // {gray: 0–100} is a grey with that much black. - if let Some(k) = g("gray") { - return Some(Color::gray((k.clamp(0.0, 100.0) / 100.0) as f32)); - } let v = [g("c")?, g("m")?, g("y")?, g("k")?]; let scale = if v.iter().any(|&x| x > 1.0) { 100.0 } else { 1.0 }; Some(Color::cmyk((v[0] / scale) as f32, (v[1] / scale) as f32, (v[2] / scale) as f32, (v[3] / scale) as f32)) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 0e73e15fe..0bc3f7a82 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -468,7 +468,7 @@ fn color_json(c: &designcraft_color::Color) -> serde_json::Value { match *c { // Fractions: every component is within 0–1, so they're read back as fractions. Color::Cmyk { c, m, y, k } => json!({"c": c, "m": m, "y": y, "k": k}), - Color::Gray { k } => json!({"gray": k as f64 * 100.0}), + Color::Gray { k } => json!({"model": "gray", "k": k}), Color::Rgb { .. } => json!(c.to_hex()), } } @@ -515,6 +515,25 @@ enum RampSel { Mid(usize), } +/// A marker being dragged: what, where (0–1), off the panel (stops only). +type RampDrag = (RampSel, f32, bool); + +/// The drag kept from the last frame, if it still names a marker of a gradient with `n` stops +/// (the selection or its gradient may have changed since). +fn live_drag(d: Option, n: usize) -> Option { + d.filter(|(h, _, _)| match *h { + RampSel::Stop(i) => i < n, + RampSel::Mid(i) => i.saturating_add(1) < n, + }) +} + +/// Sets `key` of stop `i` in `object.gradient` stops; nothing if there's no such stop. +fn set_stop(stops: &mut [serde_json::Value], i: usize, key: &str, v: serde_json::Value) { + if let Some(o) = stops.get_mut(i).and_then(serde_json::Value::as_object_mut) { + o.insert(key.to_string(), v); + } +} + /// Gradient panel, laid out as InDesign's: thumbnail, Type, Location and Angle, Reverse, and the /// ramp with midpoint diamonds above it and stops below it. Drag a stop or a diamond to move it, /// click below the ramp to add a stop, drag a stop off to remove it, and drop a swatch from the @@ -575,8 +594,8 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { if let Some(v) = crate::widgets::number(ui, "grad_loc", loc.filter(|_| is_gradient), "", 50.0, 3) { let mut stops = stops_json(&g); match sel { - RampSel::Stop(i) => stops[i]["location"] = json!(v.clamp(0.0, 100.0)), - RampSel::Mid(i) => stops[i]["midpoint"] = json!(v.clamp(13.0, 87.0)), + RampSel::Stop(i) => set_stop(&mut stops, i, "location", json!(v.clamp(0.0, 100.0))), + RampSel::Mid(i) => set_stop(&mut stops, i, "midpoint", json!(v.clamp(13.0, 87.0))), } send = Some(json!({"stops": stops})); } @@ -638,17 +657,16 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { .map(|(i, _)| RampSel::Stop(i)) } }; - // A marker being dragged: what, where (0–1), off the panel (stops only). - let mut dragging: Option<(RampSel, f32, bool)> = ui.data(|d| d.get_temp(drag_id)); + let mut dragging = live_drag(ui.data(|d| d.get_temp(drag_id)), g.stops.len()); if resp.drag_started() && let Some(p) = resp.interact_pointer_pos() && let Some(h) = hit(p) + && let Some(o) = match h { + RampSel::Stop(i) => g.stops.get(i).map(|s| s.offset), + RampSel::Mid(i) => g.stops.get(i).map(|s| s.midpoint), + } { sel = h; - let o = match h { - RampSel::Stop(i) => g.stops[i].offset, - RampSel::Mid(i) => g.stops[i].midpoint, - }; dragging = Some((h, o, false)); } if resp.dragged() @@ -657,8 +675,10 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { dragging = Some(match h { RampSel::Stop(_) => (h, o_of(p.x), p.y > area.bottom() + 16.0 && g.stops.len() > 2), RampSel::Mid(i) => { - let (a, b) = (g.stops[i].offset, g.stops[i + 1].offset); - let m = if b - a > 1e-6 { ((o_of(p.x) - a) / (b - a)).clamp(0.13, 0.87) } else { 0.5 }; + let m = match (g.stops.get(i), g.stops.get(i + 1)) { + (Some(a), Some(b)) if b.offset - a.offset > 1e-6 => ((o_of(p.x) - a.offset) / (b.offset - a.offset)).clamp(0.13, 0.87), + _ => 0.5, + }; (h, m, false) } }); @@ -669,11 +689,13 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let mut stops = stops_json(&g); match h { RampSel::Stop(i) if off => { - stops.remove(i); + if i < stops.len() { + stops.remove(i); + } sel = RampSel::Stop(0); } - RampSel::Stop(i) => stops[i]["location"] = json!(o as f64 * 100.0), - RampSel::Mid(i) => stops[i]["midpoint"] = json!(o as f64 * 100.0), + RampSel::Stop(i) => set_stop(&mut stops, i, "location", json!(o as f64 * 100.0)), + RampSel::Mid(i) => set_stop(&mut stops, i, "midpoint", json!(o as f64 * 100.0)), } send = Some(json!({"stops": stops})); } @@ -710,7 +732,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let mut rgb = [(r * 255.0) as u8, (gg * 255.0) as u8, (bb * 255.0) as u8]; if egui::color_picker::color_edit_button_srgb(ui, &mut rgb).changed() { let mut stops = stops_json(&g); - stops[i]["color"] = json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])); + set_stop(&mut stops, i, "color", json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]))); send = Some(json!({"stops": stops})); } }); @@ -719,16 +741,16 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { // A swatch dropped from the Swatches panel: on a stop it colours that stop, elsewhere on // the ramp it's a new stop there. let drop_at = |p: egui::Pos2| match hit(egui::pos2(p.x, ramp.bottom() + 4.0)) { - Some(RampSel::Stop(i)) => (Some(i), g.stops[i].offset), + Some(RampSel::Stop(i)) => match g.stops.get(i) { + Some(s) => (Some(i), s.offset), + None => (None, o_of(p.x)), + }, _ => (None, o_of(p.x)), }; if resp.dnd_hover_payload::().is_some() && let Some(p) = ui.ctx().pointer_hover_pos() { - let x = match drop_at(p) { - (Some(i), _) => x_of(g.stops[i].offset), - (None, o) => x_of(o), - }; + let x = x_of(drop_at(p).1); painter.line_segment([egui::pos2(x, ramp.top() - 3.0), egui::pos2(x, ramp.bottom() + 14.0)], egui::Stroke::new(2.0, t.accent)); } if let Some(sw) = resp.dnd_release_payload::() @@ -737,8 +759,8 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let mut stops = stops_json(&g); match drop_at(p) { (Some(i), _) => { - stops[i]["swatch"] = json!(sw.0); - if let Some(o) = stops[i].as_object_mut() { + if let Some(o) = stops.get_mut(i).and_then(serde_json::Value::as_object_mut) { + o.insert("swatch".to_string(), json!(sw.0)); o.remove("color"); } sel = RampSel::Stop(i); @@ -753,7 +775,10 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { // Diamonds. for i in 0..last { let x = match dragging { - Some((RampSel::Mid(j), m, _)) if j == i => x_of(g.stops[i].offset + (g.stops[i + 1].offset - g.stops[i].offset) * m), + Some((RampSel::Mid(j), m, _)) if j == i => match (g.stops.get(i), g.stops.get(i + 1)) { + (Some(a), Some(b)) => x_of(a.offset + (b.offset - a.offset) * m), + _ => ramp.left(), + }, _ => mid_x(i).unwrap_or(ramp.left()), }; let (cy, r) = (ramp.top() - 6.0, 4.0); @@ -789,7 +814,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) { Some(v) => { d.insert_temp(drag_id, v); } - None => d.remove::<(RampSel, f32, bool)>(drag_id), + None => d.remove::(drag_id), } }); }); @@ -822,8 +847,11 @@ mod gradient_panel_tests { #[test] fn panel_edits_keep_cmyk_and_grey_stops() { let mut app = app_with_frame(); - app.run("object.gradient", json!({"stops": [{"location": 0, "swatch": "C=0 M=100 Y=0 K=0"}, {"location": 100, "color": {"gray": 60}}]})) - .unwrap(); + app.run( + "object.gradient", + json!({"stops": [{"location": 0, "swatch": "C=0 M=100 Y=0 K=0"}, {"location": 50, "color": {"gray": 60}}, {"location": 100, "color": {"model": "gray", "k": 0.01}}]}), + ) + .unwrap(); let g = gradient(&app).unwrap(); // What the panel sends when a stop moves: the colours come back exactly. let mut stops = stops_json(&g); @@ -833,6 +861,20 @@ mod gradient_panel_tests { assert_eq!(g.stops[0].color, Color::cmyk(0.0, 1.0, 0.0, 0.0)); assert_eq!(g.stops[1].color, Color::gray(0.6)); assert!((g.stops[1].offset - 0.8).abs() < 1e-6); + assert_eq!(g.stops[2].color, Color::gray(0.01), "a 1 % grey stays 1 %"); + } + + #[test] + fn a_drag_kept_from_a_longer_gradient_is_dropped() { + let d = |h| Some((h, 0.5, false)); + assert_eq!(live_drag(d(RampSel::Stop(1)), 2), d(RampSel::Stop(1))); + assert_eq!(live_drag(d(RampSel::Stop(2)), 2), None); + assert_eq!(live_drag(d(RampSel::Mid(0)), 2), d(RampSel::Mid(0))); + assert_eq!(live_drag(d(RampSel::Mid(1)), 2), None); + assert_eq!(live_drag(d(RampSel::Mid(usize::MAX)), 2), None); + let mut stops = vec![json!({"location": 0})]; + set_stop(&mut stops, 3, "location", json!(50)); + assert_eq!(stops, vec![json!({"location": 0})]); } #[test]