mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64720de9a3 | ||
|
|
35229a6409 | ||
|
|
a38da4bff8 |
@@ -55,9 +55,6 @@ pub struct Table {
|
||||
pub last_col: bool,
|
||||
pub band_row: 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 {
|
||||
@@ -72,7 +69,6 @@ impl Default for Table {
|
||||
last_col: false,
|
||||
band_row: true,
|
||||
band_col: false,
|
||||
background: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,6 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) ->
|
||||
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() };
|
||||
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_col = pr.bool("firstCol").unwrap_or(false);
|
||||
t.last_row = pr.bool("lastRow").unwrap_or(false);
|
||||
|
||||
@@ -494,9 +494,6 @@ fn table(w: &mut W, x: &mut Exp, o: &mut Out, t: &Table) {
|
||||
.t("bandRow", t.band_row)
|
||||
.t("bandCol", t.band_col),
|
||||
);
|
||||
if let Some(f) = &t.background {
|
||||
dml::fill(w, x, o, f);
|
||||
}
|
||||
w.elt("a:tableStyleId", &guid);
|
||||
w.close("a:tblPr");
|
||||
w.open0("a:tblGrid");
|
||||
|
||||
@@ -780,19 +780,3 @@ fn line_break_options_round_trip() {
|
||||
assert_eq!(props.latin_line_break, Some(true));
|
||||
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)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,20 +300,13 @@ 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) {
|
||||
let _ = pres;
|
||||
let rects = cell_rects(t, &row_heights(rctx, t, fields));
|
||||
// Table backgrounds behind every cell: a file-defined style's, then the table's own fill.
|
||||
let style_bg = rctx
|
||||
.pres
|
||||
.table_styles
|
||||
.iter()
|
||||
.find(|d| d.id.eq_ignore_ascii_case(&t.style))
|
||||
.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);
|
||||
}
|
||||
// A file-defined style's table background, behind every cell.
|
||||
if let Some(def) = rctx.pres.table_styles.iter().find(|d| d.id.eq_ignore_ascii_case(&t.style))
|
||||
&& let Some((bg, ph)) = def.background.as_ref().and_then(|b| style_fill(rctx, b))
|
||||
&& let Some(all) = rects.iter().map(|(_, _, r)| *r).reduce(|a, b| a.union(b))
|
||||
{
|
||||
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 {
|
||||
let Some(cell) = t.cell(*r, *c) else { continue };
|
||||
|
||||
@@ -912,22 +912,3 @@ fn a_file_defined_table_style_draws_its_parts() {
|
||||
let left = px(100, 175);
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -493,17 +493,6 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64,
|
||||
// Symbol fonts (Wingdings) aren't available: map common private-use bullets.
|
||||
let ch = map_symbol_bullet(char, props.bullet_font.as_deref());
|
||||
bullet_face(db, &mut st, &ch);
|
||||
|
||||
// A symbol font (Wingdings, Symbol…) that is installed draws its own glyph, found
|
||||
// at U+F000 + the character code as such fonts map them; otherwise common symbol
|
||||
// bullets are mapped to Unicode look-alikes.
|
||||
let ch = match symbol_font_char(&st.face, char, props.bullet_font.as_deref()) {
|
||||
Some(c) => c.to_string(),
|
||||
None => map_symbol_bullet(char, props.bullet_font.as_deref()),
|
||||
};
|
||||
if !st.face.covers(ch.chars().next().unwrap_or('•')) {
|
||||
st.face = db.face("Inter", "Regular");
|
||||
}
|
||||
Some((ch, st))
|
||||
}
|
||||
Some(Bullet::AutoNum { scheme, start_at }) => {
|
||||
@@ -726,59 +715,11 @@ fn shape_item(
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `family` a symbol font (characters at U+F000 + code)?
|
||||
fn is_symbol_font(family: &str) -> bool {
|
||||
let f = family.to_ascii_lowercase();
|
||||
f.starts_with("wingdings") || f == "symbol" || f.starts_with("webdings") || f.starts_with("marlett")
|
||||
}
|
||||
|
||||
/// The character to draw bullet `c` with the installed symbol font `face`, when `font` names a
|
||||
/// symbol font that `face` is (not a substitute) and it has the glyph.
|
||||
fn symbol_font_char(face: &FontFace, c: &str, font: Option<&str>) -> Option<char> {
|
||||
let font = font?;
|
||||
if !is_symbol_font(font) || !face.family.eq_ignore_ascii_case(font) {
|
||||
return None;
|
||||
}
|
||||
let first = c.chars().next()?;
|
||||
let pua = char::from_u32(0xF000 | ((first as u32) & 0xFF))?;
|
||||
if face.covers(pua) {
|
||||
Some(pua)
|
||||
} else if face.covers(first) {
|
||||
Some(first)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn map_symbol_bullet(c: &str, font: Option<&str>) -> String {
|
||||
let f = font.unwrap_or("").to_ascii_lowercase();
|
||||
let first = c.chars().next().unwrap_or('•');
|
||||
if f.contains("wingdings") || f.contains("symbol") || ('\u{f000}'..='\u{f0ff}').contains(&first) {
|
||||
let code = (first as u32) & 0xff;
|
||||
// Wingdings 2 and 3 put different shapes at the same codes.
|
||||
if f.starts_with("wingdings 2") {
|
||||
let m = match code {
|
||||
0x97 | 0x98 => '●',
|
||||
0x9e..=0xa1 => '■',
|
||||
0xa2..=0xa5 => '◆',
|
||||
0xea..=0xf3 => '★',
|
||||
0xb7..=0xbf => '○',
|
||||
0x50 => '✓',
|
||||
_ => '•',
|
||||
};
|
||||
return m.to_string();
|
||||
}
|
||||
if f.starts_with("wingdings 3") {
|
||||
let m = match code {
|
||||
0x7d | 0x75 => '►',
|
||||
0x7c | 0x74 => '◄',
|
||||
0x70 => '▲',
|
||||
0x71 => '▼',
|
||||
0xc6 | 0xc7 => '➔',
|
||||
_ => '▶',
|
||||
};
|
||||
return m.to_string();
|
||||
}
|
||||
let m = match code {
|
||||
0xa7 => '▪',
|
||||
0xd8 => '➢',
|
||||
|
||||
@@ -624,28 +624,3 @@ fn a_bullet_the_text_font_lacks_falls_back_to_a_face_that_has_it() {
|
||||
assert!(b.glyphs.iter().all(|g| g.0 != 0), "no missing-glyph box");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_font_bullets_map_by_font() {
|
||||
// Wingdings 2 code 0xEA is a star, Wingdings 3 0x7D a right-pointing triangle; plain
|
||||
// Wingdings keeps its own table (0xA7 is a small square).
|
||||
let star = String::from_utf8(vec![0xEF, 0x83, 0xAA]).unwrap(); // U+F0EA
|
||||
assert_eq!(map_symbol_bullet(&star, Some("Wingdings 2")), "★");
|
||||
assert_eq!(map_symbol_bullet("\u{7d}", Some("Wingdings 3")), "►");
|
||||
assert_eq!(map_symbol_bullet("\u{f0a7}", Some("Wingdings")), "▪");
|
||||
assert_eq!(map_symbol_bullet("•", Some("Arial")), "•");
|
||||
assert!(is_symbol_font("Wingdings 2") && is_symbol_font("Symbol") && !is_symbol_font("Arial"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_installed_symbol_font_draws_its_own_bullet() {
|
||||
// Where the deck's symbol font is installed, its glyph is drawn (as PowerPoint does), not a
|
||||
// look-alike. Skipped without Wingdings 2.
|
||||
let db = FontDb::global();
|
||||
if !db.has_family("Wingdings 2") {
|
||||
return;
|
||||
}
|
||||
let face = db.face("Wingdings 2", "Regular");
|
||||
assert_eq!(symbol_font_char(&face, "\u{ea}", Some("Wingdings 2")), Some('\u{f0ea}'));
|
||||
assert_eq!(symbol_font_char(&face, "\u{ea}", Some("Arial")), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user