1use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
4use deckcraft_model::resolve::Ctx;
5use deckcraft_model::style::{ColorRef, Fill};
6use deckcraft_model::table::{Cell, Table, TableBorder, TablePart, TablePartStyle, TableStyleDef};
7use deckcraft_model::text::{Anchor, TextBody, TextDir};
8use deckcraft_model::{Presentation, Shape};
9use deckcraft_text::Fields;
10use kurbo::{Affine, BezPath, Rect, Shape as _};
11use vello_cpu::{RenderContext, peniko};
12
13use crate::color;
14
15pub struct CellLook {
17 pub fill: Option<Rgba>,
18 pub text: Option<Rgba>,
19 pub bold: bool,
20 pub border: Option<(Rgba, f64)>,
21 pub sides: [Option<(Rgba, f64)>; 4],
24 pub paint: Option<(Fill, Option<Rgba>)>,
27}
28
29fn accent(ctx: &Ctx, style: &str) -> Rgba {
30 let slot = match style.rsplit('-').next().unwrap_or("accent1") {
31 "accent2" => SchemeSlot::Accent2,
32 "accent3" => SchemeSlot::Accent3,
33 "accent4" => SchemeSlot::Accent4,
34 "accent5" => SchemeSlot::Accent5,
35 "accent6" => SchemeSlot::Accent6,
36 "tx1" => SchemeSlot::Tx1,
37 _ => SchemeSlot::Accent1,
38 };
39 ctx.color(&ColorRef::scheme(slot), None)
40}
41
42fn look_def(ctx: &Ctx, def: &TableStyleDef, t: &Table, r: usize, c: usize) -> CellLook {
46 let (rows, cols) = (t.rows.len(), t.cols.len());
47 let (r1, c1) = t
48 .cell(r, c)
49 .map(|cell| {
50 (
51 r + cell.row_span.max(1) as usize - 1,
52 c + cell.grid_span.max(1) as usize - 1,
53 )
54 })
55 .unwrap_or((r, c));
56 let (r1, c1) = (
57 r1.min(rows.saturating_sub(1)),
58 c1.min(cols.saturating_sub(1)),
59 );
60 let last_r = rows.saturating_sub(1);
61 let last_c = cols.saturating_sub(1);
62 let header = t.first_row && r == 0;
63 let total = t.last_row && r1 == last_r && rows > 1;
64 let first_col = t.first_col && c == 0;
65 let last_col = t.last_col && c1 == last_c && cols > 1;
66 let body_r = r.saturating_sub(usize::from(t.first_row));
67 let body_c = c.saturating_sub(usize::from(t.first_col));
68 let mut look = CellLook {
69 fill: None,
70 text: None,
71 bold: false,
72 border: None,
73 sides: [None; 4],
74 paint: None,
75 };
76 for part in TablePart::ALL {
77 let (on, region) = match part {
79 TablePart::WholeTbl => (true, (0, last_r, 0, last_c)),
80 TablePart::Band1H => (
81 t.band_row && !header && !total && body_r % 2 == 0,
82 (r, r1, 0, last_c),
83 ),
84 TablePart::Band2H => (
85 t.band_row && !header && !total && body_r % 2 == 1,
86 (r, r1, 0, last_c),
87 ),
88 TablePart::Band1V => (
89 t.band_col && !first_col && !last_col && body_c % 2 == 0,
90 (0, last_r, c, c1),
91 ),
92 TablePart::Band2V => (
93 t.band_col && !first_col && !last_col && body_c % 2 == 1,
94 (0, last_r, c, c1),
95 ),
96 TablePart::LastCol => (last_col, (0, last_r, c, last_c)),
97 TablePart::FirstCol => (first_col, (0, last_r, 0, c1)),
98 TablePart::LastRow => (total, (r, last_r, 0, last_c)),
99 TablePart::FirstRow => (header, (0, r1, 0, last_c)),
100 TablePart::SeCell => (total && last_col, (r, r1, c, c1)),
101 TablePart::SwCell => (total && first_col, (r, r1, c, c1)),
102 TablePart::NeCell => (header && last_col, (r, r1, c, c1)),
103 TablePart::NwCell => (header && first_col, (r, r1, c, c1)),
104 };
105 if !on {
106 continue;
107 }
108 let Some(ps) = def.part(part) else { continue };
109 if let Some(p) = style_fill(ctx, ps) {
110 look.fill = fill_color(ctx, &p.0, p.1);
111 look.paint = Some(p);
112 }
113 if let Some(tc) = &ps.text_color {
114 look.text = Some(ctx.color(tc, None));
115 }
116 if let Some(b) = ps.bold {
117 look.bold = b;
118 }
119 let (r0, rr1, c0, cc1) = region;
120 let pick = [
122 if c == c0 { 0 } else { 5 },
123 if c1 == cc1 { 1 } else { 5 },
124 if r == r0 { 2 } else { 4 },
125 if r1 == rr1 { 3 } else { 4 },
126 ];
127 for (side, i) in pick.iter().enumerate() {
128 if let (Some(Some(b)), Some(slot)) = (ps.borders.get(*i), look.sides.get_mut(side)) {
129 *slot = Some(border_look(ctx, b));
130 }
131 }
132 }
133 look
134}
135
136fn style_fill(ctx: &Ctx, ps: &TablePartStyle) -> Option<(Fill, Option<Rgba>)> {
139 if let Some((idx, col)) = &ps.fill_ref {
140 let ph = ctx.color(col, None);
141 return Some(
142 match ctx
143 .master
144 .theme
145 .format
146 .fills
147 .get((*idx as usize).saturating_sub(1))
148 {
149 Some(f) if *idx > 0 => (f.clone(), Some(ph)),
150 _ if *idx == 0 => (Fill::None, None),
151 _ => (Fill::solid(ColorRef::rgb(ph)), None),
152 },
153 );
154 }
155 ps.fill.clone().map(|f| (f, None))
156}
157
158fn fill_color(ctx: &Ctx, f: &Fill, ph: Option<Rgba>) -> Option<Rgba> {
160 match f {
161 Fill::None => Some(Rgba::TRANSPARENT),
162 Fill::Solid { color: c } => Some(ctx.color(c, ph)),
163 Fill::Gradient(g) => g.stops.first().map(|s| ctx.color(&s.color, ph)),
164 Fill::Pattern(p) => Some(ctx.color(&p.bg, ph)),
165 _ => None,
166 }
167}
168
169fn border_look(ctx: &Ctx, b: &TableBorder) -> (Rgba, f64) {
171 match b {
172 TableBorder::Line { line } => match &line.fill {
173 Some(Fill::Solid { color: c }) => (ctx.color(c, None), line.width.unwrap_or(0.75)),
174 Some(Fill::Gradient(g)) => (
175 g.stops
176 .first()
177 .map(|s| ctx.color(&s.color, None))
178 .unwrap_or(Rgba::BLACK),
179 line.width.unwrap_or(0.75),
180 ),
181 _ => (Rgba::TRANSPARENT, 0.0),
182 },
183 TableBorder::Ref { idx, color } => {
184 let w = ctx
185 .master
186 .theme
187 .format
188 .lines
189 .get((*idx as usize).saturating_sub(1))
190 .and_then(|l| l.width)
191 .unwrap_or(0.75);
192 if *idx == 0 {
193 (Rgba::TRANSPARENT, 0.0)
194 } else {
195 (ctx.color(color, None), w)
196 }
197 }
198 }
199}
200
201pub fn look(ctx: &Ctx, t: &Table, r: usize, c: usize) -> CellLook {
202 if let Some(def) = ctx
203 .pres
204 .table_styles
205 .iter()
206 .find(|d| d.id.eq_ignore_ascii_case(&t.style))
207 {
208 return look_def(ctx, def, t, r, c);
209 }
210 let a = accent(ctx, &t.style);
211 let kind = t.style.split('-').next().unwrap_or("medium2");
212 let header = t.first_row && r == 0;
213 let total = t.last_row && r + 1 == t.rows.len();
214 let first_col = t.first_col && c == 0;
215 let last_col = t.last_col && c + 1 == t.cols.len();
216 let body_r = if t.first_row { r.wrapping_sub(1) } else { r };
217 let band = t.band_row && !header && !total && body_r % 2 == 0;
218 let band_c = t.band_col && c.is_multiple_of(2);
219 let tint = |k: i32| deckcraft_color::apply(a, &[ColorTransform::Tint(k)]);
220 let white = Rgba::WHITE;
221 let dark_text = ctx.color(&ColorRef::scheme(SchemeSlot::Tx1), None);
222 match kind {
223 "none" => CellLook {
224 fill: None,
225 text: None,
226 bold: false,
227 border: None,
228 sides: [None; 4],
229 paint: None,
230 },
231 "grid" => CellLook {
232 fill: None,
233 text: None,
234 bold: false,
235 border: Some((dark_text, 0.75)),
236 sides: [None; 4],
237 paint: None,
238 },
239 "light1" => CellLook {
240 fill: if band || band_c {
241 Some(tint(20000))
242 } else {
243 None
244 },
245 text: None,
246 bold: header || total || first_col || last_col,
247 border: if header { Some((a, 1.0)) } else { None },
248 sides: [None; 4],
249 paint: None,
250 },
251 "light2" => CellLook {
252 fill: if header { Some(a) } else { None },
253 text: if header { Some(white) } else { None },
254 bold: header || total || first_col,
255 border: Some((a, 1.0)),
256 sides: [None; 4],
257 paint: None,
258 },
259 "dark1" => CellLook {
260 fill: Some(if header {
261 Rgba::BLACK
262 } else if band || band_c {
263 deckcraft_color::apply(a, &[ColorTransform::Shade(60000)])
264 } else {
265 deckcraft_color::apply(a, &[ColorTransform::Shade(75000)])
266 }),
267 text: Some(white),
268 bold: header || total || first_col,
269 border: Some((white, 1.0)),
270 sides: [None; 4],
271 paint: None,
272 },
273 "medium4" => CellLook {
274 fill: Some(if header || band || band_c {
275 tint(40000)
276 } else {
277 tint(20000)
278 }),
279 text: None,
280 bold: header || total || first_col || last_col,
281 border: Some((a, 0.75)),
282 sides: [None; 4],
283 paint: None,
284 },
285 _ => CellLook {
286 fill: Some(if header || total || first_col || last_col {
288 a
289 } else if band || band_c {
290 tint(40000)
291 } else {
292 tint(20000)
293 }),
294 text: if header || total || first_col || last_col {
295 Some(white)
296 } else {
297 None
298 },
299 bold: header || total || first_col || last_col,
300 border: Some((white, 1.0)),
301 sides: [None; 4],
302 paint: None,
303 },
304 }
305}
306
307fn edges(sizes: &[f64]) -> Vec<f64> {
309 std::iter::once(0.0)
310 .chain(sizes.iter().scan(0.0, |a, s| {
311 *a += s.max(0.0);
312 Some(*a)
313 }))
314 .collect()
315}
316
317pub(crate) fn cell_body(rctx: &Ctx, t: &Table, r: usize, c: usize, cell: &Cell) -> TextBody {
319 let lk = look(rctx, t, r, c);
320 let mut body = cell.text.clone();
321 let [ml, mr, mt, mb] = cell.margins.unwrap_or([7.2, 7.2, 3.6, 3.6]);
322 body.body.inset_l = Some(ml);
323 body.body.inset_r = Some(mr);
324 body.body.inset_t = Some(mt);
325 body.body.inset_b = Some(mb);
326 body.body.anchor = Some(cell.anchor.unwrap_or(Anchor::Top));
327 if cell.vertical.is_some() {
328 body.body.vert = cell.vertical;
329 }
330 for p in &mut body.paragraphs {
331 for run in &mut p.runs {
332 if run.props.fill.is_none()
333 && let Some(tc) = lk.text
334 {
335 run.props.fill = Some(Fill::solid(ColorRef::rgb(tc)));
336 }
337 if lk.bold && run.props.bold.is_none() {
338 run.props.bold = Some(true);
339 }
340 }
341 }
342 body
343}
344
345pub fn row_heights(rctx: &Ctx, t: &Table, fields: &dyn Fields) -> Vec<f64> {
349 let xs = edges(&t.cols);
350 let mut out = Vec::with_capacity(t.rows.len());
351 for (ri, row) in t.rows.iter().enumerate() {
352 let mut h = row.height.max(0.0);
353 for (ci, cell) in row.cells.iter().enumerate().take(t.cols.len()) {
354 if cell.is_covered()
355 || cell.row_span > 1
356 || !matches!(cell.vertical, None | Some(TextDir::Horizontal))
357 {
358 continue;
359 }
360 let c1 = ci
361 .saturating_add(cell.grid_span.max(1) as usize)
362 .min(t.cols.len());
363 let (Some(x0), Some(x1)) = (xs.get(ci), xs.get(c1)) else {
364 continue;
365 };
366 let body = cell_body(rctx, t, ri, ci, cell);
367 let tmp = Shape {
368 text: Some(body.clone()),
369 ..Default::default()
370 };
371 let need = deckcraft_text::fit_height(
372 rctx,
373 &tmp,
374 &body,
375 Rect::new(*x0, 0.0, *x1, h.max(1.0)),
376 fields,
377 );
378 if need.is_finite() {
379 h = h.max(need.min(20_000.0));
380 }
381 }
382 out.push(h);
383 }
384 out
385}
386
387pub fn cell_rects(t: &Table, heights: &[f64]) -> Vec<(usize, usize, Rect)> {
390 let mut out = vec![];
391 let xs = edges(&t.cols);
392 let ys = edges(heights);
393 for (ri, row) in t.rows.iter().enumerate() {
394 for (ci, cell) in row.cells.iter().enumerate().take(t.cols.len()) {
395 if cell.is_covered() {
396 continue;
397 }
398 let c1 = (ci + cell.grid_span.max(1) as usize).min(t.cols.len());
399 let r1 = (ri + cell.row_span.max(1) as usize).min(t.rows.len());
400 let (Some(x0), Some(x1), Some(y0), Some(y1)) =
401 (xs.get(ci), xs.get(c1), ys.get(ri), ys.get(r1))
402 else {
403 continue;
404 };
405 out.push((ri, ci, Rect::new(*x0, *y0, *x1, *y1)));
406 }
407 }
408 out
409}
410
411#[allow(clippy::too_many_arguments)]
413pub fn draw(
414 ctx: &mut RenderContext,
415 pres: &Presentation,
416 rctx: &Ctx,
417 t: &Table,
418 m: Affine,
419 _w: f64,
420 _h: f64,
421 fields: &dyn deckcraft_text::Fields,
422) -> Option<kurbo::Rect> {
423 let _ = pres;
424 let rects = cell_rects(t, &row_heights(rctx, t, fields));
425 let covered = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b));
426 let style_bg = rctx
428 .pres
429 .table_styles
430 .iter()
431 .find(|d| d.id.eq_ignore_ascii_case(&t.style))
432 .and_then(|d| d.background.as_ref())
433 .and_then(|b| style_fill(rctx, b));
434 let own_bg = t.background.clone().map(|f| (f, None));
435 if let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b)) {
436 for (bg, ph) in style_bg.into_iter().chain(own_bg) {
437 ctx.set_transform(m);
438 crate::paint::fill_path(
439 ctx,
440 rctx,
441 &bg,
442 ph,
443 &all.to_path(0.1),
444 all,
445 m,
446 deckcraft_geom::preset::FillMode::Norm,
447 1.0,
448 rctx.pres,
449 );
450 }
451 }
452 for (r, c, rect) in &rects {
453 let Some(cell) = t.cell(*r, *c) else { continue };
454 let lk = look(rctx, t, *r, *c);
455 ctx.set_transform(m);
456 match &cell.fill {
457 Some(f) => crate::paint::fill_path(
458 ctx,
459 rctx,
460 f,
461 None,
462 &rect.to_path(0.1),
463 *rect,
464 m,
465 deckcraft_geom::preset::FillMode::Norm,
466 1.0,
467 rctx.pres,
468 ),
469 None => match &lk.paint {
470 Some((f, ph)) => crate::paint::fill_path(
471 ctx,
472 rctx,
473 f,
474 *ph,
475 &rect.to_path(0.1),
476 *rect,
477 m,
478 deckcraft_geom::preset::FillMode::Norm,
479 1.0,
480 rctx.pres,
481 ),
482 None => {
483 if let Some(fc) = lk.fill.filter(|f| f.a > 0) {
484 ctx.set_paint(color(fc, 1.0));
485 ctx.fill_rect(rect);
486 }
487 }
488 },
489 }
490 }
491 for (r, c, rect) in &rects {
493 let Some(cell) = t.cell(*r, *c) else { continue };
494 let lk = look(rctx, t, *r, *c);
495 let edges = [
496 (rect.x0, rect.y0, rect.x0, rect.y1),
497 (rect.x1, rect.y0, rect.x1, rect.y1),
498 (rect.x0, rect.y0, rect.x1, rect.y0),
499 (rect.x0, rect.y1, rect.x1, rect.y1),
500 ];
501 for (i, e) in edges.iter().enumerate() {
502 let line = cell.borders.get(i).and_then(|b| b.as_ref());
503 let (col, wd) = match line {
504 Some(l) => match &l.fill {
505 Some(Fill::Solid { color: cc }) => {
506 (rctx.color(cc, None), l.width.unwrap_or(1.0))
507 }
508 Some(Fill::None) => continue,
509 _ => continue,
510 },
511 None => match lk.sides.get(i).copied().flatten().or(lk.border) {
512 Some(b) if b.0.a > 0 && b.1 > 0.0 => b,
513 _ => continue,
514 },
515 };
516 let mut p = BezPath::new();
517 p.move_to((e.0, e.1));
518 p.line_to((e.2, e.3));
519 ctx.set_transform(m);
520 ctx.set_paint(color(col, 1.0));
521 ctx.set_stroke(kurbo::Stroke::new(wd.max(0.25)));
522 ctx.stroke_path(&p);
523 }
524 }
525 for (r, c, rect) in &rects {
527 let Some(cell) = t.cell(*r, *c) else { continue };
528 if cell.text.is_empty() {
529 continue;
530 }
531 let body = cell_body(rctx, t, *r, *c, cell);
532 let tmp = Shape {
533 text: Some(body.clone()),
534 ..Default::default()
535 };
536 crate::draw_text(ctx, rctx, &tmp, &body, *rect, m, fields, None);
537 let _ = peniko::Fill::NonZero;
538 }
539 covered
540}