Grid: render Shrink to fit (#375)
Cells with shrinkToFit drew at full size: text spilled or was clipped and numbers showed ####. Too-wide content now gets a smaller font that fits the cell or merged area, matching the PDF/print path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ce0483fc17
commit
3ae319154c
|
|
@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Shrink to fit draws too-wide text and numbers in a smaller font inside the cell or merged area (#375) |
|
||||
| 2026-10-11 | Legacy Excel 97-2003 `.xls` (BIFF8) import with formulas, number formats, merges, sizes and names (#121) |
|
||||
| 2026-10-11 | XLSX pass-through: custom XML, external links, connections, query tables, printer settings, form/ActiveX controls and OLE objects are kept on save (#330, #324) |
|
||||
| 2026-10-11 | Data › Forecast › Forecast Sheet: a new sheet with the history, FORECAST.ETS forecast, confidence bounds, statistics and a chart (#322) |
|
||||
|
|
|
|||
|
|
@ -934,10 +934,17 @@ pub(crate) fn paint_quadrant(
|
|||
continue;
|
||||
}
|
||||
let first_line = text.lines().next().unwrap_or("").to_string();
|
||||
// Shrink to fit: too-wide content is drawn in a smaller font so it fits the cell (or the
|
||||
// merged area): it neither spills, nor is clipped, nor turns into ####.
|
||||
let small = (st.align.shrink && !rotated && st.align.h != HAlign::Fill && fill_char.is_none())
|
||||
.then(|| shrink_font(p, &first_line, &font, avail_w - 2.0 * pad - indent))
|
||||
.flatten();
|
||||
let shrunk = small.is_some();
|
||||
let font = small.unwrap_or(font);
|
||||
let mut galley = p.layout_no_wrap(first_line.clone(), font.clone(), color);
|
||||
let mut w = galley.size().x;
|
||||
// Numbers and dates never overflow: they show #### when too narrow.
|
||||
if (numeric || matches!(v, Value::Number(_))) && w > avail_w - 2.0 * pad && st.align.h != HAlign::Fill {
|
||||
if !shrunk && (numeric || matches!(v, Value::Number(_))) && w > avail_w - 2.0 * pad && st.align.h != HAlign::Fill {
|
||||
let n = ((avail_w - 2.0 * pad) / char_w).floor().max(1.0) as usize;
|
||||
galley = p.layout_no_wrap("#".repeat(n), font.clone(), color);
|
||||
w = galley.size().x;
|
||||
|
|
@ -1113,6 +1120,19 @@ pub fn filter_button_rect(geo: &Geo, sh: &Sheet, c: CellRef) -> Rect {
|
|||
Rect::from_min_size(pos2(rect.right() - s - 2.0, rect.bottom() - s - 2.0), vec2(s, s))
|
||||
}
|
||||
|
||||
/// The smaller font that makes `text` fit in `room` points of width (Shrink to fit), or `None`
|
||||
/// when it already fits at its own size.
|
||||
fn shrink_font(p: &Painter, text: &str, font: &FontId, room: f32) -> Option<FontId> {
|
||||
let natural = p.layout_no_wrap(text.to_string(), font.clone(), Color32::PLACEHOLDER).size().x;
|
||||
let room = room.max(1.0);
|
||||
if natural.is_nan() || natural <= room {
|
||||
return None;
|
||||
}
|
||||
// Glyph advances don't scale exactly linearly (rounding), so leave a little slack.
|
||||
let size = (font.size * room / natural * 0.97).max(1.0);
|
||||
Some(FontId::new(size, font.family.clone()))
|
||||
}
|
||||
|
||||
pub(crate) fn paint_border(p: &Painter, pts: [Pos2; 2], style: BorderStyle, col: Color32) {
|
||||
let w = style.width();
|
||||
let (a, b) = (pts[0], pts[1]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
//! Shrink to fit (#375): too-wide content is drawn in a smaller font inside its cell.
|
||||
|
||||
use egui::Shape;
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::CellRef;
|
||||
use gridcraft_ui_egui::{SheetApp, grid::Geo};
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness() -> Harness {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
let shrink = json!({"align": {"shrink": true}});
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "Executive Producer"})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "A1", "style": shrink})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "A2", "input": "Control Producer"})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "A4", "input": "123456789012"})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "A4", "style": {"num_fmt": "#,##0", "align": {"shrink": true}}})).unwrap();
|
||||
s.execute("cell.set", json!({"cell": "A5", "input": "123456789012"})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "A5", "style": {"num_fmt": "#,##0"}})).unwrap();
|
||||
s.execute("selection.set", json!({"range": "H20"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
/// The painted text shape whose text is `text`: (font size, width, left x).
|
||||
fn text_shape(h: &Harness, text: &str) -> Option<(f32, f32, f32)> {
|
||||
h.output().shapes.iter().find_map(|s| match &s.shape {
|
||||
Shape::Text(t) if t.galley.job.text == text => {
|
||||
let size = t.galley.job.sections.first().map(|s| s.format.font_id.size)?;
|
||||
Some((size, t.galley.size().x, t.pos.x))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrink_to_fit_text_and_numbers_fit_their_cell() {
|
||||
let h = harness();
|
||||
let app = h.state();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
|
||||
let a1 = geo.cell_rect(sheet, CellRef::parse("A1").unwrap());
|
||||
let (small, w, x) = text_shape(&h, "Executive Producer").expect("A1 is drawn");
|
||||
let (normal, cw, _) = text_shape(&h, "Control Producer").expect("A2 is drawn");
|
||||
assert!(small < normal, "A1 is drawn smaller ({small} vs {normal})");
|
||||
assert!(cw > a1.width(), "the control is wider than the column");
|
||||
assert!(x >= a1.left() && x + w <= a1.right(), "A1 fits inside column A");
|
||||
// A number shrinks instead of turning into ####; its unshrunk twin still shows ####.
|
||||
let (num, nw, _) = text_shape(&h, "123,456,789,012").expect("A4 shows the number");
|
||||
assert!(num < normal && nw <= a1.width());
|
||||
assert!(h.output().shapes.iter().any(|s| matches!(&s.shape, Shape::Text(t) if t.galley.job.text.starts_with("##"))));
|
||||
}
|
||||
Loading…
Reference in New Issue