Files
deckcraft/crates/render/src/table.rs
T
Brandon ThomasandClaude Opus 5.5 e62b7ccd15 M0: PowerPoint-style egui UI, slide show engine, icons, desktop app
- ui-egui: title bar, ribbon with every tab (Home, Insert, Draw, Design, Transitions,
  Animations, Slide Show, Record, Review, View) plus contextual Shape/Picture Format,
  Table Design/Layout, Chart Design, Playback and Slide Master tabs; thumbnail pane with
  sections and drag reorder; canvas with selection handles, smart guides, marquee,
  in-place text editing; notes pane; status bar with views and zoom; task panes
  (Format Shape, Background, Animation, Selection, Comments, Design Ideas); dialogs;
  Slide Sorter, Notes Page, Outline view; command palette; control channel
- anim: timelines for every animation effect, transition layers for every transition,
  morph pairing, show navigation (hidden slides, custom shows, loops)
- show: full-screen slide show with transitions, animations, pen, black/white
  screens, presenter view window, rehearse timings, reading view
- icons: 238 original icons drawn in code
- render: filters force single-threaded rendering (regression test), panic guard
- engine: chart type/data/options, SmartArt-style graphics
- apps/slidecraft: desktop host with control server, file dialogs, clipboard images
- placeholder app icon rendered by SlideCraft itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:08:13 -04:00

211 lines
8.0 KiB
Rust

