mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
M7: preserve bindings and design layers when pasting images
This commit is contained in:
@@ -377,6 +377,13 @@ pub fn accepts_image_fill(kind: &NodeKind) -> bool {
|
||||
kind.is_shape() || kind.is_frame_like() || matches!(kind, NodeKind::Text(_) | NodeKind::BooleanOp { .. })
|
||||
}
|
||||
|
||||
/// Replace the fill list and detach variable bindings on paints that the image replaces.
|
||||
pub(crate) fn replace_with_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) {
|
||||
node.fills = vec![Paint::image(image, mode)];
|
||||
node.bound.retain(|key, _| !matches!(super::lists::list_binding(key), Some((super::lists::BoundList::Fills, _, _))));
|
||||
node.styles.fill = None;
|
||||
}
|
||||
|
||||
/// Place an image as a rectangle with an image fill (Figma's image layer).
|
||||
pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "image.place";
|
||||
@@ -407,8 +414,7 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (hash, _, _) = figcraft_format::add_image(doc, bytes).map_err(|e| bad(cmd, e.to_string()))?;
|
||||
let profile = figcraft_render::images::profile_name(&hash, doc.images.get(&hash));
|
||||
let node = doc.get_mut(id)?;
|
||||
node.fills = vec![Paint::image(hash.clone(), mode)];
|
||||
node.styles.fill = None;
|
||||
replace_with_image_fill(node, hash.clone(), mode);
|
||||
return Ok(json!({"id": id, "image": hash, "width": node.width, "height": node.height, "imageProfile": profile}));
|
||||
}
|
||||
let page = s.page()?;
|
||||
|
||||
@@ -701,8 +701,7 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let doc = s.doc_mut()?;
|
||||
doc.images.entry(hash.clone()).or_insert(data);
|
||||
let node = doc.get_mut(*target)?;
|
||||
node.fills = vec![figcraft_color::Paint::image(hash, figcraft_color::ImageScaleMode::Fill)];
|
||||
node.styles.fill = None;
|
||||
super::create::replace_with_image_fill(node, hash, figcraft_color::ImageScaleMode::Fill);
|
||||
return Ok(json!({"ids": [target]}));
|
||||
}
|
||||
let page = s.page()?;
|
||||
|
||||
@@ -45,6 +45,34 @@ fn paste_takes_layers_copied_in_another_design_app() {
|
||||
assert_eq!(selected_names(&app), ["Copied layer"]);
|
||||
}
|
||||
|
||||
struct HtmlAndImage(String);
|
||||
|
||||
impl Services for HtmlAndImage {
|
||||
fn clipboard_get_html(&mut self) -> Option<String> {
|
||||
Some(self.0.clone())
|
||||
}
|
||||
fn clipboard_get_image(&mut self) -> Option<(u32, u32, Vec<u8>)> {
|
||||
Some((2, 2, vec![255; 16]))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editable_design_layers_take_priority_over_a_png_representation() {
|
||||
let mut session = Session::new();
|
||||
let selected = session.execute("shape.ellipse", &json!({"x": 100, "y": 100, "width": 80, "height": 80})).unwrap()["id"].as_u64().unwrap();
|
||||
let mut app = FigApp::new(session, Box::new(HtmlAndImage(copied_html())));
|
||||
let before = app.session.doc().unwrap().node_count();
|
||||
|
||||
crate::menus::paste(&mut app, None, None);
|
||||
|
||||
assert_eq!(selected_names(&app), ["Copied layer"]);
|
||||
assert_eq!(app.session.doc().unwrap().node_count(), before + 1);
|
||||
assert!(matches!(
|
||||
app.session.doc().unwrap().node(figcraft_doc::NodeId(selected)).unwrap().fills[0].kind,
|
||||
figcraft_color::PaintKind::Solid { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_here_puts_copied_design_layers_at_the_point() {
|
||||
let html = copied_html();
|
||||
|
||||
+16
-16
@@ -36,22 +36,6 @@ pub const UI_COMMANDS: &[&str] = &[
|
||||
/// other way to read it). `at`: the page point to paste at (Paste here); `None` pastes in the
|
||||
/// middle of the view.
|
||||
pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) {
|
||||
let selected_layer = match app.session.selection().as_slice() {
|
||||
[id] if at.is_none()
|
||||
&& app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) =>
|
||||
{
|
||||
Some(*id)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(target) = selected_layer
|
||||
&& let Some((png, _, _)) = clipboard_png(app)
|
||||
{
|
||||
use base64::Engine as _;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(png);
|
||||
app.run("image.place", json!({"data": b64, "target": target}));
|
||||
return;
|
||||
}
|
||||
// Only HTML that holds readable design data takes this path; other HTML pastes as before.
|
||||
if let Some(html) = app.services.clipboard_get_html().filter(|h| figcraft_engine::cmd::edit::design_clipboard_buffer(h).is_some()) {
|
||||
let mut params = json!({"html": html});
|
||||
@@ -69,6 +53,22 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>)
|
||||
}
|
||||
return;
|
||||
}
|
||||
let selected_layer = match app.session.selection().as_slice() {
|
||||
[id] if at.is_none()
|
||||
&& app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) =>
|
||||
{
|
||||
Some(*id)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(target) = selected_layer
|
||||
&& let Some((png, _, _)) = clipboard_png(app)
|
||||
{
|
||||
use base64::Engine as _;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(png);
|
||||
app.run("image.place", json!({"data": b64, "target": target}));
|
||||
return;
|
||||
}
|
||||
let sys_text = app.services.clipboard_get_text().or_else(|| event_text.map(str::to_string)).filter(|t| !t.is_empty());
|
||||
let ours = sys_text.is_some() && sys_text == app.ui.last_copy;
|
||||
let (cx, cy) = at.or_else(|| app.session.ds().ok().map(|d| (d.view.cx, d.view.cy))).unwrap_or((0.0, 0.0));
|
||||
|
||||
@@ -208,6 +208,23 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() {
|
||||
assert_eq!(w.app.session.doc().unwrap().node(oval).unwrap().fills, original_fill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pasting_an_image_detaches_the_replaced_fill_variable() {
|
||||
let mut w = Win::new();
|
||||
let oval =
|
||||
NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap());
|
||||
w.app.session.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap();
|
||||
w.app.session.execute("variable.create", &json!({"collection": "Theme", "name": "brand", "type": "color", "value": "#FF0000"})).unwrap();
|
||||
w.app.session.execute("variable.bind", &json!({"ids": [oval.0], "field": "fill", "variable": "brand"})).unwrap();
|
||||
assert!(!w.app.session.doc().unwrap().node(oval).unwrap().bound.is_empty());
|
||||
|
||||
w.copy_image_elsewhere();
|
||||
w.cmd_v(CMD);
|
||||
let node = w.app.session.doc().unwrap().node(oval).unwrap();
|
||||
assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Image(_)));
|
||||
assert!(node.bound.is_empty(), "the old colour variable must not remain on the image fill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() {
|
||||
let mut w = Win::new();
|
||||
|
||||
Reference in New Issue
Block a user