Merge pull request #3492 from nrq6993-a11y/stream/vc-feat1-g11-proxy-paint-drop-3467

G11: Copy fill/stroke by dragging between proxies (#3467)
This commit is contained in:
BFlat
2026-10-22 07:38:52 +07:00
committed by GitHub
2 changed files with 67 additions and 1 deletions
+43
View File
@@ -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<Event>| {
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::<PanelDrag>(&ctx).is_none());
}
#[test]
fn paint_params_roundtrip_shapes() {
assert_eq!(paint_params(&Paint::None), json!({"none": true}));
+24 -1
View File
@@ -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<bool>,
/// A paint dragged from the other proxy (or a swatch) dropped on the fill square.
pub dropped_fill: Option<serde_json::Value>,
/// A paint dropped on the stroke square.
pub dropped_stroke: Option<serde_json::Value>,
}
/// 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<serde_json::Value> {
if let Some(drag) = resp.dnd_hover_payload::<PanelDrag>()
&& 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::<PanelDrag>().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<usize> {
dropdown_with(ui, id, current, options, width, |_| true)