diff --git a/crates/engine/src/cmd/graph/generate.rs b/crates/engine/src/cmd/graph/generate.rs index 2f16d5e42..a789304b2 100644 --- a/crates/engine/src/cmd/graph/generate.rs +++ b/crates/engine/src/cmd/graph/generate.rs @@ -902,7 +902,7 @@ impl<'a> Plot<'a> { fn legend_label(&mut self, (x, y): (f64, f64), label: &str) -> Arc { let ls = self.ls; - let (dx, dy) = label_shift(&self.g.label_styles, label); + let (dx, dy) = label_shift(&self.g.label_styles, label, self.b.d.setup.system_layout); let (x, y) = (x + dx, y + dy); self.b.label(Role::Legend, Point::new(x + 12.0, y + 7.5 + (ls - LABEL_SIZE) * 0.35), label, Justify::Left, false) } @@ -910,13 +910,13 @@ impl<'a> Plot<'a> { /// Add Legend Across Top: swatch and label after swatch and label, in rows as wide as the plot, the last row /// just above it. fn legend_across_top(&mut self) -> Vec> { - let (g, r, ls) = (self.g, self.r, self.ls); + let (g, r, ls, system_layout) = (self.g, self.r, self.ls, self.b.d.setup.system_layout); let entries: Vec<(usize, String)> = (0..self.legend_series()).map(|s| (s, self.legend_text(s))).filter(|(_, l)| !blank_label(l)).collect(); let mut rows: Vec> = vec![vec![]]; let mut used = 0.0; for (s, label) in entries { - let w = 12.0 + label_width(&g.label_styles, &label) + 12.0; - let h = ls * 1.8 * label.split('|').count() as f64 + label_extra_height(&g.label_styles, &label); + let w = 12.0 + label_width(&g.label_styles, &label, system_layout) + 12.0; + let h = ls * 1.8 * label.split('|').count() as f64 + label_extra_height(&g.label_styles, &label, system_layout); // The gap after an entry may run past the plot's edge. if used + w - 12.0 > r.width() && rows.last().is_some_and(|row| !row.is_empty()) { rows.push(vec![]); @@ -958,7 +958,7 @@ impl<'a> Plot<'a> { y += ls * 1.8; continue; } - y += ls * 1.8 * label.split('|').count() as f64 + label_extra_height(&self.g.label_styles, &label); + y += ls * 1.8 * label.split('|').count() as f64 + label_extra_height(&self.g.label_styles, &label, self.b.d.setup.system_layout); self.legend_swatch(s, (x, row_y)); labels.push(self.legend_label((x, row_y), &label)); } diff --git a/crates/engine/src/cmd/graph/labels.rs b/crates/engine/src/cmd/graph/labels.rs index 7c0127280..faf301899 100644 --- a/crates/engine/src/cmd/graph/labels.rs +++ b/crates/engine/src/cmd/graph/labels.rs @@ -188,40 +188,47 @@ pub(super) fn shadow_of(d: &mut Document, n: &Node) -> Arc { Arc::new(d.reid(&c)) } -/// The bounds of legend label `s` set from the origin with paragraph attributes `para`, without adding it to a document. -fn legend_label_bounds(styles: &vectorcraft_doc::LabelStyles, s: &str, para: vectorcraft_doc::ParaStyle) -> Option { +/// The bounds of legend label `s` set from the origin with paragraph attributes `para` and the document's System +/// Layout setting `system_layout`, as [`Gen::text`] sets it, without adding it to a document. +fn legend_label_bounds(styles: &vectorcraft_doc::LabelStyles, s: &str, para: vectorcraft_doc::ParaStyle, system_layout: bool) -> Option { let style = label_style(styles, Role::Legend); let mut t = TextObject::point(Point::ZERO, &s.replace('|', "\n"), style); t.para = para; t.para.justify = Justify::Left; + t.system_layout = system_layout; refresh_bounds(&mut t); Node::new(NodeId(0), NodeKind::Text(Box::new(t))).geometric_bounds() } /// How far label `s` reaches right of where it starts (its widest line and any indent), after the legend moves it right /// by [`label_shift`], without adding it to a document. -pub(super) fn label_width(styles: &vectorcraft_doc::LabelStyles, s: &str) -> f64 { - legend_label_bounds(styles, s, label_para(styles, Role::Legend)).map_or(0.0, |b| (b.x1 + label_shift(styles, s).0).max(0.0)) +pub(super) fn label_width(styles: &vectorcraft_doc::LabelStyles, s: &str, system_layout: bool) -> f64 { + legend_label_bounds(styles, s, label_para(styles, Role::Legend), system_layout) + .map_or(0.0, |b| (b.x1 + label_shift(styles, s, system_layout).0).max(0.0)) } /// The bounds of legend label `s` set with its kept paragraph attributes and with the defaults, when it has kept ones. -fn kept_and_default_bounds(styles: &vectorcraft_doc::LabelStyles, s: &str) -> Option<(Rect, Rect)> { +fn kept_and_default_bounds(styles: &vectorcraft_doc::LabelStyles, s: &str, system_layout: bool) -> Option<(Rect, Rect)> { styles.legend_para.as_ref()?; - Some((legend_label_bounds(styles, s, label_para(styles, Role::Legend))?, legend_label_bounds(styles, s, vectorcraft_doc::ParaStyle::default())?)) + Some(( + legend_label_bounds(styles, s, label_para(styles, Role::Legend), system_layout)?, + legend_label_bounds(styles, s, vectorcraft_doc::ParaStyle::default(), system_layout)?, + )) } /// How much further left and further up legend label `s` reaches with its kept paragraph attributes than with the /// defaults (a negative indent or negative spacing), so the legend moves it right and down by that much and it stays /// clear of its swatch and of the entries before it. -pub(super) fn label_shift(styles: &vectorcraft_doc::LabelStyles, s: &str) -> (f64, f64) { - kept_and_default_bounds(styles, s).map_or((0.0, 0.0), |(kept, default)| ((default.x0 - kept.x0).max(0.0), (default.y0 - kept.y0).max(0.0))) +pub(super) fn label_shift(styles: &vectorcraft_doc::LabelStyles, s: &str, system_layout: bool) -> (f64, f64) { + kept_and_default_bounds(styles, s, system_layout) + .map_or((0.0, 0.0), |(kept, default)| ((default.x0 - kept.x0).max(0.0), (default.y0 - kept.y0).max(0.0))) } /// How much further down legend label `s` reaches with its kept paragraph attributes than with the defaults (more /// leading, or space before or after), after the legend moves it down by [`label_shift`], so the legend makes room /// for it. -pub(super) fn label_extra_height(styles: &vectorcraft_doc::LabelStyles, s: &str) -> f64 { - kept_and_default_bounds(styles, s).map_or(0.0, |(kept, default)| (kept.y1 + (default.y0 - kept.y0).max(0.0) - default.y1).max(0.0)) +pub(super) fn label_extra_height(styles: &vectorcraft_doc::LabelStyles, s: &str, system_layout: bool) -> f64 { + kept_and_default_bounds(styles, s, system_layout).map_or(0.0, |(kept, default)| (kept.y1 + (default.y0 - kept.y0).max(0.0) - default.y1).max(0.0)) } /// A label with nothing to show (empty, or only `|` line breaks). diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index 90c561284..c5316e765 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -4344,6 +4344,33 @@ fn a_legend_across_top_spaces_its_labels_by_their_kept_paragraph_settings() { assert!(next.x0 >= label.x1, "the next swatch starts after the indented label: {label:?} {next:?}"); } +#[test] +fn a_legend_across_top_measures_its_labels_with_system_layout() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap(); + s.execute("type.systemLayout", &json!({"on": true})).unwrap(); + // Each `i` at 2 pt advances a whole point with System Layout, far more than its own width. + let name = "i".repeat(60); + let csv = format!(",{name},2025\nQ1,3,2\nQ2,5,4\nQ3,-1,6"); + let id = NodeId( + s.execute("graph.create", &json!({"type": "column", "x": 100, "y": 100, "width": 300, "height": 200, "csv": csv})).unwrap()["id"] + .as_u64() + .unwrap(), + ); + s.execute("graph.setType", &json!({"legendAcrossTop": true})).unwrap(); + let l = text_id(&s, id, "Legend", &name); + s.execute("select.set", &json!({"ids": [l.0]})).unwrap(); + s.execute("text.setStyle", &json!({"size": 2})).unwrap(); + s.execute("text.setFormat", &json!({"leftIndent": 1})).unwrap(); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "csv": csv})).unwrap(); + let n = s.doc().unwrap().doc.node(id).unwrap().clone(); + let label = group(&n, "Legend").children().unwrap().iter().find(|c| matches!(&c.kind, NodeKind::Text(t) if t.plain_text() == name)).unwrap(); + let label = label.geometric_bounds().unwrap(); + let next = series_parts(&s, id, 1).last().unwrap().geometric_bounds().unwrap(); + assert!(next.x0 >= label.x1, "the next swatch starts after the label set with System Layout: {label:?} {next:?}"); +} + #[test] fn a_legend_label_with_a_kept_negative_indent_stays_clear_of_its_swatch() { for across_top in [true, false] {