diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index a6400054d..1499b37e5 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1005,6 +1005,24 @@ fn selected_frame_options(app: &DesignApp) -> Option bool { + let Some(st) = app.session.active() else { + return true; + }; + let mut ids = st.selection.items.clone(); + if ids.is_empty() + && let Some(t) = st.selection.text + { + ids = match t.frame { + Some(f) => vec![f], + None => st.doc.story(t.story).map(|s| s.frames.clone()).unwrap_or_default(), + }; + } + ids.iter().filter_map(|id| st.doc.item(*id)?.text_frame()).all(|t| t.options.inset.iter().all(|v| (v - t.options.inset[0]).abs() < 1e-9)) +} + /// Object › Text Frame Options shows the selected frame's options. `_shown` keeps what it showed, /// so OK sends only the fields the user changed. Fields given when the dialog opened count as /// changed. @@ -1056,7 +1074,8 @@ fn seed_text_frame_options(app: &DesignApp, d: &mut Dialog) { d.fields.entry(k.clone()).or_insert_with(|| v.clone()); } } - d.fields.entry("insetLinked".to_string()).or_insert_with(|| json!(o.inset.iter().all(|v| (v - o.inset[0]).abs() < 1e-9))); + // Linked only when no selected frame has uneven sides, so a typed side cannot overwrite them. + d.fields.entry("insetLinked".to_string()).or_insert_with(|| json!(selected_frames_have_even_insets(app))); // The frame's width: Fixed Number shows each column's width from it as the fields change. if let Some(w) = selected_frame_width(app) { d.fields.insert("_frameWidth".into(), json!(w)); diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 272165b08..5c2cf749e 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -1485,6 +1485,57 @@ fn text_frame_options_change_only_what_was_edited() { assert_eq!(opts(&app), before); } +#[test] +fn text_frame_options_link_the_insets_only_when_every_selected_frame_has_even_sides() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let frame = |app: &mut DesignApp, rect: [u32; 4], inset: Value| { + let r = app.run("frame.create", json!({"rect": rect, "content": "text", "text": "Hello"})).unwrap(); + let id = r["id"].as_u64().unwrap(); + app.run("object.textFrameOptions", json!({"ids": [id], "inset": inset})).unwrap(); + (designcraft_doc::ItemId(id), r["story"].clone()) + }; + let (even, _) = frame(&mut app, [36, 36, 236, 236], json!(6)); + let (uneven, uneven_story) = frame(&mut app, [300, 36, 500, 236], json!([1, 2, 3, 4])); + let (other_even, other_even_story) = frame(&mut app, [36, 300, 236, 500], json!(9)); + let inset = |app: &DesignApp, id| app.session.doc().unwrap().doc.item(id).unwrap().text_frame().unwrap().options.inset; + let seeded = |app: &DesignApp| { + let mut d = Dialog::new("textFrameOptions", json!({})); + seed_text_frame_options(app, &mut d); + d + }; + // The frame with even sides is selected first: the uneven one still turns the link off. + app.run("selection.set", json!({"ids": [even.0, uneven.0]})).unwrap(); + let mut d = seeded(&app); + assert_eq!(d.s("insetTop"), "0p6"); + assert!(!d.b("insetLinked"), "a selected frame with uneven sides leaves the link off"); + // Top edited while unlinked: only Top changes, and each frame keeps its other sides. + d.fields.insert("insetTop".into(), json!("1p0")); + app.ui.dialog = Some(d); + confirm(&mut app).unwrap(); + assert_eq!(inset(&app, even), [12.0, 6.0, 6.0, 6.0]); + assert_eq!(inset(&app, uneven), [12.0, 2.0, 3.0, 4.0]); + // A frame with even sides alone keeps the link on; selected with the uneven one, the link is off. + app.run("selection.set", json!({"ids": [other_even.0]})).unwrap(); + assert!(seeded(&app).b("insetLinked")); + app.run("selection.set", json!({"ids": [other_even.0, uneven.0]})).unwrap(); + assert!(!seeded(&app).b("insetLinked")); + // A caret: the frames the dialog changes decide. + app.run("text.select", json!({"story": uneven_story, "anchor": 0, "focus": 0})).unwrap(); + assert!(!seeded(&app).b("insetLinked")); + app.run("text.select", json!({"story": other_even_story, "anchor": 0, "focus": 0})).unwrap(); + assert!(seeded(&app).b("insetLinked")); + // A story threaded on into the uneven frame: a caret placed in the first frame changes only + // that frame, and a caret with no frame changes every frame of the story. + let (first, story) = frame(&mut app, [300, 300, 500, 500], json!(6)); + let (last, _) = frame(&mut app, [300, 520, 500, 700], json!([1, 2, 3, 4])); + app.run("text.thread", json!({"from": first.0, "to": last.0})).unwrap(); + app.run("text.placeCaret", json!({"frame": first.0, "point": [310, 310]})).unwrap(); + assert!(seeded(&app).b("insetLinked")); + app.run("text.select", json!({"story": story, "anchor": 0, "focus": 0})).unwrap(); + assert!(!seeded(&app).b("insetLinked")); +} + #[test] fn toc_dialog_adds_a_style_and_ok_makes_the_table_of_contents() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());