Merge pull request #1207 from xc4pr/fix/1201-ideas-band-reset

This commit is contained in:
Brandon Thomas
2026-10-23 17:10:11 -04:00
+102 -2
View File
@@ -84,6 +84,22 @@ fn cover_crop(src: f64, dst: f64) -> [f64; 4] {
} }
} }
/// The light fill behind the title in the full-bleed idea. Its opacity is an `alpha` transform
/// (about 220/255), as PPTX stores it, so the fill compares equal after a save and reopen.
fn band_fill() -> Fill {
Fill::solid(deckcraft_model::ColorRef::rgb(Rgba::rgb(255, 255, 255)).with(deckcraft_model::ColorTransform::Alpha(86274)))
}
/// The band as released versions up to 0.10 stored it (opacity in the colour's own alpha).
fn legacy_band_fill() -> Fill {
Fill::solid(deckcraft_model::ColorRef::rgb(Rgba::rgba(255, 255, 255, 220)))
}
/// Whether `f` is a Full-bleed band, in the current or the legacy form.
fn is_band(f: &Fill) -> bool {
*f == band_fill() || *f == legacy_band_fill()
}
/// Apply idea `id` to slide `index` of `doc` (`sel` resolves inherited placeholder boxes). /// Apply idea `id` to slide `index` of `doc` (`sel` resolves inherited placeholder boxes).
pub fn apply_idea(doc: &mut Presentation, sel: &Selection, index: usize, id: &str) -> Result<()> { pub fn apply_idea(doc: &mut Presentation, sel: &Selection, index: usize, id: &str) -> Result<()> {
if !ideas_for(doc, index).iter().any(|i| i.0 == id) { if !ideas_for(doc, index).iter().any(|i| i.0 == id) {
@@ -180,8 +196,13 @@ pub fn apply_idea(doc: &mut Presentation, sel: &Selection, index: usize, id: &st
let pic = slide.shapes.remove(p); let pic = slide.shapes.remove(p);
slide.shapes.insert(0, pic); slide.shapes.insert(0, pic);
} }
if band && let Some(t) = slide.shapes.iter_mut().find(|s| s.ph.as_ref().is_some_and(|p| p.kind.is_title())) { if let Some(t) = slide.shapes.iter_mut().find(|s| !s.hidden && s.ph.as_ref().is_some_and(|p| p.kind.is_title())) {
t.fill = Some(Fill::solid(deckcraft_model::ColorRef::rgb(Rgba::rgba(255, 255, 255, 220)))); if band {
t.fill = Some(band_fill());
} else if t.fill.as_ref().is_some_and(is_band) {
// The band belongs to full-bleed only: another idea takes it away again.
t.fill = None;
}
} }
Ok(()) Ok(())
} }
@@ -314,6 +335,85 @@ mod tests {
assert!(s.execute("design.applyIdea", &json!({"id": "nope"})).is_err()); assert!(s.execute("design.applyIdea", &json!({"id": "nope"})).is_err());
} }
#[test]
fn switching_from_full_bleed_removes_the_title_band() {
let mut s = deck(true);
s.execute("design.applyIdea", &json!({"id": "fullBleed"})).unwrap();
assert_eq!(shape(&s, 500).fill, Some(band_fill()));
s.execute("design.applyIdea", &json!({"id": "pictureLeft"})).unwrap();
assert!(shape(&s, 500).fill.is_none(), "the band belongs to full-bleed only");
}
#[test]
fn the_band_goes_on_the_visible_title_not_a_hidden_one_before_it() {
let mut s = deck(true);
let st = s.doc_mut().unwrap();
let slide = Arc::make_mut(&mut Arc::make_mut(&mut st.doc).slides[0]);
let hidden = Shape {
id: ShapeId(503),
hidden: true,
ph: Some(Placeholder { kind: PhType::Title, ..Default::default() }),
xfrm: Some(Xfrm::new(50.0, 30.0, 860.0, 90.0)),
text: Some(deckcraft_model::text::TextBody::from_text("Old title")),
..Default::default()
};
slide.shapes.insert(0, hidden);
s.execute("design.applyIdea", &json!({"id": "fullBleed"})).unwrap();
assert_eq!(shape(&s, 500).fill, Some(band_fill()), "visible title gets the band");
assert!(shape(&s, 503).fill.is_none(), "hidden title is untouched");
s.execute("design.applyIdea", &json!({"id": "pictureLeft"})).unwrap();
assert!(shape(&s, 500).fill.is_none(), "switching clears the visible title's band");
assert!(shape(&s, 503).fill.is_none());
}
#[test]
fn switching_ideas_removes_the_band_after_a_pptx_round_trip() {
let mut s = deck(true);
s.execute("design.applyIdea", &json!({"id": "fullBleed"})).unwrap();
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
let back = deckcraft_pptx::import(&bytes).unwrap();
s.doc_mut().unwrap().doc = Arc::new(back);
let title = s.doc().unwrap().doc.slides[0].shapes.iter().position(|x| x.ph.as_ref().is_some_and(|p| p.kind.is_title())).unwrap();
let id = s.doc().unwrap().doc.slides[0].shapes[title].id.0;
assert_eq!(shape(&s, id).fill, Some(band_fill()), "band survives the save");
s.execute("design.applyIdea", &json!({"id": "pictureLeft"})).unwrap();
assert!(shape(&s, id).fill.is_none(), "the band is removed after reopening");
}
#[test]
fn switching_ideas_removes_a_band_saved_by_earlier_versions() {
let mut s = deck(true);
let legacy = Fill::solid(deckcraft_model::ColorRef::rgb(Rgba::rgba(255, 255, 255, 220)));
let st = s.doc_mut().unwrap();
let slide = Arc::make_mut(&mut Arc::make_mut(&mut st.doc).slides[0]);
slide.shapes.iter_mut().find(|x| x.id == ShapeId(500)).unwrap().fill = Some(legacy);
s.execute("design.applyIdea", &json!({"id": "pictureLeft"})).unwrap();
assert!(shape(&s, 500).fill.is_none());
}
#[test]
fn a_title_fill_set_by_the_user_survives_other_ideas() {
// Set through the command, and set to exactly the band's own fill.
let mut s = deck(true);
s.execute("shape.fill", &json!({"id": 500, "color": "#ffffff", "transparency": 0.14})).unwrap();
let user = shape(&s, 500).fill;
assert!(user.is_some());
s.execute("design.applyIdea", &json!({"id": "pictureLeft"})).unwrap();
assert_eq!(shape(&s, 500).fill, user);
}
#[test]
fn full_bleed_twice_keeps_the_band_and_undo_after_switching_brings_it_back() {
let mut s = deck(true);
s.execute("design.applyIdea", &json!({"id": "fullBleed"})).unwrap();
s.execute("design.applyIdea", &json!({"id": "fullBleed"})).unwrap();
assert_eq!(shape(&s, 500).fill, Some(band_fill()));
s.execute("design.applyIdea", &json!({"id": "framed"})).unwrap();
assert!(shape(&s, 500).fill.is_none());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(shape(&s, 500).fill, Some(band_fill()));
}
#[test] #[test]
fn cover_crop_matches_the_box_aspect() { fn cover_crop_matches_the_box_aspect() {
let c = cover_crop(2.0, 1.0); let c = cover_crop(2.0, 1.0);