diff --git a/crates/engine/src/autosize.rs b/crates/engine/src/autosize.rs index 88d2e62b..5df21cc5 100644 --- a/crates/engine/src/autosize.rs +++ b/crates/engine/src/autosize.rs @@ -232,7 +232,7 @@ pub fn fitted(d: &Document, fid: ItemId, sid: StoryId) -> Option { } /// Give frame `fid` the inner bounds `to` (its path scaled into them). -fn resize(d: &mut Document, fid: ItemId, to: Rect) -> bool { +pub(crate) fn resize(d: &mut Document, fid: ItemId, to: Rect) -> bool { let Some(it) = d.item_mut(fid) else { return false }; let r = it.inner_bounds(); if r.width() < EPS || r.height() < EPS { diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 341ecda2..16036f27 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -2265,6 +2265,8 @@ fn text_frame_options(s: &mut Session, p: &Value) -> Result { let rule_bottom = ranged("columnRuleBottomInset", -1440.0, 1440.0, "a number of points")?; let min_height = ranged("minimumHeight", 0.0, crate::autosize::MAX_SIDE, "a number of points")?; let min_width = ranged("minimumWidth", 0.0, crate::autosize::MAX_SIDE, "a number of points")?; + let column_width = ranged("columnWidth", 0.0, 15552.0, "a number of points")?; + let first_baseline_min = ranged("firstBaselineMin", 0.0, 15552.0, "a number of points")?; let reference = match p.get("autoSizeReference") { None | Some(Value::Null) => None, Some(v) => Some( @@ -2334,11 +2336,11 @@ fn text_frame_options(s: &mut Session, p: &Value) -> Result { if let Some(v) = p.get("ignoreWrap").and_then(Value::as_bool) { o.ignore_wrap = v; } - if let Some(v) = p.get("columnWidth").and_then(Value::as_f64) { - o.column_width = v.max(0.0); + if let Some(v) = column_width { + o.column_width = v; } - if let Some(v) = p.get("firstBaselineMin").and_then(Value::as_f64) { - o.first_baseline_min = v.max(0.0); + if let Some(v) = first_baseline_min { + o.first_baseline_min = v; } if let Some(v) = p.get("balanceColumns").and_then(Value::as_bool) { o.balance_columns = v; @@ -2364,6 +2366,16 @@ fn text_frame_options(s: &mut Session, p: &Value) -> Result { if let Some(v) = rule_bottom { o.column_rule_bottom_inset = v; } + // Fixed Width columns set the frame's width: n columns, the gutters between them and + // the left and right insets. + let fixed_width = o.columns_kind == designcraft_doc::ColumnsKind::FixedWidth && o.column_width > 0.0; + let width = f64::from(o.columns.max(1)) * o.column_width + f64::from(o.columns.max(1) - 1) * o.gutter + o.inset[1] + o.inset[3]; + if fixed_width + && ["columnsKind", "columnWidth", "columns", "gutter", "inset"].iter().any(|k| p.get(*k).is_some()) + && let Some(r) = d.item(*id).map(Item::inner_bounds) + { + crate::autosize::resize(d, *id, Rect::new(r.x0, r.y0, r.x0 + width, r.y1)); + } } // Story direction belongs to the story: every frame of the thread turns. if let Some(v) = p.get("vertical").and_then(Value::as_bool) { @@ -4965,3 +4977,26 @@ mod ungroup_tests { d.check().unwrap(); } } + +#[cfg(test)] +mod fixed_width_tests { + use super::*; + + #[test] + fn fixed_width_columns_set_the_frame_width() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let r = s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "Hello"})).unwrap(); + let id = ItemId(r["id"].as_u64().unwrap()); + let p = json!({"ids": [id.0], "columns": 2, "gutter": 12, "inset": 0, "columnsKind": "fixedWidth", "columnWidth": 120}); + s.execute("object.textFrameOptions", &p).unwrap(); + let b = s.doc().unwrap().doc.item(id).unwrap().inner_bounds(); + assert!((b.width() - (2.0 * 120.0 + 12.0)).abs() < 1e-6, "{b:?}"); + assert!((b.x0 - 36.0).abs() < 1e-6 && (b.height() - 264.0).abs() < 1e-6, "{b:?}"); + // Widths beyond the largest page are refused and change nothing. + for (k, v) in [("columnWidth", 1e9), ("columnWidth", -1.0), ("firstBaselineMin", 1e9)] { + assert!(s.execute("object.textFrameOptions", &json!({"ids": [id.0], k: v})).is_err(), "{k} {v}"); + } + assert!((s.doc().unwrap().doc.item(id).unwrap().inner_bounds().width() - 252.0).abs() < 1e-6); + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fc8e3062..808540a6 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -413,7 +413,6 @@ fn pdf_export(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } } -/// The options of the selected text frame (the first selected item, or the frame of the caret). /// The selected text frame's width (its own, before rotation). fn selected_frame_width(app: &DesignApp) -> Option { let st = app.session.active()?; @@ -421,6 +420,7 @@ fn selected_frame_width(app: &DesignApp) -> Option { Some(st.doc.item(fid)?.inner_bounds().width()) } +/// The options of the selected text frame (the first selected item, or the frame of the caret). fn selected_frame_options(app: &DesignApp) -> Option { let st = app.session.active()?; let fid = st.selection.items.first().copied().or_else(|| st.selection.text.and_then(|t| t.frame))?;