collective

Browse / github.com/storytold/designcraft / patch

Gradient panel: choose the selected stop's colour in the panel

Gradient stops can't be coloured when the Gradient and Swatches panels share the flyout ### What happens In the Essentials workspace the Gradient and Swatches panels open as flyouts from the icon column, and only one flyout is open at a time. The only way to give a gradient stop a colour is to drag a swatch from the Swatches panel onto the ramp, but opening Swatches closes Gradient, so there is nothing to drop on. Right-clicking a stop offers an RGB picker only, and only once the fill already is a gradient. Floating both panels works, but the float button is easy to miss. ### Steps 1. Select a frame. 2. Open the Gradient panel from the icon column and click the thumbnail to st

Status
open
Score
0.00
Adopters
0 other operators
Author
PiMinusMax (imported from pull request #1533, open)
Base
ed8aabd1dc5a88652a076631c2d46886ca60c031
Size
+91 −0 in 1 file
Review
pending
Shared
2026-10-28
Id
ef149f318c4dddab1102506f

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/panels/swatches.rs

diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs--- a/crates/ui-egui/src/panels/swatches.rs+++ b/crates/ui-egui/src/panels/swatches.rs@@ -980,6 +980,57 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {             painter.rect_filled(sq, 0.0, color32(&s.color, 1.0));             painter.rect_stroke(sq, 0.0, egui::Stroke::new(if on { 1.5 } else { 1.0 }, edge), egui::StrokeKind::Inside);         }+        // The selected stop's colour, chosen here: in a workspace where the Swatches panel and+        // this one share the flyout, dragging a swatch onto the ramp is impossible.+        if let RampSel::Stop(i) = sel+            && let Some(s) = g.stops.get(i)+        {+            ui.add_space(4.0);+            ui.horizontal(|ui| {+                crate::widgets::caption(ui, crate::i18n::tr(&lang, "Stop Color:"));+                let colors: Vec<(&str, designcraft_color::Color)> = st+                    .doc+                    .swatches+                    .iter()+                    .filter(|w| !w.hidden)+                    // Every swatch a stop can take: colours, tints and [Paper] (not [None],+                    // [Registration] or gradients), with the colour it resolves to.+                    .filter(|w| matches!(w.value, SwatchValue::Color { .. } | SwatchValue::Tint { .. } | SwatchValue::Paper { .. }))+                    .filter_map(|w| st.doc.resolve_color(&w.name, 1.0).map(|c| (w.name.as_str(), c)))+                    .collect();+                let current = colors.iter().find(|(_, c)| *c == s.color).map_or(crate::i18n::tr(&lang, "Custom"), |(n, _)| *n);+                let combo =+                    egui::ComboBox::from_id_salt("grad_stop_swatch").selected_text(crate::rtl::widget(ui, current)).width(120.0).show_ui(ui, |ui| {+                        for (name, c) in &colors {+                            let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width().max(140.0), 20.0), Sense::click());+                            resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, *c == s.color, *name));+                            if resp.hovered() {+                                ui.painter().rect_filled(r, 0.0, t.hover);+                            }+                            let chip = egui::Rect::from_min_size(r.min + vec2(4.0, 3.0), vec2(14.0, 14.0));+                            ui.painter().rect_filled(chip, 0.0, color32(c, 1.0));+                            ui.painter().rect_stroke(chip, 0.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);+                            ui.painter().text(r.min + vec2(24.0, 10.0), egui::Align2::LEFT_CENTER, *name, egui::FontId::proportional(12.5), t.text);+                            if resp.clicked() {+                                let mut stops = stops_json(&g);+                                if let Some(o) = stops.get_mut(i).and_then(serde_json::Value::as_object_mut) {+                                    o.insert("swatch".to_string(), json!(name));+                                    o.remove("color");+                                }+                                send = Some(json!({"stops": stops}));+                            }+                        }+                    });+                let _ = crate::widgets::name_combo(combo, crate::i18n::tr(&lang, "Color"));+                let [r, gg, bb] = s.color.to_rgb();+                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).on_hover_text(crate::i18n::tr(&lang, "Stop Color:")).changed() {+                    let mut stops = stops_json(&g);+                    set_stop(&mut stops, i, "color", json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])));+                    send = Some(json!({"stops": stops}));+                }+            });+        }         ui.data_mut(|d| {             d.insert_temp(sel_id, sel);             match dragging {@@ -1019,6 +1070,46 @@ mod gradient_panel_tests {         designcraft_color::swatch::resolve_gradient(&d.swatches, &d.item(id).unwrap().fill.swatch).cloned()     } +    /// In a workspace where the Gradient and Swatches panels share one flyout no swatch can be+    /// dragged onto the ramp: the selected stop takes a colour swatch from the Stop Color list,+    /// and choosing one with no gradient yet starts it.+    #[test]+    fn the_stop_color_list_colours_the_selected_stop() {+        let mut app = app_with_frame();+        app.ui.open_panel = Some("gradient".into());+        let mut h = crate::test_window::open(app, vec2(1200.0, 800.0));+        h.run_steps(4);+        assert!(gradient(&h.state().app).is_none(), "no gradient yet");+        // The list beside the Stop Color caption (accessibility trees name a combo box by its+        // label or its value, depending on the platform).+        let stop_list = |h: &Harness<'static, crate::test_window::Window>| {+            let caption = h.get_by_label("Stop Color:").rect();+            h.query_all_by(|n| n.role() == egui::accesskit::Role::ComboBox)+                .map(|n| n.rect())+                .find(|r| (r.center().y - caption.center().y).abs() < 8.0 && r.left() > caption.right())+                .expect("the Stop Color list")+        };+        let at = stop_list(&h).center();+        crate::test_window::click_at(&mut h, at);+        h.get_by_label("C=100 M=0 Y=0 K=0").click();+        h.run_steps(4);+        let g = gradient(&h.state().app).expect("choosing a stop colour starts the gradient");+        assert_eq!(g.stops[0].color, Color::cmyk(1.0, 0.0, 0.0, 0.0), "the first stop is chosen at first");+        assert_eq!(g.stops.len(), 2);+        let at = stop_list(&h).center();+        crate::test_window::click_at(&mut h, at);+        h.get_by_label("C=0 M=100 Y=0 K=0").click();+        h.run_steps(4);+        assert_eq!(gradient(&h.state().app).unwrap().stops[0].color, Color::cmyk(0.0, 1.0, 0.0, 0.0));+        // [Paper] and tints are offered too.+        let at = stop_list(&h).center();+        crate::test_window::click_at(&mut h, at);+        h.get_by_label("[Paper]").click();+        h.run_steps(4);+        let paper = h.state().app.session.doc().unwrap().doc.resolve_color("[Paper]", 1.0).unwrap();+        assert_eq!(gradient(&h.state().app).unwrap().stops[0].color, paper);+    }+     #[test]     fn panel_edits_keep_cmyk_and_grey_stops() {         let mut app = app_with_frame();