diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 048774c88..566db5efc 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -382,6 +382,12 @@ pub(crate) fn proxy(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) { if let Some(stroke) = c.pick { app.run("ui.colorPicker", json!({ "stroke": stroke })).ok(); } + if let Some(p) = c.dropped_fill { + app.run("paint.setFill", p).ok(); + } + if let Some(p) = c.dropped_stroke { + app.run("paint.setStroke", p).ok(); + } } /// The Fill (or `stroke`) chip of the Control bar and the Properties panel, `size` square (with a @@ -538,6 +544,43 @@ pub(crate) fn empty_state(ui: &mut Ui, icon: &str, title: &str, body: &str) { mod tests { use super::*; + #[test] + fn dropping_a_proxy_paint_on_the_other_proxy_applies_it() { + use egui::{Event, Modifiers, PointerButton}; + use vectorcraft_color::{Color, Paint}; + + use crate::widgets::PanelDrag; + + let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap(); + app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap(); + app.run("paint.setStroke", json!({"color": "#0000ff"})).unwrap(); + let ctx = egui::Context::default(); + let stroke_at = std::cell::Cell::new(egui::Pos2::ZERO); + let frame = |app: &mut crate::VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { + proxy(app, ui, 36.0); + if let Some(c) = ctx.read_response(ui.id().with("stroke-proxy")).map(|r| r.rect.center()) { + stroke_at.set(c); + } + }); + out.textures_delta.clear(); + }; + frame(&mut app, vec![]); + let at = stroke_at.get(); + let red = PanelDrag::paint(Paint::solid(Color::from_hex("#ff0000").unwrap())); + egui::DragAndDrop::set_payload(&ctx, red); + let release = Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }; + frame(&mut app, vec![Event::PointerMoved(at), release]); + let stroke = app.session.shown_stroke().unwrap(); + assert_eq!(stroke.paint.color().map(|c| c.to_hex()), Some("#ff0000".into()), "{}", app.ui.status); + let st = app.session.active().unwrap(); + let id = st.selection.objects[0]; + assert_eq!(st.doc.node(id).unwrap().appearance.fill_paint().color().map(|c| c.to_hex()), Some("#ff0000".into())); + assert!(egui::DragAndDrop::payload::(&ctx).is_none()); + } + #[test] fn paint_params_roundtrip_shapes() { assert_eq!(paint_params(&Paint::None), json!({"none": true})); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 065669080..f62f105c0 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -839,7 +839,7 @@ pub fn chip_button_named(ui: &mut Ui, size: f32, chevron: bool, tip: &str, draw: } /// What the user did on a [`fill_stroke_proxy`]. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ProxyClicks { pub fill: bool, pub stroke: bool, @@ -847,6 +847,10 @@ pub struct ProxyClicks { pub default: bool, /// Double-clicked a square: open the Color Picker for it (`true` = the stroke). pub pick: Option, + /// A paint dragged from the other proxy (or a swatch) dropped on the fill square. + pub dropped_fill: Option, + /// A paint dropped on the stroke square. + pub dropped_stroke: Option, } /// The Fill/Stroke proxy pair (two overlapping squares). `mixed` (fill, stroke) draws a "?" square @@ -859,6 +863,8 @@ pub fn fill_stroke_proxy(ui: &mut Ui, fill: &Paint, stroke: &Paint, mixed: (bool let stroke_r = Rect::from_min_size(rect.max - Vec2::splat(s), Vec2::splat(s)); let stroke_resp = ui.interact(stroke_r, ui.id().with("stroke-proxy"), Sense::click_and_drag()); let fill_resp = ui.interact(fill_r, ui.id().with("fill-proxy"), Sense::click_and_drag()); + let dropped_fill = if mixed.0 { None } else { proxy_paint_drop(ui, &fill_resp) }; + let dropped_stroke = if mixed.1 { None } else { proxy_paint_drop(ui, &stroke_resp) }; // A proxy drags its paint (onto the Swatches panel or art), unless it shows "?". for (resp, paint, mixed) in [(&fill_resp, fill, mixed.0), (&stroke_resp, stroke, mixed.1)] { if !mixed { @@ -915,9 +921,26 @@ pub fn fill_stroke_proxy(ui: &mut Ui, fill: &Paint, stroke: &Paint, mixed: (bool swap: swap.on_hover_text(tl!("Swap Fill and Stroke (Shift+X)")).clicked(), default: def.on_hover_text(tl!("Default Fill and Stroke (D)")).clicked(), pick, + dropped_fill, + dropped_stroke, } } +/// A [`PanelDrag::Paint`] released on a Fill/Stroke proxy square: highlight while held over it. +fn proxy_paint_drop(ui: &Ui, resp: &Response) -> Option { + if let Some(drag) = resp.dnd_hover_payload::() + && let PanelDrag::Paint { params, .. } = &*drag + && !params.is_null() + { + let t = Tokens::get(ui.ctx()); + ui.painter().rect_stroke(resp.rect, 0.0, Stroke::new(2.0, t.accent), StrokeKind::Outside); + } + resp.dnd_release_payload::().and_then(|d| { + let PanelDrag::Paint { params, .. } = &*d else { return None }; + (!params.is_null()).then_some(params.clone()) + }) +} + /// A compact dropdown of interface labels (shown in the UI language). Returns the chosen index. pub fn dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: &str, options: &[&str], width: f32) -> Option { dropdown_with(ui, id, current, options, width, |_| true)