diff --git a/crates/engine/src/cmd/edit/props.rs b/crates/engine/src/cmd/edit/props.rs index 8724f267..8156d072 100644 --- a/crates/engine/src/cmd/edit/props.rs +++ b/crates/engine/src/cmd/edit/props.rs @@ -393,7 +393,8 @@ pub(super) fn paint_paste(s: &mut Session, p: &Value, fills: bool) -> Result Result 999 { return Err(bad(cmd, "no free image key for the copied image")); } - key = format!("{base}-{n}"); + key = format!("{base}{n}"); } doc.images.entry(key.clone()).or_insert(d); img.image = key; diff --git a/crates/engine/src/property_copy_tests.rs b/crates/engine/src/property_copy_tests.rs index 0d85901f..160dfaaf 100644 --- a/crates/engine/src/property_copy_tests.rs +++ b/crates/engine/src/property_copy_tests.rs @@ -165,6 +165,22 @@ fn a_copied_image_whose_hash_key_is_also_taken_gets_a_free_key() { assert_ne!(img.image, hash, "the taken hash is not reused"); assert_eq!(doc.images.get(&img.image), Some(&copied), "the pasted paint points at the copied data"); assert_eq!(doc.images.get(&hash), Some(&taken), "the destination's image stays"); + let key = img.image.clone(); + // An image key names an archive member, and only letters and digits are read back: the + // numbered key must bring the copied image through a save and reopen. + assert!(key.bytes().all(|b| b.is_ascii_alphanumeric()), "the key is archive-valid: {key}"); + let dir = std::env::temp_dir().join(format!("figcraft-image-key-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("pasted.figcraft").to_string_lossy().to_string(); + ex(&mut s, "file.save", json!({"path": path})); + let mut t = Session::empty(); + ex(&mut t, "file.open", json!({"path": path})); + let saved = t.doc().unwrap(); + let figcraft_color::PaintKind::Image(img) = &saved.node(b).unwrap().fills[0].kind else { panic!("the pasted fill is an image") }; + assert_eq!(img.image, key, "the reloaded paint keeps its key"); + // A reopened image's size is read from its pixels on demand, so only its bytes are compared. + assert_eq!(saved.images.get(&key).map(|i| i.bytes.as_slice()), Some(copied.bytes.as_slice()), "the copied image survives the reload"); + let _ = std::fs::remove_dir_all(&dir); } #[test] diff --git a/crates/ui-egui/src/panels/right/menu.rs b/crates/ui-egui/src/panels/right/menu.rs index 8161d20a..db3820c5 100644 --- a/crates/ui-egui/src/panels/right/menu.rs +++ b/crates/ui-egui/src/panels/right/menu.rs @@ -1,4 +1,4 @@ -use super::{BlendMode, Color32, CornerRadius, FIELD_H, Id, PAD, Rect, Sense, Tokens, Ui, a11y, pos2, regular, tr, vec2}; +use super::{BlendMode, Color32, CornerRadius, FIELD_H, FigApp, Id, NodeId, PAD, Rect, Sense, Tokens, Ui, a11y, json, pos2, regular, tr, vec2}; /// The smallest width a popup menu is measured in (see [`menu_popup`]). const MENU_MIN_W: f32 = 48.0; @@ -14,11 +14,6 @@ pub(crate) fn menu_style(s: &mut egui::Style) { s.wrap_mode = Some(egui::TextWrapMode::Extend); } -/// The menu a dropdown or button opens: as wide as its widest row and at least as wide as -/// `anchor`. egui measures a popup in a first, invisible pass as wide as the width it is given -/// (else its 600 px default area width) and keeps the size measured; rows ([`menu_row`]) take the -/// larger of that width and their content, so the menu fits its content. Chain `.width(w)` for a -/// menu at least `w` wide. /// The blend modes as menu items in groups (pass-through only for layers); the one picked. Each /// row `preview`s its mode on the canvas while hovered. pub fn blend_menu(ui: &mut Ui, with_pass_through: bool, mut preview: impl FnMut(BlendMode)) -> Option { @@ -44,6 +39,11 @@ pub(crate) fn preview_blend(app: &mut FigApp, ctx: &egui::Context, ids: &[NodeId app.preview_text(ctx, "node.set", json!({"ids": ids, "props": {"blend": blend.label()}})); } +/// The menu a dropdown or button opens: as wide as its widest row and at least as wide as +/// `anchor`. egui measures a popup in a first, invisible pass as wide as the width it is given +/// (else its 600 px default area width) and keeps the size measured; rows ([`menu_row`]) take the +/// larger of that width and their content, so the menu fits its content. Chain `.width(w)` for a +/// menu at least `w` wide. pub fn menu_popup(anchor: &egui::Response) -> egui::Popup<'static> { egui::Popup::menu(anchor).width(anchor.rect.width().max(MENU_MIN_W)).style(menu_style) }