mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
Squash of PR #548 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
6.2 KiB
Rust
130 lines
6.2 KiB
Rust
//! Table cell styles (#534): Title/Header/Data and custom cell styles in TABLESTYLE (JSON and
|
|
//! typed), applied to rows and cells, and drawn with their fill, borders, alignment and number
|
|
//! format.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::color::Color;
|
|
use cadcraft_engine::doc::{CellFormat, EntityKind, Handle, Table, TableStyle};
|
|
use cadcraft_engine::render;
|
|
use serde_json::json;
|
|
|
|
fn style(s: &Session, name: &str) -> TableStyle {
|
|
s.doc().unwrap().table_styles.iter().find(|t| t.name == name).cloned().unwrap()
|
|
}
|
|
|
|
fn table(s: &Session, h: &str) -> Table {
|
|
match &s.doc().unwrap().entity(Handle::parse_hex(h).unwrap()).unwrap().kind {
|
|
EntityKind::Table(t) => t.clone(),
|
|
_ => panic!("not a table"),
|
|
}
|
|
}
|
|
|
|
fn new_table(s: &mut Session) -> String {
|
|
let r = s
|
|
.execute("table", &json!({ "at": [0, 0], "title": "Schedule", "header": ["Item", "Cost"], "cells": [["a", "1"], ["b", "2.5"], ["Total", ""]], "colWidth": 4.0 }))
|
|
.unwrap();
|
|
r["handle"].as_str().unwrap().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn define_cell_styles_with_json() {
|
|
let mut s = Session::new();
|
|
s.execute("tablestyle", &json!({ "name": "Costs", "cellStyle": { "name": "Header", "fill": "yellow", "textColor": 5 } })).unwrap();
|
|
let r = s
|
|
.execute(
|
|
"tablestyle.cellstyle",
|
|
&json!({ "style": "Costs", "name": "Totals", "align": "MR", "format": "decimal", "precision": 2, "margin": 0.1,
|
|
"borders": { "all": { "lineweight": 0.5 }, "top": { "color": "red" }, "inside": false } }),
|
|
)
|
|
.unwrap();
|
|
let names: Vec<&str> = r["cellStyles"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
|
|
assert_eq!(names, ["Title", "Header", "Data", "Totals"]);
|
|
let st = style(&s, "Costs");
|
|
let h = st.cell_style("Header");
|
|
assert_eq!((h.fill, h.text_color, h.align), (Some(Color::Index(2)), Color::Index(5), 5));
|
|
let t = st.cell_style("totals");
|
|
assert_eq!((t.align, t.format, t.precision, t.margin_h), (6, CellFormat::Decimal, 2, 0.1));
|
|
assert_eq!(t.borders.top.color, Color::Index(1));
|
|
assert!(!t.borders.inside_v.visible && t.borders.left.visible);
|
|
assert_eq!(t.borders.left.lineweight, cadcraft_engine::doc::Lineweight::Mm100(50));
|
|
// Bad values are refused, never stored.
|
|
assert!(s.execute("tablestyle.cellstyle", &json!({ "style": "Costs", "name": "Totals", "align": "XX" })).is_err());
|
|
assert!(s.execute("tablestyle.cellstyle", &json!({ "style": "Costs", "name": "Totals", "textHeight": -1 })).is_err());
|
|
assert!(s.execute("tablestyle.cellstyle", &json!({ "style": "Nope", "name": "Totals" })).is_err());
|
|
// Delete a custom style; deleting a built-in one resets it.
|
|
s.execute("tablestyle.cellstyle", &json!({ "style": "Costs", "name": "Totals", "delete": true })).unwrap();
|
|
s.execute("tablestyle.cellstyle", &json!({ "style": "Costs", "name": "Header", "delete": true })).unwrap();
|
|
assert!(style(&s, "Costs").cell_styles.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn rows_and_cells_take_cell_styles() {
|
|
let mut s = Session::new();
|
|
s.execute("tablestyle.cellstyle", &json!({ "name": "Totals", "fill": "green", "format": "decimal", "precision": 2, "align": "MR" })).unwrap();
|
|
let h = new_table(&mut s);
|
|
// Row 4 (Total) gets the custom style; B5 sums the costs.
|
|
s.execute("table.cellstyle", &json!({ "handle": h, "row": 4, "style": "Totals" })).unwrap();
|
|
s.execute("table.set", &json!({ "handle": h, "row": 4, "col": 1, "text": "=Sum(B3:B4)" })).unwrap();
|
|
let t = table(&s, &h);
|
|
assert_eq!(t.row_style(4), "Totals");
|
|
assert_eq!(t.cells[4][1].text, "3.50");
|
|
// A single cell back to Data: the result is formatted the Data way.
|
|
s.execute("table.cellstyle", &json!({ "handle": h, "row": 4, "col": 1, "style": "Data" })).unwrap();
|
|
assert_eq!(table(&s, &h).cells[4][1].text, "3.5");
|
|
assert!(s.execute("table.cellstyle", &json!({ "handle": h, "row": 4, "style": "Missing" })).is_err());
|
|
assert!(s.execute("table.cellstyle", &json!({ "handle": h, "row": 99, "style": "Data" })).is_err());
|
|
// Changing the cell style reformats the tables using it.
|
|
s.execute("tablestyle.cellstyle", &json!({ "name": "Data", "format": "percent", "precision": 0 })).unwrap();
|
|
assert_eq!(table(&s, &h).cells[4][1].text, "350%");
|
|
}
|
|
|
|
#[test]
|
|
fn styles_are_drawn() {
|
|
let mut s = Session::new();
|
|
s.execute("tablestyle.cellstyle", &json!({ "name": "Header", "fill": "red", "borders": { "inside": false } })).unwrap();
|
|
s.execute("tablestyle.cellstyle", &json!({ "name": "Data", "align": "TR" })).unwrap();
|
|
let h = new_table(&mut s);
|
|
let t = table(&s, &h);
|
|
let g = render::table_geometry(s.doc().unwrap(), &t);
|
|
// One fill per header cell.
|
|
assert_eq!(g.fills.len(), 2);
|
|
assert!(g.fills.iter().all(|f| f.color == Color::Index(1)));
|
|
// No vertical line between the two header cells.
|
|
let y_header = (-t.row_heights[0] - t.row_heights[1], -t.row_heights[0]);
|
|
let inner = g.lines.iter().any(|l| {
|
|
(l.a.x - 4.0).abs() < 1e-9
|
|
&& (l.b.x - 4.0).abs() < 1e-9
|
|
&& l.a.y.min(l.b.y) < y_header.1 - 1e-9
|
|
&& l.a.y.max(l.b.y) > y_header.0 + 1e-9
|
|
&& l.a.y.max(l.b.y) <= y_header.1 + 1e-9
|
|
});
|
|
assert!(!inner);
|
|
// Data text sits top right inside the margin.
|
|
let a = g.texts.iter().find(|x| x.text == "1").unwrap();
|
|
assert_eq!(a.attach, 3);
|
|
let margin = style(&s, "Standard").margin;
|
|
assert!((a.at.x - (8.0 - margin)).abs() < 1e-9);
|
|
// The display list has the fill triangles.
|
|
let list = render::build(s.doc().unwrap(), &cadcraft_engine::doc::Space::Model, &render::Options::default());
|
|
assert!(list.prims.iter().any(|p| p.kind == render::Kind::Tris));
|
|
}
|
|
|
|
#[test]
|
|
fn typed_cell_style() {
|
|
let mut s = Session::new();
|
|
s.cmdline("tablestyle.cellstyle").unwrap();
|
|
s.cmdline("Notes").unwrap();
|
|
s.cmdline("h").unwrap();
|
|
s.cmdline("0.3").unwrap();
|
|
s.cmdline("f").unwrap();
|
|
s.cmdline("cyan").unwrap();
|
|
s.cmdline("a").unwrap();
|
|
s.cmdline("BC").unwrap();
|
|
s.cmdline("fo").unwrap();
|
|
s.cmdline("percent").unwrap();
|
|
s.cmdline("x").unwrap();
|
|
let c = style(&s, "Standard").cell_style("Notes");
|
|
assert_eq!((c.text_height, c.fill, c.align, c.format), (0.3, Some(Color::Index(4)), 8, CellFormat::Percent));
|
|
assert_eq!(c.name, "Notes");
|
|
}
|