//! Tables: cell fills from the table style (or overrides), borders, and cell text.
use kurbo::{Affine, BezPath, Rect, Shape as _};
use slidecraft_color::{ColorTransform, Rgba, SchemeSlot};
use slidecraft_model::resolve::Ctx;
use slidecraft_model::style::{ColorRef, Fill};
use slidecraft_model::table::Table;
use slidecraft_model::text::Anchor;
use slidecraft_model::{Presentation, Shape};
use vello_cpu::{RenderContext, peniko};
use crate::color;
/// Style colours for a cell: (fill, text colour, bold).
pub struct CellLook {
pub fill: Option<Rgba>,
pub text: Option<Rgba>,
pub bold: bool,
pub border: Option<(Rgba, f64)>,
}
fn accent(ctx: &Ctx, style: &str) -> Rgba {
let slot = match style.rsplit('-').next().unwrap_or("accent1") {
"accent2" => SchemeSlot::Accent2,
"accent3" => SchemeSlot::Accent3,
"accent4" => SchemeSlot::Accent4,
"accent5" => SchemeSlot::Accent5,
"accent6" => SchemeSlot::Accent6,
"tx1" => SchemeSlot::Tx1,
_ => SchemeSlot::Accent1,
};
ctx.color(&ColorRef::scheme(slot), None)
}
pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
let a = accent(ctx, &t.style);
let kind = t.style.split('-').next().unwrap_or("medium2");
let header = t.first_row && r == 0;
let total = t.last_row && r + 1 == t.rows.len();
let first_col = t.first_col && c == 0;
let last_col = t.last_col && c + 1 == t.cols.len();
let body_r = if t.first_row { r.wrapping_sub(1) } else { r };
let band = t.band_row && !header && !total && body_r % 2 == 0;
let band_c = t.band_col && c.is_multiple_of(2);
let tint = |k: i32| slidecraft_color::apply(a, &[ColorTransform::Tint(k)]);
let white = Rgba::WHITE;
let dark_text = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None);
match kind {
"none" => CellLook { fill: None, text: None, bold: false, border: None },
"grid" => CellLook { fill: None, text: None, bold: false, border: Some((dark_text, 0.75)) },
"light1" => CellLook {
fill: if band || band_c { Some(tint(20000)) } else { None },
text: None,
bold: header || total || first_col || last_col,
border: if header { Some((a, 1.0)) } else { None },
},
"light2" => CellLook {
fill: if header { Some(a) } else { None },
text: if header { Some(white) } else { None },
bold: header || total || first_col,
border: Some((a, 1.0)),
},
"dark1" => CellLook {
fill: Some(if header {
Rgba::BLACK
} else if band || band_c {
slidecraft_color::apply(a, &[ColorTransform::Shade(60000)])
} else {
slidecraft_color::apply(a, &[ColorTransform::Shade(75000)])
}),
text: Some(white),
bold: header || total || first_col,
border: Some((white, 1.0)),
},
"medium4" => CellLook {
fill: Some(if header || band || band_c { tint(40000) } else { tint(20000) }),
text: None,
bold: header || total || first_col || last_col,
border: Some((a, 0.75)),
},
_ => CellLook {
// medium2: strong header, banded light tints, white grid.
fill: Some(if header || total || first_col || last_col {
a
} else if band || band_c {
tint(40000)
} else {
tint(20000)
}),
text: if header || total || first_col || last_col { Some(white) } else { None },
bold: header || total || first_col || last_col,
border: Some((white, 1.0)),
},
}
}
/// Cell rectangles in table-local space, honouring merges: (row, col, rect) for visible cells.
pub fn cell_rects(t: &Table) -> Vec<(usize, usize, Rect)> {
let mut out = vec![];
let xs: Vec<f64> = std::iter::once(0.0)
.chain(t.cols.iter().scan(0.0, |a, w| {
*a += w.max(0.0);
Some(*a)
}))
.collect();
let ys: Vec<f64> = std::iter::once(0.0)
.chain(t.rows.iter().scan(0.0, |a, r| {
*a += r.height.max(0.0);
Some(*a)
}))
.collect();
for (ri, row) in t.rows.iter().enumerate() {
for (ci, cell) in row.cells.iter().enumerate().take(t.cols.len()) {
if cell.is_covered() {
continue;
}
let c1 = (ci + cell.grid_span.max(1) as usize).min(t.cols.len());
let r1 = (ri + cell.row_span.max(1) as usize).min(t.rows.len());
let (Some(x0), Some(x1), Some(y0), Some(y1)) = (xs.get(ci), xs.get(c1), ys.get(ri), ys.get(r1)) else { continue };
out.push((ri, ci, Rect::new(*x0, *y0, *x1, *y1)));
}
}
out
}
pub fn draw(ctx: &mut RenderContext, pres: &Presentation, rctx: &Ctx, t: &Table, m: Affine, _w: f64, _h: f64, fields: &dyn slidecraft_text::Fields) {
let _ = pres;
let rects = cell_rects(t);
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
let lk = look(rctx, t, *r, *c);
ctx.set_transform(m);
match &cell.fill {
Some(f) => {
crate::paint::fill_path(ctx, rctx, f, None, &rect.to_path(0.1), *rect, m, slidecraft_geom::preset::FillMode::Norm, 1.0, rctx.pres)
}
None => {
if let Some(fc) = lk.fill {
ctx.set_paint(color(fc, 1.0));
ctx.fill_rect(rect);
}
}
}
}
// Borders: explicit cell borders, else the style's grid.
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
let lk = look(rctx, t, *r, *c);
let edges = [
(rect.x0, rect.y0, rect.x0, rect.y1),
(rect.x1, rect.y0, rect.x1, rect.y1),
(rect.x0, rect.y0, rect.x1, rect.y0),
(rect.x0, rect.y1, rect.x1, rect.y1),
];
for (i, e) in edges.iter().enumerate() {
let line = cell.borders.get(i).and_then(|b| b.as_ref());
let (col, wd) = match line {
Some(l) => match &l.fill {
Some(Fill::Solid { color: cc }) => (rctx.color(cc, None), l.width.unwrap_or(1.0)),
Some(Fill::None) => continue,
_ => continue,
},
None => match lk.border {
Some(b) => b,
None => continue,
},
};
let mut p = BezPath::new();
p.move_to((e.0, e.1));
p.line_to((e.2, e.3));
ctx.set_transform(m);
ctx.set_paint(color(col, 1.0));
ctx.set_stroke(kurbo::Stroke::new(wd.max(0.25)));
ctx.stroke_path(&p);
}
}
// Text.
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
if cell.text.is_empty() {
continue;
}
let lk = look(rctx, t, *r, *c);
let mut body = cell.text.clone();
let [ml, mr, mt, mb] = cell.margins.unwrap_or([7.2, 7.2, 3.6, 3.6]);
body.body.inset_l = Some(ml);
body.body.inset_r = Some(mr);
body.body.inset_t = Some(mt);
body.body.inset_b = Some(mb);
body.body.anchor = Some(cell.anchor.unwrap_or(Anchor::Top));
if cell.vertical.is_some() {
body.body.vert = cell.vertical;
}
for p in &mut body.paragraphs {
for run in &mut p.runs {
if run.props.fill.is_none()
&& let Some(tc) = lk.text
{
run.props.fill = Some(Fill::solid(ColorRef::rgb(tc)));
}
if lk.bold && run.props.bold.is_none() {
run.props.bold = Some(true);
}
}
}
let tmp = Shape { text: Some(body.clone()), ..Default::default() };
crate::draw_text(ctx, rctx, &tmp, &body, *rect, m, fields, None);
let _ = peniko::Fill::NonZero;
}
}