From d080a5635a7be3376b129551dd814b11348a23ea Mon Sep 17 00:00:00 2001 From: PiMinusMax <121135695+PiMinusMax@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:13:31 +0200 Subject: [PATCH] Swatches panel: a gradient dropped on a colour group row is kept too The drop zone held the ungrouped swatches only, so releasing the gradient over a colour group's row did nothing. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/swatches.rs | 130 +++++++++++++++----------- 1 file changed, 76 insertions(+), 54 deletions(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index e5e9a934c..878904a5f 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -18,11 +18,57 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { // Unnamed colours (mixed in the Color panel) aren't listed until Add to Swatches; swatches in // colour groups are listed under their folder. let grouped: Vec<&str> = doc.color_groups.iter().flat_map(|g| g.swatches.iter().map(String::as_str)).collect(); - // A gradient dragged from the Gradient panel's thumbnail is dropped on the list. + // A gradient dragged from the Gradient panel's thumbnail is dropped on the list, colour + // groups included. let (_, dropped) = ui.dnd_drop_zone::(egui::Frame::NONE, |ui| { for sw in doc.swatches.iter().filter(|w| !w.hidden && !grouped.contains(&w.name.as_str())) { swatch_row(app, ui, &doc, sw, 0.0, fill_cur.as_deref()); } + for g in &doc.color_groups { + let open_id = egui::Id::new(("color_group_open", &g.name)); + let mut open: bool = ui.data(|d| d.get_temp(open_id)).unwrap_or(true); + let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &g.name)); + if resp.hovered() { + ui.painter().rect_filled(row, 0.0, t.hover); + } + icons::paint( + ui.painter(), + egui::Rect::from_min_size(row.min + vec2(2.0, 3.0), vec2(16.0, 16.0)), + if open { "chevron-down" } else { "chevron-right" }, + t.text_dim, + ); + // A folder. + let f = egui::Rect::from_min_size(row.min + vec2(20.0, 5.0), vec2(16.0, 12.0)); + ui.painter().rect_stroke(f, 1.5, egui::Stroke::new(1.2, t.text_dim), egui::StrokeKind::Inside); + ui.painter().line_segment([f.left_top() + vec2(1.0, 3.0), f.right_top() + vec2(-1.0, 3.0)], egui::Stroke::new(1.0, t.text_dim)); + ui.painter().text(row.min + vec2(42.0, 11.0), egui::Align2::LEFT_CENTER, &g.name, egui::FontId::proportional(12.5), t.text); + // A double click's first click folded the group: it stays as it was. + if resp.clicked() { + open = !open; + ui.data_mut(|d| d.insert_temp(open_id, open)); + } + if resp.double_clicked() { + let _ = app.run("app.colorGroupOptionsDialog", json!({"name": g.name})); + } + resp.context_menu(|ui| { + if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Color Group Options…"))).clicked() { + let _ = app.run("app.colorGroupOptionsDialog", json!({"name": g.name})); + ui.close(); + } + if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ungroup Color Group"))).clicked() { + let _ = app.run("swatch.ungroupColorGroup", json!({"name": g.name})); + ui.close(); + } + }); + if open { + for n in &g.swatches { + if let Some(sw) = doc.swatches.iter().find(|w| w.name == *n) { + swatch_row(app, ui, &doc, sw, 16.0, fill_cur.as_deref()); + } + } + } + } }); // An unnamed gradient becomes "New Gradient Swatch"; one that is a swatch already stays one. if let Some(g) = dropped @@ -30,51 +76,6 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { { let _ = app.run("swatch.new", json!({"swatch": g.0})); } - for g in &doc.color_groups { - let open_id = egui::Id::new(("color_group_open", &g.name)); - let mut open: bool = ui.data(|d| d.get_temp(open_id)).unwrap_or(true); - let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &g.name)); - if resp.hovered() { - ui.painter().rect_filled(row, 0.0, t.hover); - } - icons::paint( - ui.painter(), - egui::Rect::from_min_size(row.min + vec2(2.0, 3.0), vec2(16.0, 16.0)), - if open { "chevron-down" } else { "chevron-right" }, - t.text_dim, - ); - // A folder. - let f = egui::Rect::from_min_size(row.min + vec2(20.0, 5.0), vec2(16.0, 12.0)); - ui.painter().rect_stroke(f, 1.5, egui::Stroke::new(1.2, t.text_dim), egui::StrokeKind::Inside); - ui.painter().line_segment([f.left_top() + vec2(1.0, 3.0), f.right_top() + vec2(-1.0, 3.0)], egui::Stroke::new(1.0, t.text_dim)); - ui.painter().text(row.min + vec2(42.0, 11.0), egui::Align2::LEFT_CENTER, &g.name, egui::FontId::proportional(12.5), t.text); - // A double click's first click folded the group: it stays as it was. - if resp.clicked() { - open = !open; - ui.data_mut(|d| d.insert_temp(open_id, open)); - } - if resp.double_clicked() { - let _ = app.run("app.colorGroupOptionsDialog", json!({"name": g.name})); - } - resp.context_menu(|ui| { - if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Color Group Options…"))).clicked() { - let _ = app.run("app.colorGroupOptionsDialog", json!({"name": g.name})); - ui.close(); - } - if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ungroup Color Group"))).clicked() { - let _ = app.run("swatch.ungroupColorGroup", json!({"name": g.name})); - ui.close(); - } - }); - if open { - for n in &g.swatches { - if let Some(sw) = doc.swatches.iter().find(|w| w.name == *n) { - swatch_row(app, ui, &doc, sw, 16.0, fill_cur.as_deref()); - } - } - } - } ui.add_space(6.0); // InDesign's bottom row: New Color Group, New Swatch, Delete Swatch (the one last clicked). let picked: Option = ui.data(|d| d.get_temp(egui::Id::new(PICKED))).filter(|n: &String| doc.swatch(n).is_some_and(|w| !w.locked)); @@ -1054,7 +1055,6 @@ mod gradient_panel_tests { /// which keeps it as a gradient swatch. #[test] fn a_gradient_dragged_onto_the_swatches_becomes_a_swatch() { - use egui::{Event, Modifiers, PointerButton}; let mut app = app_with_frame(); app.run("object.gradient", json!({})).unwrap(); app.run("window.panel.float", json!({"panel": "gradient", "x": 300, "y": 120})).unwrap(); @@ -1064,10 +1064,23 @@ mod gradient_panel_tests { let before = named(&app); let mut h = crate::test_window::open(app, vec2(1400.0, 900.0)); h.run_steps(6); + drag_gradient_to(&mut h, "[Black]"); + let after = named(&h.state().app); + assert_eq!(after.len(), before.len() + 1); + assert_eq!(after.last().map(String::as_str), Some("New Gradient Swatch")); + let d = &h.state().app.session.doc().unwrap().doc; + assert!(matches!(d.swatch("New Gradient Swatch").map(|w| &w.value), Some(SwatchValue::Gradient { .. }))); + let id = h.state().app.session.doc().unwrap().selection.items[0]; + assert_eq!(d.item(id).unwrap().fill.swatch, "New Gradient Swatch", "the object keeps it, now by name"); + } + + /// Drag the Gradient panel's thumbnail onto the Swatches panel row labelled `label`. + fn drag_gradient_to(h: &mut egui_kittest::Harness<'static, crate::test_window::Window>, label: &str) { + use egui::{Event, Modifiers, PointerButton}; // The thumbnail sits left of the Type row. let ty = h.get_by_label("Type:").rect(); let from = egui::pos2(ty.min.x - 21.0, ty.min.y + 12.0); - let to = h.get_by_label("[Black]").rect().center(); + let to = h.get_by_label(label).rect().center(); h.event(Event::PointerMoved(from)); h.step(); h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); @@ -1078,13 +1091,22 @@ mod gradient_panel_tests { } h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); h.run_steps(4); - let after = named(&h.state().app); - assert_eq!(after.len(), before.len() + 1); - assert_eq!(after.last().map(String::as_str), Some("New Gradient Swatch")); + } + + /// A colour group's row is part of the list too: a gradient dropped on it becomes a swatch. + #[test] + fn a_gradient_dropped_on_a_colour_group_becomes_a_swatch() { + let mut app = app_with_frame(); + app.run("object.gradient", json!({})).unwrap(); + let group = app.run("swatch.newColorGroup", json!({})).unwrap()["name"].as_str().unwrap().to_string(); + app.run("window.panel.float", json!({"panel": "gradient", "x": 300, "y": 120})).unwrap(); + // Tall enough to show the group's row below the swatches. + app.run("window.panel.float", json!({"panel": "swatches", "x": 620, "y": 120, "height": 700})).unwrap(); + let mut h = crate::test_window::open(app, vec2(1400.0, 900.0)); + h.run_steps(6); + drag_gradient_to(&mut h, &group); let d = &h.state().app.session.doc().unwrap().doc; - assert!(matches!(d.swatch("New Gradient Swatch").map(|w| &w.value), Some(SwatchValue::Gradient { .. }))); - let id = h.state().app.session.doc().unwrap().selection.items[0]; - assert_eq!(d.item(id).unwrap().fill.swatch, "New Gradient Swatch", "the object keeps it, now by name"); + assert!(matches!(d.swatch("New Gradient Swatch").map(|w| &w.value), Some(SwatchValue::Gradient { .. })), "dropped on the group's row"); } }