From 7d9c0a4dd9f355d2c044fdb50da2aaf4f9e771e8 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:33:06 +0200 Subject: [PATCH] Center Across Selection: center text over the empty cells to its right (#479) --- crates/engine/src/cmd/print.rs | 41 +++++++++++++++++++++++++++++++--- crates/ui-egui/src/grid.rs | 39 ++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/print.rs b/crates/engine/src/cmd/print.rs index 986351e..0581261 100644 --- a/crates/engine/src/cmd/print.rs +++ b/crates/engine/src/cmd/print.rs @@ -1027,6 +1027,21 @@ fn empty_for_overflow(sh: &Sheet, c: CellRef, merges: &[RangeRef]) -> bool { sh.value_ref(c).is_none_or(|v| v.is_empty()) && !merges.iter().any(|m| m.contains(c)) } +/// Unscaled width of the empty Center Across Selection cells right of `c`, which its text is centred over too. +fn center_across_width(styles: &StyleTable, sh: &Sheet, c: CellRef, merges: &[RangeRef]) -> f32 { + let mut ext = 0.0; + let mut col = c.col + 1; + while col < MAX_COLS && ext < 20_000.0 { + let nc = CellRef::new(c.row, col); + if !empty_for_overflow(sh, nc, merges) || styles.get(sh.style_id(nc)).align.h != HAlign::CenterAcross { + break; + } + ext += sh.col_width(col); + col += 1; + } + ext +} + fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f32, f32), merged: bool, merges: &[RangeRef], limits: (f32, f32)) { let (x, y, w, h) = rect; let sh = b.sh; @@ -1110,8 +1125,11 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3 VAlign::Bottom => y + h - pad - size * 0.22 - (n - 1.0) * line_h, }; // Overflow into empty neighbours (single-line text, not merged). - let (mut clip_l, mut clip_r) = (x, x + w); - if !wrap && !merged && !is_num && rot.abs() != 90 && tw + 2.0 * pad + indent > w { + // Center Across Selection spans the empty Center Across cells to its right; the text stays within that run. + let spans = st.align.h == HAlign::CenterAcross && !wrap && !merged && rot == 0; + let across = if spans { center_across_width(&wb.styles, sh, c, merges) * b.k } else { 0.0 }; + let (mut clip_l, mut clip_r) = (x, x + w + across); + if !wrap && !merged && !is_num && rot.abs() != 90 && across <= 0.0 && tw + 2.0 * pad + indent > w { let need = tw + 2.0 * pad + indent - w; let grow = |dir: i64, mut need: f32| -> f32 { let mut ext = 0.0; @@ -1183,7 +1201,7 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3 let lw = gridcraft_pdf::text_width(font, size, line); let lx = match h_align { HAlign::Right => x + w - pad - lw - indent, - HAlign::Center => x + (w - lw) / 2.0, + HAlign::Center => x + (w + across - lw) / 2.0, _ => x + pad + indent, }; let ly = first_baseline + i as f32 * line_h; @@ -2063,6 +2081,23 @@ mod tests { assert!(w.len() > 2); } + #[test] + fn center_across_spans_following_empty_center_across_cells() { + let mut styles = StyleTable::default(); + let across = styles.intern(Style { align: Alignment { h: HAlign::CenterAcross, ..Alignment::default() }, ..Style::default() }); + let mut sh = Sheet::new("S"); + for col in 0..3 { + sh.set_style(CellRef::new(0, col), across); + } + sh.set_value(CellRef::new(0, 0), Value::Text("Title".into())); + let a1 = CellRef::new(0, 0); + assert_eq!(center_across_width(&styles, &sh, a1, &[]), sh.col_width(1) + sh.col_width(2)); + sh.set_value(CellRef::new(0, 2), Value::Text("x".into())); + assert_eq!(center_across_width(&styles, &sh, a1, &[]), sh.col_width(1)); + let merged = [RangeRef::new(CellRef::new(0, 1), CellRef::new(1, 1))]; + assert_eq!(center_across_width(&styles, &sh, a1, &merged), 0.0); + } + #[test] fn rotated_wrapped_text_prints_rotated() { // Upright headers over narrow columns (mark sheets) combine rotation with Wrap Text. diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index d0eef85..e6399e0 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -368,6 +368,15 @@ fn numfmt_color(c: gridcraft_engine::numfmt::FormatColor) -> Color32 { } } +/// Last column of a Center Across Selection run starting at `start`: it extends over following columns (up to `limit`) while `continues` holds. +fn center_across_end(start: u32, limit: u32, continues: impl Fn(u32) -> bool) -> u32 { + let mut end = start; + while end < limit && end + 1 < MAX_COLS && continues(end + 1) { + end += 1; + } + end +} + fn look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut CfCache) -> (Look, Option) { let mut style = wb.styles.get(sh.style_id(c)).clone(); if let Some(t) = sh.table_at(c) { @@ -1035,17 +1044,25 @@ pub(crate) fn paint_quadrant( galley = p.layout_no_wrap(s, font.clone(), color); w = galley.size().x; } + // Center Across Selection: the text is centred over this cell and the empty cells to its right that also use it. + let across_end = if halign == HAlign::CenterAcross && !rotated && in_merge(c).is_none() { + center_across_end(c.col, c1 + 20, |col| { + let n = CellRef::new(c.row, col); + empty_ok(n) && wb.styles.get(sh.style_id(n)).align.h == HAlign::CenterAcross + }) + } else { + c.col + }; // Overflow into empty neighbours for text (not upright text, whose width is its line height). let mut clip = *rect; - if filter_space == 0.0 - && !numeric - && !matches!(v, Value::Number(_)) - && st.align.rotation.abs() != 90 - && w > rect.width() - 2.0 * pad - && in_merge(c).is_none() + let overflows = !numeric && !matches!(v, Value::Number(_)) && st.align.rotation.abs() != 90 && w > rect.width() - 2.0 * pad; + if filter_space == 0.0 && (overflows || across_end > c.col) && in_merge(c).is_none() { let mut right = rect.right(); let mut left = rect.left(); + if across_end > c.col { + right = geo.x(sh, across_end + 1); + } if matches!(halign, HAlign::Left | HAlign::Center | HAlign::Justify) { let mut cc = c.col + 1; while right - rect.left() < w + 2.0 * pad && cc < MAX_COLS && empty_ok(CellRef::new(c.row, cc)) && cc <= c1 + 20 { @@ -1075,7 +1092,8 @@ pub(crate) fn paint_quadrant( } } let x = match halign { - HAlign::Center | HAlign::CenterAcross | HAlign::Distributed => rect.center().x - w / 2.0, + HAlign::CenterAcross => clip.center().x - w / 2.0, + HAlign::Center | HAlign::Distributed => rect.center().x - w / 2.0, HAlign::Right => rect.right() - pad - w - indent, _ => text_left + pad + indent, }; @@ -3053,6 +3071,13 @@ mod selection_fill_tests { mod tests { use super::*; + #[test] + fn center_across_run_stops_at_first_non_continuing_column() { + assert_eq!(center_across_end(0, 20, |c| c <= 2), 2); + assert_eq!(center_across_end(0, 20, |_| false), 0); + assert_eq!(center_across_end(0, 1, |_| true), 1); + } + #[test] fn cell_text_y_moves_monotonically_in_standard_and_tall_rows() { let rect_std = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 20.0));