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
@@ -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();