Merge pull request #401 from vangel76/fix/text-box-formatting

Drawing: text box formatting — run size/bold/font/outline, alignment, anchor, no outline (#377)
This commit is contained in:
Brandon Thomas 2026-10-11 16:14:44 -04:00
commit a5137872c1
10 changed files with 271 additions and 24 deletions

View File

@ -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 | Text boxes keep their formatting: run size, bold, italic, typeface, text colour and outline, alignment, vertical anchor and "no outline" (#377) |
| 2026-10-11 | Desktop printing opens the print PDF in the system viewer; Save As offers CSV, web page and PDF; macOS File › Properties opens File › Info (#370, #343) |
| 2026-10-11 | Windows: DirectX 12 shaders compile with FXC, so a missing or stray `dxcompiler.dll` no longer stops the app at start (#339, #354) |
| 2026-10-11 | Merged cells draw their whole outline: right and bottom edges come from the last column's and last row's cells (#376) |

View File

@ -94,6 +94,7 @@ fn stroke(s: &mut Session, p: &Json) -> Result<Json> {
fill: Color::from_hex(&format!("#{:02X}0000", width.round().clamp(0.0, 255.0) as u8)).unwrap_or_default(),
line: Color::from_hex(&color).unwrap_or(Color::rgb(0x1F, 0x5F, 0xC9)),
text,
format: Default::default(),
};
edit(s, |cx| {
cx.sheet_mut(si)?.shapes.push(shape);

View File

@ -983,6 +983,7 @@ fn insert_shape(s: &mut Session, p: &Json) -> Result<Json> {
fill,
line,
text: str_param(p, "text").unwrap_or("").into(),
format: Default::default(),
};
let sheet = d.wb.active_sheet;
edit(s, |cx| {
@ -1415,6 +1416,7 @@ fn insert_icon(s: &mut Session, p: &Json) -> Result<Json> {
fill: color,
line: Color::Auto,
text: name,
format: Default::default(),
};
let sheet = d.wb.active_sheet;
edit(s, |cx| {

View File

@ -1267,7 +1267,7 @@ fn draw_objects(pg: &mut Page, wb: &Workbook, si: usize, sh: &Sheet, b: &Block)
}
let (x, y, w, h) = place(r);
let fill = rgb_of(shp.fill, wb);
let line = rgb_of(shp.line, wb).map(|c| (c, (0.75 * b.k).max(0.25)));
let line = rgb_of(shp.line, wb).filter(|_| !shp.format.no_line).map(|c| (c, (0.75 * b.k).max(0.25)));
match shp.kind {
ShapeKind::Ellipse => {
let pts: Vec<[f32; 2]> = (0..72)
@ -1294,13 +1294,29 @@ fn draw_objects(pg: &mut Page, wb: &Workbook, si: usize, sh: &Sheet, b: &Block)
}
}
if !shp.text.is_empty() {
let size = 11.0 * b.scale;
let lines = wrap_lines(&shp.text, Font::Helvetica, size, (w - 4.0 * b.k).max(size));
let mut yy = y + 2.0 * b.k + size * 0.8;
use gridcraft_model::{ShapeTextAlign, ShapeTextAnchor};
let fm = &shp.format;
let text_box = shp.kind == ShapeKind::TextBox;
let size = fm.font_size.unwrap_or(11.0).clamp(1.0, 4000.0) * b.scale;
let font = Font::styled(Font::family_for(fm.font.as_deref().unwrap_or("")), fm.bold, fm.italic);
let color = fm.text_color.and_then(|c| rgb_of(c, wb)).unwrap_or([0, 0, 0]);
let align = fm.align.unwrap_or(if text_box { ShapeTextAlign::Left } else { ShapeTextAlign::Center });
let anchor = fm.anchor.unwrap_or(if text_box { ShapeTextAnchor::Top } else { ShapeTextAnchor::Middle });
let lines = wrap_lines(&shp.text, font, size, (w - 4.0 * b.k).max(size));
let block_h = lines.len().min(500) as f32 * size * 1.2;
let mut yy = match anchor {
ShapeTextAnchor::Top => y + 2.0 * b.k,
ShapeTextAnchor::Middle => y + (h - block_h) / 2.0,
ShapeTextAnchor::Bottom => y + h - 2.0 * b.k - block_h,
} + size * 0.8;
for l in lines.iter().take(500) {
let lw = gridcraft_pdf::text_width(Font::Helvetica, size, l);
let lx = if shp.kind == ShapeKind::TextBox { x + 2.0 * b.k } else { x + (w - lw) / 2.0 };
pg.text(lx, yy, size, Font::Helvetica, [0, 0, 0], l);
let lw = gridcraft_pdf::text_width(font, size, l);
let lx = match align {
ShapeTextAlign::Left => x + 2.0 * b.k,
ShapeTextAlign::Center => x + (w - lw) / 2.0,
ShapeTextAlign::Right => x + w - 2.0 * b.k - lw,
};
pg.text(lx, yy, size, font, color, l);
yy += size * 1.2;
}
}

View File

@ -448,6 +448,50 @@ pub struct Shape {
pub fill: Color,
pub line: Color,
pub text: String,
/// Outline visibility and text formatting (DrawingML `a:ln`, `a:bodyPr`, `a:pPr`, `a:rPr`).
#[serde(default)]
pub format: ShapeFormat,
}
/// Horizontal alignment of a shape's text (`a:pPr algn`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShapeTextAlign {
#[default]
Left,
Center,
Right,
}
/// Vertical anchor of a shape's text in its box (`a:bodyPr anchor`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShapeTextAnchor {
#[default]
Top,
Middle,
Bottom,
}
/// How a shape's outline and text look. The text properties apply to the whole text (the first
/// run's properties when a file has several).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ShapeFormat {
/// No outline (`a:ln/a:noFill`), whatever `Shape::line` says.
pub no_line: bool,
/// Text size in points (`a:rPr sz` / 100); `None` is the default size.
pub font_size: Option<f32>,
pub bold: bool,
pub italic: bool,
/// Typeface (`a:latin typeface`); `None` is the default font.
pub font: Option<String>,
/// Text fill colour; `None` is the default (black in text boxes).
pub text_color: Option<Color>,
/// Text outline (`a:rPr/a:ln`): colour and width in points.
pub text_outline: Option<(Color, f32)>,
/// `None`: the shape's default (left for text boxes, centred for other shapes).
pub align: Option<ShapeTextAlign>,
/// `None`: the shape's default (top for text boxes, middle for other shapes).
pub anchor: Option<ShapeTextAnchor>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -195,6 +195,54 @@ fn area_mesh(v: &[Pos2], col: Color32) -> egui::Mesh {
mesh
}
/// A shape's text with its formatting: size, bold/italic, typeface, colour, outline, horizontal
/// alignment and vertical anchor. Shapes other than text boxes default to white text centred in
/// the shape; text boxes to black text at the top left.
fn paint_shape_text(p: &Painter, wb: &Workbook, sp: &gridcraft_engine::model::Shape, r: Rect, z: f32) {
use gridcraft_engine::model::{ShapeTextAlign, ShapeTextAnchor};
let f = &sp.format;
let text_box = sp.kind == ShapeKind::TextBox;
let align = f.align.unwrap_or(if text_box { ShapeTextAlign::Left } else { ShapeTextAlign::Center });
let anchor = f.anchor.unwrap_or(if text_box { ShapeTextAnchor::Top } else { ShapeTextAnchor::Middle });
let default_color = if text_box { Color32::BLACK } else { Color32::WHITE };
let color = f.text_color.and_then(|c| c.resolve(&wb.theme)).map(theme::color32).unwrap_or(default_color);
// Default 14 px (as before); a set size is in points.
let size = f.font_size.map_or(14.0, |pt| pt * 96.0 / 72.0).clamp(1.0, 2000.0) * z;
let family = theme::cell_family(f.font.as_deref().unwrap_or(""), f.bold, f.italic);
let inset = 4.0;
let width = (r.width() - 2.0 * inset).max(1.0);
let mut job = egui::text::LayoutJob::simple(sp.text.clone(), FontId::new(size, family), color, width);
job.halign = match align {
ShapeTextAlign::Left => egui::Align::LEFT,
ShapeTextAlign::Center => egui::Align::Center,
ShapeTextAlign::Right => egui::Align::RIGHT,
};
let g = p.layout_job(job);
let x = match align {
ShapeTextAlign::Left => r.left() + inset,
ShapeTextAlign::Center => r.center().x,
ShapeTextAlign::Right => r.right() - inset,
};
let y = match anchor {
ShapeTextAnchor::Top => r.top() + inset,
ShapeTextAnchor::Middle => r.center().y - g.size().y / 2.0,
ShapeTextAnchor::Bottom => r.bottom() - inset - g.size().y,
};
let pos = pos2(x, y);
let clip = p.with_clip_rect(r.intersect(p.clip_rect()));
// egui has no stroked text: the outline is the text drawn in the outline colour, offset in
// eight directions by the outline width, under the filled text.
if let Some((c, w)) = f.text_outline
&& let Some(oc) = c.resolve(&wb.theme).map(theme::color32)
{
let d = (w * 96.0 / 72.0 * z / 2.0).clamp(0.5, 20.0);
for (dx, dy) in [(-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), (-1.0, 0.0), (1.0, 0.0), (-1.0, 1.0), (0.0, 1.0), (1.0, 1.0)] {
clip.galley_with_override_text_color(pos + vec2(dx * d, dy * d), g.clone(), oc);
}
}
clip.galley(pos, g, color);
}
pub(crate) fn anchor_rect(geo: &Geo, sh: &Sheet, a: &Anchor) -> Rect {
let x = geo.x(sh, a.cell.col) + a.dx * geo.z;
let y = geo.y(sh, a.cell.row) + a.dy * geo.z;
@ -221,7 +269,8 @@ pub(crate) fn paint_objects(app: &SheetApp, p: &Painter, geo: &Geo, wb: &Workboo
let start = p.add(egui::Shape::Noop);
let mut label = true;
let fill = sp.fill.resolve(&wb.theme).map(theme::color32).unwrap_or(Color32::TRANSPARENT);
let line = sp.line.resolve(&wb.theme).map(theme::color32).unwrap_or(Color32::DARK_GRAY);
let line =
if sp.format.no_line { Color32::TRANSPARENT } else { sp.line.resolve(&wb.theme).map(theme::color32).unwrap_or(Color32::DARK_GRAY) };
match sp.kind {
ShapeKind::Rectangle | ShapeKind::TextBox => {
p.rect_filled(r, 0.0, fill);
@ -270,15 +319,7 @@ pub(crate) fn paint_objects(app: &SheetApp, p: &Painter, geo: &Geo, wb: &Workboo
}
}
if label && !sp.text.is_empty() && !app.editing_text_box(sp.id) {
let job = egui::text::LayoutJob::simple(
sp.text.clone(),
FontId::new(14.0 * geo.z, egui::FontFamily::Name(theme::CELL.into())),
if sp.kind == ShapeKind::TextBox { Color32::BLACK } else { Color32::WHITE },
r.width() - 8.0,
);
let g = p.layout_job(job);
let pos = if sp.kind == ShapeKind::TextBox { r.min + vec2(4.0, 4.0) } else { r.center() - g.size() / 2.0 };
p.galley(pos, g, Color32::BLACK);
paint_shape_text(p, wb, sp, r, geo.z);
}
if label && sp.kind == ShapeKind::TextBox && app.selected_chart == Some(sp.id) {
selection_frame(p, r);

View File

@ -300,6 +300,8 @@ impl SheetApp {
names.extend(d.wb.styles.iter().map(|(_, s)| s.font.name.clone()));
names.push(d.wb.theme.major_font.clone());
names.push(d.wb.theme.minor_font.clone());
// Text boxes and shapes name their own typefaces.
names.extend(d.wb.sheets.iter().flat_map(|sh| sh.shapes.iter().filter_map(|s| s.format.font.clone())));
}
names
}

View File

@ -4,7 +4,7 @@
use std::fmt::Write as _;
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS};
use gridcraft_model::{Anchor, AnchorMode, Color, Image, Shape, ShapeKind, Sheet, Theme};
use gridcraft_model::{Anchor, AnchorMode, Color, Image, Shape, ShapeFormat, ShapeKind, ShapeTextAlign, ShapeTextAnchor, Sheet, Theme};
use crate::IoError;
use crate::read::Ctx;
@ -217,7 +217,44 @@ fn read_shape(obj: &El, anchor: Anchor) -> Option<Shape> {
tb.kids("p").map(|p| p.kids("r").filter_map(|r| r.child("t")).map(|t| t.text.as_str()).collect::<String>()).collect::<Vec<_>>().join("\n")
})
.unwrap_or_default();
Some(Shape { id: 0, kind, anchor, fill, line, text })
let mut format = read_text_format(obj.child("txBody"));
format.no_line = ln.is_some_and(|l| l.child("noFill").is_some());
Some(Shape { id: 0, kind, anchor, fill, line, text, format })
}
/// A shape's text formatting: vertical anchor (`a:bodyPr anchor`), the first paragraph's
/// alignment (`a:pPr algn`) and the first run's properties (`a:rPr`: size, bold, italic, fill,
/// outline, `a:latin` typeface).
fn read_text_format(body: Option<&El>) -> ShapeFormat {
let mut f = ShapeFormat::default();
let Some(body) = body else { return f };
f.anchor = body.child("bodyPr").and_then(|b| b.attr("anchor")).map(|a| match a {
"ctr" => ShapeTextAnchor::Middle,
"b" => ShapeTextAnchor::Bottom,
_ => ShapeTextAnchor::Top,
});
let first_p = body.kids("p").next();
f.align = first_p.and_then(|p| p.child("pPr")).and_then(|p| p.attr("algn")).map(|a| match a {
"ctr" => ShapeTextAlign::Center,
"r" => ShapeTextAlign::Right,
_ => ShapeTextAlign::Left,
});
let rpr = body.kids("p").flat_map(|p| p.kids("r")).find_map(|r| r.child("rPr"));
if let Some(r) = rpr {
// `sz` is in hundredths of a point (100..=400000 in the schema).
f.font_size = r.attr_f64("sz").filter(|v| v.is_finite()).map(|v| (v / 100.0).clamp(1.0, 4000.0) as f32);
f.bold = r.flag("b", false);
f.italic = r.flag("i", false);
f.font = r.child("latin").and_then(|l| l.attr("typeface")).filter(|t| !t.is_empty() && !t.starts_with('+')).map(str::to_string);
f.text_color = r.child("solidFill").and_then(dml_color);
f.text_outline = r.child("ln").and_then(|l| {
let c = l.child("solidFill").and_then(dml_color)?;
// Width in EMU (12700 per point); Excel's default hairline is 0.75 pt.
let w = l.attr_f64("w").filter(|w| w.is_finite()).map_or(0.75, |w| (w / 12700.0).clamp(0.0, 100.0)) as f32;
Some((c, w))
});
}
f
}
// ---------------------------------------------------------------- writing
@ -341,7 +378,9 @@ pub fn write_drawing(sheet: &Sheet, theme: &Theme, objs: &[Obj<'_>]) -> String {
let _ = write!(s, "<a:solidFill>{c}</a:solidFill>");
}
let line_c = srgb(&sh.line, theme);
if line_c.is_some() || sh.kind == ShapeKind::Arrow {
if sh.format.no_line {
s.push_str("<a:ln><a:noFill/></a:ln>");
} else if line_c.is_some() || sh.kind == ShapeKind::Arrow {
s.push_str("<a:ln w=\"9525\">");
match line_c {
Some(c) => {
@ -356,12 +395,30 @@ pub fn write_drawing(sheet: &Sheet, theme: &Theme, objs: &[Obj<'_>]) -> String {
}
s.push_str("</xdr:spPr>");
if !sh.text.is_empty() {
s.push_str("<xdr:txBody><a:bodyPr wrap=\"square\" rtlCol=\"0\" anchor=\"t\"/><a:lstStyle/>");
let f = &sh.format;
let anchor = match f.anchor {
Some(ShapeTextAnchor::Top) => " anchor=\"t\"",
Some(ShapeTextAnchor::Middle) => " anchor=\"ctr\"",
Some(ShapeTextAnchor::Bottom) => " anchor=\"b\"",
None if sh.kind == ShapeKind::TextBox => " anchor=\"t\"",
None => "",
};
let _ = write!(s, "<xdr:txBody><a:bodyPr wrap=\"square\" rtlCol=\"0\"{anchor}/><a:lstStyle/>");
let ppr = match f.align {
None => "",
Some(ShapeTextAlign::Left) => "<a:pPr algn=\"l\"/>",
Some(ShapeTextAlign::Center) => "<a:pPr algn=\"ctr\"/>",
Some(ShapeTextAlign::Right) => "<a:pPr algn=\"r\"/>",
};
let (rpr_attrs, rpr_body) = run_props(f, theme);
for line in sh.text.split('\n') {
if line.is_empty() {
s.push_str("<a:p><a:endParaRPr lang=\"en-US\"/></a:p>");
let _ = write!(s, "<a:p>{ppr}<a:endParaRPr lang=\"en-US\"{rpr_attrs}/></a:p>");
} else if rpr_body.is_empty() {
let _ = write!(s, "<a:p>{ppr}<a:r><a:rPr lang=\"en-US\"{rpr_attrs}/><a:t>{}</a:t></a:r></a:p>", esc(line));
} else {
let _ = write!(s, "<a:p><a:r><a:rPr lang=\"en-US\"/><a:t>{}</a:t></a:r></a:p>", esc(line));
let _ =
write!(s, "<a:p>{ppr}<a:r><a:rPr lang=\"en-US\"{rpr_attrs}>{rpr_body}</a:rPr><a:t>{}</a:t></a:r></a:p>", esc(line));
}
}
s.push_str("</xdr:txBody>");
@ -376,6 +433,35 @@ pub fn write_drawing(sheet: &Sheet, theme: &Theme, objs: &[Obj<'_>]) -> String {
s
}
/// A text run's `a:rPr` attributes (size, bold, italic) and children in schema order (outline,
/// fill, typeface).
fn run_props(f: &ShapeFormat, theme: &Theme) -> (String, String) {
let mut attrs = String::new();
if let Some(sz) = f.font_size.filter(|v| v.is_finite()) {
let _ = write!(attrs, " sz=\"{}\"", (sz.clamp(1.0, 4000.0) * 100.0).round() as u32);
}
if f.bold {
attrs.push_str(" b=\"1\"");
}
if f.italic {
attrs.push_str(" i=\"1\"");
}
let mut body = String::new();
if let Some((c, w)) = &f.text_outline
&& let Some(c) = srgb(c, theme)
{
let emu = (w.clamp(0.0, 100.0) * 12700.0).round() as u32;
let _ = write!(body, "<a:ln w=\"{emu}\"><a:solidFill>{c}</a:solidFill></a:ln>");
}
if let Some(c) = f.text_color.as_ref().and_then(|c| srgb(c, theme)) {
let _ = write!(body, "<a:solidFill>{c}</a:solidFill>");
}
if let Some(font) = &f.font {
let _ = write!(body, "<a:latin typeface=\"{}\"/>", esc(font));
}
(attrs, body)
}
/// File extension for an image MIME type.
pub fn image_ext(mime: &str, data: &[u8]) -> &'static str {
match mime.to_ascii_lowercase().as_str() {

View File

@ -454,3 +454,44 @@ fn chartex_parts_as_excel_expects_them() {
let s = &back.sheet(0).unwrap().charts[0].series[0];
assert_eq!((s.name.as_deref(), s.categories.as_deref(), s.values.as_str()), (Some("Data!$B$1"), Some("Data!$A$2:$A$5"), "Data!$B$2:$B$5"));
}
/// #377: a WordArt-style text box keeps its run size, bold, typeface, text fill and outline, its
/// centring (horizontal and vertical) and its "no outline" shape line, through a round trip.
#[test]
fn text_box_formatting_is_read_and_written() {
let drawing = r#"<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><xdr:twoCellAnchor>
<xdr:from><xdr:col>1</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>2</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from><xdr:to><xdr:col>7</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>8</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:sp macro="" textlink=""><xdr:nvSpPr><xdr:cNvPr id="2" name="TextBox 1"/><xdr:cNvSpPr txBox="1"/></xdr:nvSpPr>
<xdr:spPr><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:schemeClr val="bg1"/></a:solidFill><a:ln w="9525" cmpd="sng"><a:noFill/></a:ln></xdr:spPr>
<xdr:txBody><a:bodyPr vertOverflow="clip" horzOverflow="clip" wrap="square" rtlCol="0" anchor="ctr"/><a:lstStyle/>
<a:p><a:pPr algn="ctr"/><a:r><a:rPr lang="en-US" sz="5400" b="1"><a:ln w="19050"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:ln><a:solidFill><a:schemeClr val="bg1"/></a:solidFill><a:latin typeface="Arial Black"/></a:rPr><a:t>DRAFT</a:t></a:r></a:p></xdr:txBody></xdr:sp><xdr:clientData/></xdr:twoCellAnchor></xdr:wsDr>"#;
let srels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing1.xml"/></Relationships>"#;
let bytes = minimal(
r#"<sheetData/><drawing r:id="rId1"/>"#,
&[("xl/worksheets/_rels/sheet1.xml.rels", srels), ("xl/drawings/drawing1.xml", drawing)],
"",
"",
);
let (wb, _) = read_xlsx(&bytes).unwrap();
let sp = &wb.sheet(0).unwrap().shapes[0];
assert_eq!(sp.kind, gridcraft_model::ShapeKind::TextBox);
assert_eq!(sp.text, "DRAFT");
let f = &sp.format;
assert!(f.no_line, "a:ln/a:noFill: no border");
assert_eq!(f.font_size, Some(54.0));
assert!(f.bold && !f.italic);
assert_eq!(f.font.as_deref(), Some("Arial Black"));
assert_eq!(f.text_color, Some(Color::Theme(0, 0)));
assert_eq!(f.text_outline, Some((Color::Theme(1, 0), 1.5)));
assert_eq!(f.align, Some(gridcraft_model::ShapeTextAlign::Center));
assert_eq!(f.anchor, Some(gridcraft_model::ShapeTextAnchor::Middle));
let (back, _) = read_xlsx(&crate::write_xlsx(&wb).unwrap()).unwrap();
let g = &back.sheet(0).unwrap().shapes[0].format;
let rgb = |c: Option<Color>| c.and_then(|c| c.resolve(&wb.theme));
assert_eq!((g.no_line, g.font_size, g.bold, g.font.as_deref()), (true, Some(54.0), true, Some("Arial Black")));
assert_eq!((g.align, g.anchor), (f.align, f.anchor));
assert_eq!(rgb(g.text_color), rgb(f.text_color));
assert_eq!(rgb(g.text_outline.map(|o| o.0)), rgb(f.text_outline.map(|o| o.0)));
assert_eq!(g.text_outline.map(|o| o.1), Some(1.5));
}

View File

@ -317,6 +317,7 @@ fn sample() -> Workbook {
fill: Color::Rgb(0x4472C4),
line: Color::Rgb(0x000000),
text: "Hi\nthere".into(),
format: Default::default(),
});
s.shapes.push(Shape {
id: 12,
@ -325,6 +326,7 @@ fn sample() -> Workbook {
fill: Color::Auto,
line: Color::Rgb(0xFF0000),
text: String::new(),
format: Default::default(),
});
s.shapes.push(Shape {
id: 13,
@ -333,6 +335,17 @@ fn sample() -> Workbook {
fill: Color::Auto,
line: Color::Auto,
text: "box".into(),
format: ShapeFormat {
no_line: true,
font_size: Some(20.0),
bold: true,
italic: true,
font: Some("Arial".into()),
text_color: Some(Color::Rgb(0x112233)),
text_outline: Some((Color::Rgb(0xFF0000), 1.5)),
align: Some(ShapeTextAlign::Right),
anchor: Some(ShapeTextAnchor::Bottom),
},
});
let base_chart = Chart {
id: 20,
@ -498,7 +511,7 @@ fn full_roundtrip() {
assert_eq!(b.shapes[1].anchor.mode, AnchorMode::MoveAndSize, "default anchor mode");
assert_eq!(b.shapes.len(), 3);
for (x, y) in a.shapes.iter().zip(&b.shapes) {
assert_eq!((x.kind, &x.text, x.fill, x.line), (y.kind, &y.text, y.fill, y.line));
assert_eq!((x.kind, &x.text, x.fill, x.line, &x.format), (y.kind, &y.text, y.fill, y.line, &y.format));
}
assert_eq!(b.charts.len(), a.charts.len());
for (x, y) in a.charts.iter().zip(&b.charts) {