fix: pasted image keys stay archive-valid when the content hash is taken

The blend menu and its hover preview now live beside upstream's split menu module.
This commit is contained in:
Nicek
2026-10-29 09:46:50 +08:00
committed by trae-solo-agent
parent fae356fd50
commit 1c253ef30d
3 changed files with 25 additions and 8 deletions
+3 -2
View File
@@ -393,7 +393,8 @@ pub(super) fn paint_paste(s: &mut Session, p: &Value, fills: bool) -> Result<Val
// The destination may already use that key for different data (other layers keep
// that image): the pasted paint takes the copied data's content hash instead. That key
// can be taken too — images are usually keyed by their hash, so the copied key often
// is that hash — and then a numbered variant of it.
// is that hash — and then a numbered variant of it. Image keys become archive member
// names, which keep only letters and digits, so the number is appended plainly.
let base = if doc.images.get(&copied).is_some_and(|existing| existing != &d) { figcraft_format::image_hash(&d.bytes) } else { copied };
let mut key = base.clone();
let mut n = 0u32;
@@ -402,7 +403,7 @@ pub(super) fn paint_paste(s: &mut Session, p: &Value, fills: bool) -> Result<Val
if n > 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;
+16
View File
@@ -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]
+6 -6
View File
@@ -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<BlendMode> {
@@ -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)
}