Compare commits

...

1 Commits

Author SHA1 Message Date
Brandon Thomas b2be813006 Tables: the table's own fill (a:tblPr fill) is drawn behind its cells
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 18:03:41 -04:00
6 changed files with 57 additions and 7 deletions

View File

@ -55,6 +55,9 @@ pub struct Table {
pub last_col: bool, pub last_col: bool,
pub band_row: bool, pub band_row: bool,
pub band_col: bool, pub band_col: bool,
/// `a:tblPr` fill: a fill behind the whole table (over the style's own background).
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<Fill>,
} }
impl Default for Table { impl Default for Table {
@ -69,6 +72,7 @@ impl Default for Table {
last_col: false, last_col: false,
band_row: true, band_row: true,
band_col: false, band_col: false,
background: None,
} }
} }
} }

View File

@ -394,6 +394,7 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) ->
fn table(imp: &mut Imp, part: &Part, tbl: &El) -> Table { fn table(imp: &mut Imp, part: &Part, tbl: &El) -> Table {
let mut t = Table { style: "none".into(), first_row: false, band_row: false, ..Default::default() }; let mut t = Table { style: "none".into(), first_row: false, band_row: false, ..Default::default() };
if let Some(pr) = tbl.child("tblPr") { if let Some(pr) = tbl.child("tblPr") {
t.background = pr.elements().find(|e| e.local().ends_with("Fill") && e.local() != "noFill").and_then(|f| dml::fill_el(imp, part, f));
t.first_row = pr.bool("firstRow").unwrap_or(false); t.first_row = pr.bool("firstRow").unwrap_or(false);
t.first_col = pr.bool("firstCol").unwrap_or(false); t.first_col = pr.bool("firstCol").unwrap_or(false);
t.last_row = pr.bool("lastRow").unwrap_or(false); t.last_row = pr.bool("lastRow").unwrap_or(false);

View File

@ -494,6 +494,9 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) {
.t("bandRow", t.band_row) .t("bandRow", t.band_row)
.t("bandCol", t.band_col), .t("bandCol", t.band_col),
); );
if let Some(f) = &t.background {
dml::fill(w, x, o, f);
}
w.elt("a:tableStyleId", &guid); w.elt("a:tableStyleId", &guid);
w.close("a:tblPr"); w.close("a:tblPr");
w.open0("a:tblGrid"); w.open0("a:tblGrid");

View File

@ -780,3 +780,19 @@ fn line_break_options_round_trip() {
assert_eq!(props.latin_line_break, Some(true)); assert_eq!(props.latin_line_break, Some(true));
assert_eq!(props.hanging_punct, Some(false)); assert_eq!(props.hanging_punct, Some(false));
} }
#[test]
fn a_table_fill_round_trips() {
let mut p = defaults::new_presentation(None);
let mut t = Table::new(2, 2, 200.0, 40.0);
t.background = Some(Fill::solid(ColorRef::rgb(Rgba::rgb(0, 112, 192))));
let mut sh = shape(&mut p, "Filled table", 10.0, 10.0, 200.0, 80.0);
sh.kind = deckcraft_model::ShapeKind::Table(t);
let first = p.slides[0].id;
p.slide_mut(first).expect("slide").shapes.push(sh);
let q = round(&p);
let deckcraft_model::ShapeKind::Table(t) = &by_name(&q.slides[0], "Filled table").kind else { panic!("table") };
assert!(
matches!(&t.background, Some(Fill::Solid { color }) if matches!(color.base, deckcraft_model::ColorBase::Rgb { rgb } if rgb == Rgba::rgb(0, 112, 192)))
);
}

View File

@ -300,13 +300,20 @@ pub fn cell_rects(t: &Table, heights: &[f64]) -> Vec<(usize, usize, Rect)> {
pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn deckcraft_text::Fields) { pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn deckcraft_text::Fields) {
let _ = pres; let _ = pres;
let rects = cell_rects(t, &row_heights(rctx, t, fields)); let rects = cell_rects(t, &row_heights(rctx, t, fields));
// A file-defined style's table background, behind every cell. // Table backgrounds behind every cell: a file-defined style's, then the table's own fill.
if let Some(def) = rctx.pres.table_styles.iter().find(|d| d.id.eq_ignore_ascii_case(&t.style)) let style_bg = rctx
&& let Some((bg, ph)) = def.background.as_ref().and_then(|b| style_fill(rctx, b)) .pres
&& let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) .table_styles
{ .iter()
ctx.set_transform(m); .find(|d| d.id.eq_ignore_ascii_case(&t.style))
crate::paint::fill_path(ctx, rctx, &bg, ph, &all.to_path(0.1), all, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres); .and_then(|d| d.background.as_ref())
.and_then(|b| style_fill(rctx, b));
let own_bg = t.background.clone().map(|f| (f, None));
if let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) {
for (bg, ph) in style_bg.into_iter().chain(own_bg) {
ctx.set_transform(m);
crate::paint::fill_path(ctx, rctx, &bg, ph, &all.to_path(0.1), all, m, deckcraft_geom::preset::FillMode::Norm, 1.0, rctx.pres);
}
} }
for (r, c, rect) in &rects { for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue }; let Some(cell) = t.cell(*r, *c) else { continue };

View File

@ -912,3 +912,22 @@ fn a_file_defined_table_style_draws_its_parts() {
let left = px(100, 175); let left = px(100, 175);
assert!(left[2] > 200 && left[0] < 60, "{left:?}"); assert!(left[2] > 200 && left[0] < 60, "{left:?}");
} }
#[test]
fn a_table_fill_shows_behind_cells_without_their_own() {
// `a:tblPr` solidFill (tdf#127237): cells with no fill show the table's blue.
use deckcraft_model::style::Fill;
let mut t = Table::new(2, 2, 200.0, 50.0);
t.style = "none".into();
t.background = Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(0, 112, 192))));
for r in 0..2 {
for c in 0..2 {
t.cell_mut(r, c).unwrap().fill = Some(Fill::None);
}
}
let tbl = Shape { id: ShapeId(795), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), kind: ShapeKind::Table(t), ..Default::default() };
let p = deck_with(vec![tbl]);
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
assert_eq!(img.pixel(150, 160)[..3], [0, 112, 192]);
assert_eq!(img.pixel(50, 50)[..3], [255, 255, 255]);
}