From 0f52f3ba89b1d48e31a157c2b5b49d66e29b136b Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 8 Oct 2026 01:57:30 -0400 Subject: [PATCH] Text: stories made in a right-to-left-bound document fill their columns from the right (#100) --- crates/doc/src/edit.rs | 7 +++++++ crates/engine/src/cmd/layout.rs | 29 +++++++++++++++++++++++++++++ crates/engine/src/cmd/object.rs | 1 + crates/engine/src/cmd/path_type.rs | 1 + crates/engine/src/cmd/place_text.rs | 1 + 5 files changed, 39 insertions(+) diff --git a/crates/doc/src/edit.rs b/crates/doc/src/edit.rs index 188f514d2..3bb827135 100644 --- a/crates/doc/src/edit.rs +++ b/crates/doc/src/edit.rs @@ -171,11 +171,18 @@ impl Document { Ok(removed) } + /// The column direction a new story starts with: right to left in a document whose binding is + /// right to left (Document Setup), else left to right. Stories already made keep theirs. + pub fn new_story_direction(&self) -> crate::TextDirection { + if self.settings.right_to_left_binding { crate::TextDirection::RightToLeft } else { crate::TextDirection::LeftToRight } + } + /// Create a text frame with a new (or given) story at `rect` on spread `r`. pub fn add_text_frame(&mut self, r: SpreadRef, rect: Rect, layer: LayerId, text: &str, para: ParaFormat) -> Result<(ItemId, StoryId)> { let id = ItemId(self.alloc()); let sid = StoryId(self.alloc()); let mut story = Story::with_text(sid, text, para); + story.direction = self.new_story_direction(); story.frames.push(id); self.stories.insert(sid, Arc::new(story)); let mut item = Item::new(id, layer, Shape::Rectangle, shapes::rectangle(rect)); diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 2a1b0751c..bd5320799 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -759,6 +759,35 @@ mod setup_tests { let d = &s.doc().unwrap().doc; assert_eq!(d.spreads[1].pages.iter().map(|p| (p.side, p.x)).collect::>(), vec![(Left, 0.0), (Right, w)]); } + + /// Multi-column frames filled the left column first in a right-to-left document (#100): + /// stories made in a right-to-left-bound document start their columns on the right. + #[test] + fn new_stories_follow_the_binding_direction() { + use designcraft_doc::TextDirection::{LeftToRight, RightToLeft}; + let text = (1..=12).map(|i| format!("الفقرة {i}")).collect::>().join("\n"); + for (binding, expected) in [("rightToLeft", RightToLeft), ("leftToRight", LeftToRight)] { + let mut s = crate::Session::new(); + s.execute("file.new", &json!({"preset": "A4", "pages": 1})).unwrap(); + s.execute("layout.documentSetup", &json!({"binding": binding})).unwrap(); + let f = s.execute("frame.create", &json!({"rect": [40, 170, 555, 260], "content": "text", "text": text})).unwrap(); + s.execute("object.textFrameOptions", &json!({"ids": [f["id"]], "columns": 2, "gutter": 20})).unwrap(); + let sid = designcraft_doc::StoryId(f["story"].as_u64().unwrap()); + let st = s.doc().unwrap(); + assert_eq!(st.doc.story(sid).unwrap().direction, expected, "{binding}"); + let cs = s.cache.get(&st.doc, sid, None); + let fr = &cs.frames[0]; + let (first, last) = (fr.lines.first().unwrap(), fr.lines.last().unwrap()); + assert!(fr.columns.len() == 2 && first.x0 != last.x0, "the text fills both columns: {:?}", fr.columns); + assert_eq!(first.x0 > last.x0, expected == RightToLeft, "{binding}: the first line is in the first column"); + // An empty frame given text content starts a story too. + let g = s.execute("frame.create", &json!({"rect": [40, 400, 555, 500]})).unwrap(); + s.execute("object.content", &json!({"ids": [g["id"]], "type": "text"})).unwrap(); + let d = &s.doc().unwrap().doc; + let tf = d.item(designcraft_doc::ItemId(g["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story; + assert_eq!(d.story(tf).unwrap().direction, expected, "{binding}: object.content"); + } + } } #[cfg(test)] diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 40802d548..dc456a134 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -1379,6 +1379,7 @@ fn content_type(s: &mut Session, p: &Value) -> Result { ("text", Some(Content::Unassigned | Content::Graphic(_))) => { let sid = StoryId(d.alloc()); let mut st = Story::new(sid); + st.direction = d.new_story_direction(); st.frames.push(*id); d.stories.insert(sid, Arc::new(st)); if let Some(it) = d.item_mut(*id) { diff --git a/crates/engine/src/cmd/path_type.rs b/crates/engine/src/cmd/path_type.rs index 883f0c20a..77bbf87ab 100644 --- a/crates/engine/src/cmd/path_type.rs +++ b/crates/engine/src/cmd/path_type.rs @@ -54,6 +54,7 @@ fn on_path(s: &mut Session, p: &Value) -> Result { } let sid = StoryId(d.alloc()); let mut st = Story::with_text(sid, &text, Default::default()); + st.direction = d.new_story_direction(); st.frames = vec![id]; d.stories.insert(sid, std::sync::Arc::new(st)); let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?; diff --git a/crates/engine/src/cmd/place_text.rs b/crates/engine/src/cmd/place_text.rs index 39805bfb0..766811b50 100644 --- a/crates/engine/src/cmd/place_text.rs +++ b/crates/engine/src/cmd/place_text.rs @@ -200,6 +200,7 @@ pub(super) fn place_text(s: &mut Session, p: &Value, name: &str, bytes: &[u8]) - Some(_) => { let sid = StoryId(d.alloc()); let mut ns = Story::new(sid); + ns.direction = d.new_story_direction(); ns.frames = vec![fid]; d.stories.insert(sid, std::sync::Arc::new(ns)); if let Some(it) = d.item_mut(fid) {