mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-07 02:41:55 +00:00
- 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>
211 lines
8.0 KiB
Rust
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;
|
|
}
|
|
}
|