Text: table text takes the master's other-text style

PowerPoint for Mac lays out a table's text in the master's other-text
style (p:otherStyle), while shapes and text boxes take the presentation's
default text style (p:defaultTextStyle). A synthetic deck with an
otherStyle of 14 pt and a defaultTextStyle of 26 pt, exported as PDF,
draws a table cell at 14 pt and a shape's and a text box's text at 26 pt.
DeckCraft laid cells out through a stand-in shape that wasn't a table, so
they took the default text style: a deck whose two styles differ (18 and
20 pt) drew its table text 2 pt too large and wrapped its cells.

master_list_style sends text in a non-placeholder table to otherStyle,
and Shape::table_cell builds the stand-in every cell layout uses (drawing,
row heights, text placement, the caret and selection in a cell) as a
table. The table shape itself resolves the same way, so the formatting
state of a selected table reports the size its cells draw at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dominik Strebel
2026-10-22 08:49:42 +02:00
co-authored by Claude Opus 5.5
parent 128cb0b883
commit aa6f45ba2c
7 changed files with 37 additions and 6 deletions
+2
View File
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| Date | What landed |
|---|---|
| 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 |
| 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
@@ -245,6 +246,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged |
| 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) |
| 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) |
| 2026-10-22 | trivial | Progress log: table text takes the master's other-text style |
| 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) |
| 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing |
| 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha |
+2 -2
View File
@@ -934,7 +934,7 @@ pub fn cell_range_underlines(
return vec![];
};
let body = deckcraft_render::table_cell_body(&ctx, tb, row, col, cell);
let tmp = deckcraft_model::Shape { text: Some(body.clone()), ..Default::default() };
let tmp = deckcraft_model::Shape::table_cell(body.clone());
let l = deckcraft_text::layout(
&ctx,
&tmp,
@@ -1003,7 +1003,7 @@ fn cell_on_slide(st: &DocState, id: ShapeId, row: usize, col: usize) -> Option<C
if body.paragraphs.is_empty() {
body.paragraphs.push(deckcraft_model::text::Paragraph::default());
}
let tmp = deckcraft_model::Shape { text: Some(body.clone()), ..Default::default() };
let tmp = deckcraft_model::Shape::table_cell(body.clone());
let layout = deckcraft_text::layout(
&ctx,
&tmp,
+5
View File
@@ -439,6 +439,11 @@ impl Default for Shape {
}
impl Shape {
/// A stand-in shape for laying out one table cell's `body`: a table, so the cell's text takes
/// the master's other-text style as PowerPoint's does.
pub fn table_cell(body: text::TextBody) -> Shape {
Shape { text: Some(body), kind: ShapeKind::Table(Table::default()), ..Default::default() }
}
pub fn is_placeholder(&self) -> bool {
self.ph.is_some()
}
+5 -1
View File
@@ -100,7 +100,10 @@ pub fn parents<'a>(ctx: &Ctx<'a>, shape: &Shape) -> (Option<&'a Shape>, Option<&
}
}
/// The text style group (title / body / other) a shape draws from.
/// The text style group (title / body / other) a shape draws from. Text in a table that isn't a
/// placeholder takes the master's other-text style, as in PowerPoint for Mac (a table cell is 14 pt
/// under an `otherStyle` of 14 pt and a `defaultTextStyle` of 26 pt, while a shape's or a text
/// box's text is 26 pt); other shapes take the presentation's default text style.
pub fn master_list_style<'a>(ctx: &Ctx<'a>, shape: &Shape) -> &'a ListStyle {
match shape.ph_type() {
Some(PhType::Title | PhType::CtrTitle) => &ctx.master.title_style,
@@ -116,6 +119,7 @@ pub fn master_list_style<'a>(ctx: &Ctx<'a>, shape: &Shape) -> &'a ListStyle {
| PhType::ClipArt,
) => &ctx.master.body_style,
Some(_) => &ctx.master.other_style,
None if matches!(shape.kind, crate::ShapeKind::Table(_)) => &ctx.master.other_style,
None => &ctx.pres.default_text_style,
}
}
+20
View File
@@ -270,3 +270,23 @@ fn ink_width_follows_pressure_and_keeps_the_old_placeholder() {
assert!((floor - 0.64).abs() < 1e-4, "a light touch stays visible, got {floor}");
assert!((ink_sample_width(8.0, f32::NAN, false) - 8.0).abs() < 1e-9);
}
#[test]
fn table_text_takes_the_masters_other_style_and_shapes_the_default_text_style() {
// PowerPoint for Mac, with an otherStyle of 14 pt and a defaultTextStyle of 26 pt: a table
// cell's text is 14 pt, a shape's and a text box's 26 pt.
let mut p = Presentation::default();
let size = |pt: f64| crate::text::LevelStyle { run: RunProps { size: Some(pt), ..Default::default() }, ..Default::default() };
p.default_text_style.set(0, size(26.0));
std::sync::Arc::make_mut(&mut p.masters[0]).other_style.set(0, size(14.0));
let s = p.slides[0].clone();
let ctx = Ctx::for_slide(&p, &s).unwrap();
let para = Paragraph::default();
let pt = |sh: &Shape| resolve::run(&ctx, sh, &para, &RunProps::default()).size;
let body = crate::text::TextBody::from_text("Cell");
assert_eq!(pt(&Shape::table_cell(body.clone())), Some(14.0), "a table cell");
let table = Shape { kind: ShapeKind::Table(Table::default()), ..Default::default() };
assert_eq!(pt(&table), Some(14.0), "the table itself (its formatting state)");
assert_eq!(pt(&Shape { text: Some(body.clone()), ..Default::default() }), Some(26.0), "a shape");
assert_eq!(pt(&Shape { text: Some(body), text_box: true, ..Default::default() }), Some(26.0), "a text box");
}
+1 -1
View File
@@ -134,7 +134,7 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent:
for (r, c, rect) in table::cell_rects(t, &table::row_heights(rctx, t, fields)) {
let Some(cell) = t.cell(r, c).filter(|cell| !cell.text.is_empty()) else { continue };
let body = table::cell_body(rctx, t, r, c, cell);
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
let tmp = Shape::table_cell(body.clone());
place_text(out, rctx, fields, s.id, &tmp, &body, rect, m);
}
}
+2 -2
View File
@@ -266,7 +266,7 @@ pub fn row_heights(rctx: &Ctx, t: &Table, fields: &dyn Fields) -> Vec<f64> {
let c1 = ci.saturating_add(cell.grid_span.max(1) as usize).min(t.cols.len());
let (Some(x0), Some(x1)) = (xs.get(ci), xs.get(c1)) else { continue };
let body = cell_body(rctx, t, ri, ci, cell);
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
let tmp = Shape::table_cell(body.clone());
let need = deckcraft_text::fit_height(rctx, &tmp, &body, Rect::new(*x0, 0.0, *x1, h.max(1.0)), fields);
if need.is_finite() {
h = h.max(need.min(20_000.0));
@@ -423,7 +423,7 @@ pub fn draw(
continue;
}
let body = cell_body(rctx, t, *r, *c, cell);
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
let tmp = Shape::table_cell(body.clone());
#[cfg(test)]
TEXT_CELLS_DRAWN.with(|n| n.set(n.get() + 1));
let mut l = deckcraft_text::layout(rctx, &tmp, &body, &deckcraft_text::Opts { rect: *rect, fields, prompt_color: None, no_shrink: false });