diff --git a/crates/engine/src/cmd/objects/arrange.rs b/crates/engine/src/cmd/objects/arrange.rs index 15cc5555d7..bfae1b31d6 100644 --- a/crates/engine/src/cmd/objects/arrange.rs +++ b/crates/engine/src/cmd/objects/arrange.rs @@ -147,8 +147,8 @@ pub(super) fn move_object(s: &mut Session, pos: Pos, page: Option, x: f32, } /// [`move_object`]'s one placement: move the object's character to the anchor under (x, y) on -/// `page` and offset it from there. Objects in headers/footers, on pages without anchoring text, -/// or configured not to move with text retain page-relative anchoring. +/// `page` and offset it from there. Objects in headers/footers, or on pages without anchoring +/// text, retain page-relative anchoring; a body object is placed by its paragraph (#5705). fn move_object_once(s: &mut Session, pos: &Pos, page: usize, x: f32, y: f32, (w, h): (f32, f32)) -> Result { use wordcraft_doc::para::Anchor; if pos.path.depth() > 0 || !movable_story(s, pos.story) { @@ -165,8 +165,6 @@ fn move_object_once(s: &mut Session, pos: &Pos, page: usize, x: f32, y: f32, (w, Some(path) => (Pos { story, path, off: 0 }, false), None => (nearest_line(&layout, page, y, story).ok_or_else(|| CmdError::Params("no text on that page to anchor to".into()))?, true), }; - let was_with_text = - s.doc.para_at(pos).and_then(|p| p.object_at(pos.off)).and_then(|o| o.frame()).map_or(story == StoryRef::Body, |(_, _, f)| moves_with_text(f)); // Move its character to the anchor, unless it's already in that paragraph and drawn on this // page (one anchored on a later page of it is drawn there, #4250). let at = if anchor.story == pos.story && anchor.path == pos.path && !on_page && layout.object(pos, page).is_some_and(|o| o.page == page) { @@ -199,7 +197,7 @@ fn move_object_once(s: &mut Session, pos: &Pos, page: usize, x: f32, y: f32, (w, f.x = x.clamp(-MAX_OFFSET, MAX_OFFSET); f.y = y.clamp(-MAX_OFFSET, MAX_OFFSET); })?; - if on_page || story != StoryRef::Body || !was_with_text { + if on_page || story != StoryRef::Body { return Ok(at); } // Then offsets from its paragraph, as laid out with the object there. diff --git a/crates/engine/src/cmd/objects/tests.rs b/crates/engine/src/cmd/objects/tests.rs index cf302543ac..5758eb0290 100644 --- a/crates/engine/src/cmd/objects/tests.rs +++ b/crates/engine/src/cmd/objects/tests.rs @@ -1295,10 +1295,11 @@ fn arrange_bounds_on_header_picture_writes_page_anchored_odt_frame() { assert!(styles.contains(r#"svg:x="10pt""#) && styles.contains(r#"svg:y="20pt""#), "{styles}"); } -/// Turning off `moveWithText` on a body picture anchors it to the page, and subsequent -/// `arrange.bounds` retains page-relative anchoring and writes a page-anchored ODT frame. +/// A body picture set to stay on the page (`moveWithText` off) is still placed by its paragraph +/// once moved: the move is the placement, and the file keeps no page number to put it back +/// (#5705). Header and footer pictures stay page-anchored (see above). #[test] -fn body_picture_with_move_with_text_false_retains_page_anchor_and_coordinates() { +fn a_moved_body_picture_is_placed_by_its_paragraph_even_when_fixed_to_the_page() { use wordcraft_doc::para::Anchor; let mut s = Session::new(wordcraft_doc::Document::new()); s.run("text.insert", &json!({"text": "First line of paragraph.\n"})).unwrap(); @@ -1310,24 +1311,25 @@ fn body_picture_with_move_with_text_false_retains_page_anchor_and_coordinates() let float = obj.frame().unwrap().2; assert_eq!((float.h_rel, float.v_rel), (Anchor::Column, Anchor::Column)); - // Turn off moveWithText + // Turn off moveWithText: fixed on the page until it is moved. s.run("arrange.moveWithText", &json!({"value": false})).unwrap(); let (_, obj) = selected(&s).unwrap(); let float = obj.frame().unwrap().2; assert_eq!((float.h_rel, float.v_rel), (Anchor::Column, Anchor::Page)); - // Now call arrange.bounds on a page where a paragraph starts s.run("arrange.bounds", &json!({"page": 0, "x": 50.0, "y": 80.0})).unwrap(); - let (_, obj) = selected(&s).unwrap(); + let (pos, obj) = selected(&s).unwrap(); let float = obj.frame().unwrap().2; - assert_eq!((float.h_rel, float.v_rel), (Anchor::Page, Anchor::Page)); - assert_eq!((float.x, float.y), (50.0, 80.0)); + assert_eq!((float.h_rel, float.v_rel), (Anchor::Column, Anchor::Paragraph)); + assert_eq!(float.page, 0); + let hit = s.layout().object(&pos, 0).expect("laid out"); + assert!((hit.rect.x - 50.0).abs() < 1.0 && (hit.rect.y - 80.0).abs() < 1.0, "dropped: {:?}", hit.rect); let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&s.doc).unwrap())).unwrap(); let mut content = String::new(); std::io::Read::read_to_string(&mut z.by_name("content.xml").unwrap(), &mut content).unwrap(); - assert!(content.contains(r#"text:anchor-type="page""#), "body picture should export with page anchor: {content}"); - assert!(content.contains(r#"svg:x="50pt""#) && content.contains(r#"svg:y="80pt""#), "{content}"); + assert!(content.contains(r#"text:anchor-type="paragraph""#), "body picture should export with paragraph anchor: {content}"); + assert!(!content.contains("anchor-page-number"), "{content}"); } /// Changing `moveWithText` on an object with `h_align` set preserves its horizontal