mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #297: the Swatches and Brushes panels take drag and drop
Art dragged off the canvas and released over the Swatches panel becomes a pattern swatch of a copy of it (object.pattern.make without entering pattern editing; the art stays), and over the Brushes panel an Art brush (brush.new, as the panel's New Brush button makes). A brush dragged out of the Brushes panel (PanelDrag::Brush, with a preview chip) is applied to the path it is dropped on (brush.apply). The art drop zone helper moves to widgets, shared with the Symbols panel. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
17e654a738
commit
7830a41b3e
6 files changed
+145
-19
No files matched your search
@@ -966,6 +966,8 @@ fn panel_drop(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, xf: &Xf)
|
||||
let add = ui.input(|i| i.modifiers.alt);
|
||||
("graphicStyle.apply", json!({"name": name, "ids": [hit.top_object(st.isolation).0], "add": add}))
|
||||
}
|
||||
// A brush from the Brushes panel: the path it lands on takes it.
|
||||
widgets::PanelDrag::Brush { name, .. } => ("brush.apply", json!({"name": name, "ids": [hit.leaf.0]})),
|
||||
// Art dragged back onto the canvas: its move was already dropped. (A symbol was placed
|
||||
// above.)
|
||||
widgets::PanelDrag::Art(_) | widgets::PanelDrag::Symbol(_) => return,
|
||||
@@ -1769,6 +1771,34 @@ mod tests {
|
||||
assert!((c - xf.to_doc(at)).hypot() < 1e-6, "{c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_brush_dropped_on_a_path_is_applied_to_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let id =
|
||||
app.session.execute("path.create", &json!({"anchors": [{"x": 100, "y": 150}, {"x": 300, "y": 150}]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let brush = |app: &VectorcraftApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.stroke().and_then(|s| s.brush.clone())
|
||||
};
|
||||
let drop = |app: &mut VectorcraftApp, at: Point| {
|
||||
let at = xf.to_screen(at);
|
||||
egui::DragAndDrop::set_payload(&ctx, widgets::PanelDrag::Brush { name: "Arrow".into(), def: json!({}) });
|
||||
let up = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() };
|
||||
frame(app, &ctx, vec![egui::Event::PointerMoved(at), up]);
|
||||
};
|
||||
// Off the path: nothing.
|
||||
drop(&mut app, Point::new(200.0, 250.0));
|
||||
assert_eq!(brush(&app), None);
|
||||
// On it: the path's stroke takes the brush.
|
||||
drop(&mut app, Point::new(200.0, 150.0));
|
||||
assert_eq!(brush(&app).as_deref(), Some("Arrow"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_drag_pans_the_view_with_any_tool() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
@@ -10,7 +10,7 @@ use vectorcraft_doc::{Appearance, Document, Node, color::Color, color::Paint};
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
use crate::widgets::{self, PanelDrag, menu_item};
|
||||
|
||||
const KINDS: [(&str, &str); 5] =
|
||||
[("calligraphic", "Calligraphic"), ("scatter", "Scatter"), ("art", "Art"), ("bristle", "Bristle"), ("pattern", "Pattern")];
|
||||
@@ -46,6 +46,15 @@ fn preview(ui: &Ui, def: &Value, size: egui::Vec2) -> Option<egui::TextureHandle
|
||||
})
|
||||
}
|
||||
|
||||
/// The stroke preview of brush definition `def` on white in `r` (the chip a dragged brush shows at
|
||||
/// the pointer).
|
||||
pub(crate) fn chip(ui: &Ui, r: egui::Rect, def: &Value) {
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
|
||||
if let Some(tex) = preview(ui, def, r.size()) {
|
||||
ui.painter().image(tex.id(), r, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), egui::Color32::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_brush(app: &VectorcraftApp) -> Option<String> {
|
||||
first_selected(app).and_then(|n| n.appearance.stroke().and_then(|s| s.brush.clone()))
|
||||
}
|
||||
@@ -59,12 +68,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let defs: HashMap<String, Value> =
|
||||
list.iter().filter_map(|(n, _)| app.run("brush.get", json!({"name": n})).ok().map(|v| (n.clone(), v))).collect();
|
||||
let mut clicked: Option<String> = None;
|
||||
widgets::list_box(ui, |ui| {
|
||||
let list_rect = widgets::list_box(ui, |ui| {
|
||||
ui.set_min_height(110.0);
|
||||
ui.set_width(ui.available_width());
|
||||
if list.is_empty() {
|
||||
super::empty_state(ui, "paintbrush", tl!("No brushes"), tl!("Select art and use New Brush to make one."));
|
||||
return;
|
||||
return ui.min_rect();
|
||||
}
|
||||
for (ty, _) in KINDS {
|
||||
if hidden.iter().any(|h| h == ty) {
|
||||
@@ -75,7 +84,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
continue;
|
||||
}
|
||||
let row = |ui: &mut Ui, name: &str, size: egui::Vec2, label: bool| -> egui::Response {
|
||||
let (r, resp) = ui.allocate_exact_size(size, Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(size, Sense::click_and_drag());
|
||||
// Dragged onto a path, the brush is applied to it.
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::Brush { name: name.to_string(), def: defs.get(name).cloned().unwrap_or_default() });
|
||||
let on = sel_brush.as_deref() == Some(name) || (sel_brush.is_none() && current.as_deref() == Some(name));
|
||||
if on {
|
||||
ui.painter().rect_filled(r, 0.0, t.row_selected);
|
||||
@@ -118,7 +129,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
ui.min_rect()
|
||||
});
|
||||
// Art dragged off the canvas and dropped on the list becomes an Art brush, as New Brush makes.
|
||||
let zone = ui.interact(list_rect, ui.id().with("brushes-drop"), Sense::hover());
|
||||
if let Some(ids) = widgets::art_drop(ui, &zone)
|
||||
&& let Err(e) = app.run("brush.new", json!({ "type": "art", "ids": ids }))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if let Some(name) = clicked {
|
||||
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
|
||||
if has_sel {
|
||||
@@ -207,3 +226,49 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
set_pstate(ui.ctx(), "br-list", true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{Pos2, Rect, pos2};
|
||||
use vectorcraft_doc::NodeId;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// One headless frame of the panel, 236 pt wide as in the dock.
|
||||
fn frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>) {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 600.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
fn button(at: Pos2, pressed: bool) -> egui::Event {
|
||||
egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_dropped_on_the_panel_becomes_an_art_brush_and_a_brush_drags_out() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let id = app.run("shape.star", json!({"cx": 50, "cy": 50, "radius1": 20, "radius2": 9, "points": 5})).unwrap()["id"].as_u64().unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
let (before, _) = brushes(&mut app);
|
||||
// Art dragged off the canvas and released over the list.
|
||||
let over = pos2(100.0, 60.0);
|
||||
egui::DragAndDrop::set_payload(&ctx, PanelDrag::Art(vec![NodeId(id)]));
|
||||
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(over), button(over, false)]);
|
||||
let (after, _) = brushes(&mut app);
|
||||
assert_eq!(after.len(), before.len() + 1);
|
||||
let new: Vec<_> = after.iter().filter(|b| !before.contains(b)).collect();
|
||||
assert!(matches!(new.as_slice(), [(_, ty)] if ty == "art"), "{new:?}");
|
||||
// Dragging the first tile out of the panel carries that brush.
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
let tile = pos2(36.0, 16.0);
|
||||
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile), button(tile, true)]);
|
||||
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile + vec2(40.0, 40.0))]);
|
||||
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile + vec2(80.0, 80.0))]);
|
||||
let drag = egui::DragAndDrop::payload::<PanelDrag>(&ctx);
|
||||
assert!(matches!(drag.as_deref(), Some(PanelDrag::Brush { name, def }) if *name == after[0].0 && def["name"] == json!(name)), "{drag:?}");
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@ fn zone_input(ui: &Ui, zone: &Response, d: &Document, ev: &mut Events) {
|
||||
PanelDrag::GraphicStyle(n) => (Some(n), None),
|
||||
PanelDrag::Art(ids) => (None, ids.first().copied()),
|
||||
PanelDrag::Appearance(id) => (None, Some(*id)),
|
||||
PanelDrag::Paint { .. } | PanelDrag::Symbol(_) => (None, None),
|
||||
PanelDrag::Paint { .. } | PanelDrag::Symbol(_) | PanelDrag::Brush { .. } => (None, None),
|
||||
};
|
||||
if style.is_none() && source.is_none() {
|
||||
return;
|
||||
|
||||
@@ -361,23 +361,28 @@ enum Drop {
|
||||
/// `target` (or between rows) becomes a swatch; with Alt held (`replace`) it replaces swatch
|
||||
/// `target` when that is of its kind.
|
||||
New { paint: Paint, target: Option<String>, replace: bool },
|
||||
/// Art dragged off the canvas becomes a pattern swatch made of a copy of it (the art stays).
|
||||
Pattern { ids: Vec<u64> },
|
||||
}
|
||||
|
||||
impl Drop {
|
||||
/// What releasing `d` before or `after` row `target` (`None`: at the end) does: move the rows
|
||||
/// it carries, or make a swatch of a paint dragged from elsewhere (appearances: nothing).
|
||||
/// it carries, make a swatch of a paint dragged from elsewhere, or a pattern swatch of art dragged
|
||||
/// off the canvas (appearances: nothing).
|
||||
fn of(d: &PanelDrag, target: Option<String>, after: bool, replace: bool) -> Option<Self> {
|
||||
match d {
|
||||
PanelDrag::Paint { rows: Some(r), .. } => Some(Drop::Move { names: r.names.clone(), target, after }),
|
||||
PanelDrag::Paint { paint, .. } => Some(Drop::New { paint: paint.clone(), target, replace }),
|
||||
PanelDrag::Art(ids) => Some(Drop::Pattern { ids: ids.iter().map(|id| id.0).collect() }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The drag held over `resp`, if the panel takes it (paints, not the Appearance panel's thumbnail).
|
||||
/// The drag held over `resp`, if the panel takes it (paints and art dragged off the canvas, not the
|
||||
/// Appearance panel's thumbnail).
|
||||
fn held(resp: &Response) -> Option<std::sync::Arc<PanelDrag>> {
|
||||
resp.dnd_hover_payload::<PanelDrag>().filter(|d| matches!(**d, PanelDrag::Paint { .. }))
|
||||
resp.dnd_hover_payload::<PanelDrag>().filter(|d| matches!(**d, PanelDrag::Paint { .. } | PanelDrag::Art(_)))
|
||||
}
|
||||
|
||||
/// The drag a tile or row of `e` starts, moving rows `names`. Colour groups have no paint of their
|
||||
@@ -545,6 +550,7 @@ fn apply_drop(app: &mut VectorcraftApp, drop: Drop) {
|
||||
}
|
||||
("swatch.new", p)
|
||||
}
|
||||
Drop::Pattern { ids } => ("object.pattern.make", json!({"ids": ids, "edit": false})),
|
||||
};
|
||||
if let Err(e) = app.run(cmd, params) {
|
||||
app.status(e);
|
||||
@@ -575,6 +581,7 @@ pub(crate) fn drag_preview(app: &VectorcraftApp, ctx: &egui::Context) {
|
||||
Some(st) => return pointer_chip(ctx, |ui, r| super::symbols::chip(ui, &st.doc, r, name)),
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::Brush { def, .. } => return pointer_chip(ctx, |ui, r| super::brushes::chip(ui, r, def)),
|
||||
};
|
||||
pointer_chip(ctx, |ui, r| {
|
||||
if registration {
|
||||
@@ -1472,6 +1479,26 @@ mod tests {
|
||||
assert_eq!((names_of(&app, None), app.session.doc().unwrap().history.undo.len()), (before, undo));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_dropped_on_the_panel_becomes_a_pattern_swatch() {
|
||||
let mut app = app();
|
||||
let id = app.run("shape.star", json!({"cx": 50, "cy": 50, "radius1": 20, "radius2": 9, "points": 5})).unwrap()["id"].as_u64().unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], 0.0, show);
|
||||
// Released over a swatch tile: the list takes it.
|
||||
let at = tile_center(&ctx, "Bright Red");
|
||||
egui::DragAndDrop::set_payload(&ctx, PanelDrag::Art(vec![vectorcraft_doc::NodeId(id)]));
|
||||
let release = Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE };
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at), release], 1.0, show);
|
||||
let patterns = app.run("pattern.list", json!({})).unwrap();
|
||||
assert_eq!(patterns["patterns"].as_array().map(Vec::len), Some(1), "{patterns}");
|
||||
assert_eq!(patterns["editing"], Value::Null, "no pattern editing mode");
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.doc.node(vectorcraft_doc::NodeId(id)), before.node(vectorcraft_doc::NodeId(id)), "the art stays as it was");
|
||||
assert!(egui::DragAndDrop::payload::<PanelDrag>(&ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_proxy_dropped_on_the_panel_becomes_a_swatch() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -59,15 +59,6 @@ pub(crate) fn chip(ui: &Ui, doc: &Document, r: egui::Rect, name: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Art dragged off the canvas over the symbol list: outline it; dropped, the art becomes a new
|
||||
/// symbol (`ids`).
|
||||
fn art_drop(ui: &Ui, zone: &egui::Response) -> Option<Vec<u64>> {
|
||||
let drag = zone.dnd_hover_payload::<PanelDrag>()?;
|
||||
let PanelDrag::Art(ids) = &*drag else { return None };
|
||||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
|
||||
zone.dnd_release_payload::<PanelDrag>().map(|_| ids.iter().map(|id| id.0).collect())
|
||||
}
|
||||
|
||||
fn current(app: &mut VectorcraftApp) -> Option<String> {
|
||||
app.run("symbol.list", json!({})).ok().and_then(|v| v["current"].as_str().map(str::to_string))
|
||||
}
|
||||
@@ -141,7 +132,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.min_rect()
|
||||
});
|
||||
let zone = ui.interact(list_rect, ui.id().with("symbols-drop"), Sense::hover());
|
||||
if let Some(ids) = art_drop(ui, &zone)
|
||||
if let Some(ids) = widgets::art_drop(ui, &zone)
|
||||
&& let Err(e) = app.run("symbol.new", json!({ "ids": ids }))
|
||||
{
|
||||
app.status(e);
|
||||
|
||||
@@ -1141,11 +1141,24 @@ pub enum PanelDrag {
|
||||
GraphicStyle(String),
|
||||
/// The selected art, dragged off the canvas with the Selection tool: the Graphic Styles panel
|
||||
/// makes a style of the first object (`graphicStyle.new`), the Symbols panel a symbol of it all
|
||||
/// (`symbol.new`).
|
||||
/// (`symbol.new`), the Swatches panel a pattern swatch of a copy (`object.pattern.make`) and the
|
||||
/// Brushes panel an Art brush (`brush.new`).
|
||||
Art(Vec<vectorcraft_doc::NodeId>),
|
||||
/// A Symbols panel symbol: the canvas places an instance of it centred where it is dropped
|
||||
/// (`symbol.place`).
|
||||
Symbol(String),
|
||||
/// A Brushes panel brush (`def`: its definition, for the chip at the pointer): the path it is
|
||||
/// dropped on takes it (`brush.apply`).
|
||||
Brush { name: String, def: serde_json::Value },
|
||||
}
|
||||
|
||||
/// A panel list `zone` that takes art dragged off the canvas: outlined while art is held over it;
|
||||
/// → the dragged ids when it is released there.
|
||||
pub fn art_drop(ui: &Ui, zone: &Response) -> Option<Vec<u64>> {
|
||||
let drag = zone.dnd_hover_payload::<PanelDrag>()?;
|
||||
let PanelDrag::Art(ids) = &*drag else { return None };
|
||||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
|
||||
zone.dnd_release_payload::<PanelDrag>().map(|_| ids.iter().map(|id| id.0).collect())
|
||||
}
|
||||
|
||||
/// The Swatches panel rows a drag from that panel moves: the swatch, None, Registration or colour
|
||||
|
||||
Reference in new issue
Block a user