From da765088a0bbbb849d1e82e1b2cb952902d0d37e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 00:35:52 -0800 Subject: [PATCH] Graphs: the legend measures its labels with the document's System Layout The legend measured each label with System Layout off, while the graph draws the labels with the document's setting. With System Layout on, a long legend label could run over the next swatch in Legend Across Top. The legend now measures labels with the same setting. --- crates/engine/src/cmd/graph/generate.rs | 10 ++++----- crates/engine/src/cmd/graph/labels.rs | 27 ++++++++++++++++--------- crates/engine/src/cmd/graph/tests.rs | 27 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) 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] {