mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
text: a bare normAutofit draws at 100% and an edit stores the fitted scale (#724)
This commit is contained in:
@@ -1089,7 +1089,7 @@ fn columns(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
fn autofit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let m = match str_param(p, "mode").unwrap_or("none") {
|
||||
"shrink" => AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 },
|
||||
"shrink" => AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 },
|
||||
"resize" | "shape" => AutoFit::Shape,
|
||||
_ => AutoFit::None,
|
||||
};
|
||||
|
||||
@@ -541,7 +541,7 @@ fn shadow(blur: f64, dist: f64) -> Shadow {
|
||||
/// `title` false every line is a bullet (the bullet boxes of the bullet lists).
|
||||
fn text_shape(preset: &str, x: Xfrm, lines: &[&DiagramNode], title: bool) -> Shape {
|
||||
let mut body = TextBody::default();
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 });
|
||||
let top = lines.first().map(|n| if title { n.level } else { n.level.saturating_sub(1) }).unwrap_or(0);
|
||||
for (k, n) in lines.iter().enumerate() {
|
||||
let mut para = Paragraph::new(&n.text);
|
||||
@@ -1913,7 +1913,7 @@ fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
body.paragraphs.push(Paragraph::new(""));
|
||||
}
|
||||
body.body.wrap = Some(true);
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 });
|
||||
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
|
||||
let tb =
|
||||
Shape { id, name: "Text".into(), xfrm: Some(area), text_box: true, text: Some(body), geom: Geom::preset("rect"), ..Default::default() };
|
||||
|
||||
@@ -119,9 +119,14 @@ pub(crate) fn edit_text<T>(s: &mut Session, f: impl FnOnce(&mut TextBody, &mut T
|
||||
let mut t = t0.clone();
|
||||
let body = body_mut(doc, sel, &t).ok_or_else(|| bad("text", "the text being edited is gone"))?;
|
||||
let r = f(body, &mut t);
|
||||
// The saved shrink-on-overflow scale was for the old text: fit the new text afresh.
|
||||
// The saved shrink-on-overflow scale was for the old text: fit the new text afresh. A
|
||||
// body whose file carried no `fontScale` keeps none (100 %) here; `fit_shrink_scale`
|
||||
// below fills in the computed scale when the new text overflows (#724).
|
||||
if let Some(AutoFit::Shrink { font_scale, line_reduction }) = &mut body.body.autofit {
|
||||
(*font_scale, *line_reduction) = (1.0, 0.0);
|
||||
if font_scale.is_some() {
|
||||
*font_scale = Some(1.0);
|
||||
}
|
||||
*line_reduction = 0.0;
|
||||
}
|
||||
sel.text = Some(t);
|
||||
Ok(r)
|
||||
@@ -132,8 +137,11 @@ pub(crate) fn edit_text<T>(s: &mut Session, f: impl FnOnce(&mut TextBody, &mut T
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Grow/shrink an auto-sizing text box (Resize shape to fit text) to its text.
|
||||
/// Refit a shape after its text changed: remember the shrink-on-overflow scale the layout
|
||||
/// computes (#724), then grow/shrink an auto-sizing text box (Resize shape to fit text) to its
|
||||
/// text.
|
||||
pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
|
||||
fit_shrink_scale(s, id)?;
|
||||
let st = s.doc()?;
|
||||
let Some(sh) = st.shape(id).cloned() else { return Ok(()) };
|
||||
let Some(body) = sh.text.clone() else { return Ok(()) };
|
||||
@@ -171,6 +179,53 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store the shrink-on-overflow scale the layout computes for `id`, so a save keeps
|
||||
/// `fontScale`/`lnSpcReduction` in step with what DeckCraft draws (#724). Runs after a text
|
||||
/// change: a body with no meaningful saved scale — a file's bare `<a:normAutofit/>` (`None`) or
|
||||
/// the reset an edit leaves (`1.0`, no reduction) — is fitted afresh; a scale the file saved is
|
||||
/// left as it is, as PowerPoint draws it.
|
||||
fn fit_shrink_scale(s: &mut Session, id: ShapeId) -> Result<()> {
|
||||
let st = s.doc()?;
|
||||
let Some(sh) = st.shape(id).cloned() else { return Ok(()) };
|
||||
let Some(body) = sh.text.clone() else { return Ok(()) };
|
||||
let Some(AutoFit::Shrink { font_scale, line_reduction }) = body.body.autofit else { return Ok(()) };
|
||||
if !(font_scale.is_none() || (font_scale == Some(1.0) && line_reduction == 0.0)) {
|
||||
return Ok(());
|
||||
}
|
||||
let doc = st.doc.clone();
|
||||
let sel = st.selection.clone();
|
||||
let Some(ctx) = ctx_for(&doc, &sel) else { return Ok(()) };
|
||||
let x = xfrm_of(&doc, &sel, &sh);
|
||||
let tr = deckcraft_render::shape_geometry(&sh, x.w, x.h).text_rect;
|
||||
// Lay out as if the scale had just been reset, so the fit is computed even for `None` (which
|
||||
// is drawn at 100 % and only fitted again when its text changes).
|
||||
let mut probe = body.clone();
|
||||
probe.body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 });
|
||||
let opts = deckcraft_text::Opts { rect: tr, fields: &deckcraft_text::NoFields, prompt_color: None, no_shrink: false };
|
||||
let l = deckcraft_text::layout(&ctx, &sh, &probe, &opts);
|
||||
if l.font_scale >= 1.0 && l.line_reduction <= 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
// Change the body in place without an extra undo step (it is part of the edit).
|
||||
let st = s.doc_mut()?;
|
||||
let mut d = (*st.doc).clone();
|
||||
let stored = if let Some(list) = crate::shapes_mut(&mut d, &st.selection)
|
||||
&& let Some(target) = deckcraft_model::find_shape_mut(list, id)
|
||||
&& let Some(t) = target.text.as_mut()
|
||||
&& let Some(AutoFit::Shrink { font_scale, line_reduction }) = &mut t.body.autofit
|
||||
{
|
||||
(*font_scale, *line_reduction) = (Some(l.font_scale), l.line_reduction);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if stored {
|
||||
st.doc = Arc::new(d);
|
||||
st.revision += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option<Ctx<'a>> {
|
||||
match sel.target {
|
||||
Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)),
|
||||
|
||||
@@ -105,13 +105,45 @@ fn editing_text_refits_a_saved_shrink() {
|
||||
s.edit(|doc, sel| {
|
||||
let shapes = crate::shapes_mut(doc, sel).unwrap();
|
||||
let sh = deckcraft_model::find_shape_mut(shapes, title).unwrap();
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.5, line_reduction: 0.2 });
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.5), line_reduction: 0.2 });
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("text.insert", &json!({"text": "!"})).unwrap();
|
||||
let sh = s.doc().unwrap().shape(title).cloned().unwrap();
|
||||
assert_eq!(sh.text.unwrap().body.autofit, Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }));
|
||||
assert_eq!(sh.text.unwrap().body.autofit, Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }));
|
||||
}
|
||||
|
||||
/// After an edit, the shrink-on-overflow scale the layout computes is stored, so the saved file
|
||||
/// carries the scale DeckCraft draws instead of a bare normAutofit (#724).
|
||||
#[test]
|
||||
fn editing_shrink_text_stores_the_layout_scale() {
|
||||
use deckcraft_model::text::AutoFit;
|
||||
let mut s = session();
|
||||
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 200.0, 40.0]})).unwrap();
|
||||
let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
s.execute("format.autofit", &json!({"mode": "shrink"})).unwrap();
|
||||
let text = (0..12).map(|i| format!("Line {i}")).collect::<Vec<_>>().join("\n");
|
||||
s.execute("text.set", &json!({"id": id.0, "text": text})).unwrap();
|
||||
let sh = s.doc().unwrap().shape(id).cloned().unwrap();
|
||||
let Some(AutoFit::Shrink { font_scale, line_reduction }) = sh.text.unwrap().body.autofit else { panic!("shrink") };
|
||||
assert!(font_scale.is_some_and(|s| s < 1.0), "the computed fit is stored: {font_scale:?} / {line_reduction}");
|
||||
// The saved file must carry the scale DeckCraft draws, so PowerPoint opens it the same way.
|
||||
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("ppt/slides/slide1.xml").unwrap(), &mut xml).unwrap();
|
||||
let scales: Vec<i64> = xml.split("fontScale=\"").skip(1).filter_map(|s| s.split('"').next()?.parse().ok()).collect();
|
||||
assert!(scales.iter().any(|v| *v < 100_000), "the fitted scale is saved: {scales:?}");
|
||||
// Undo restores the state before the edit, and redo brings the stored fit back.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let sh = s.doc().unwrap().shape(id).cloned().unwrap();
|
||||
let Some(AutoFit::Shrink { font_scale, .. }) = sh.text.unwrap().body.autofit else { panic!("shrink") };
|
||||
assert_eq!(font_scale, Some(1.0), "undo restores the scale the edit started from");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
let sh = s.doc().unwrap().shape(id).cloned().unwrap();
|
||||
let Some(AutoFit::Shrink { font_scale, .. }) = sh.text.unwrap().body.autofit else { panic!("shrink") };
|
||||
assert!(font_scale.is_some_and(|v| v < 1.0), "redo brings the stored fit back: {font_scale:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -690,6 +690,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A body whose file had no `fontScale` (`<a:normAutofit/>`) keeps that state through the
|
||||
/// native format: it is drawn at 100 % and only fitted when its text changes (#724).
|
||||
#[test]
|
||||
fn a_bare_normautofit_body_round_trips() {
|
||||
use deckcraft_model::text::{AutoFit, TextBody};
|
||||
let mut p = Presentation::default();
|
||||
let mut body = TextBody::from_text("Overflowing");
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 });
|
||||
let sh = deckcraft_model::Shape { text: Some(body), ..Default::default() };
|
||||
Arc::make_mut(&mut p.slides[0]).shapes.push(sh);
|
||||
let back = load(&save(&p).unwrap()).unwrap_or_else(|e| panic!("{e}"));
|
||||
let body = back.slides[0].shapes.last().unwrap().text.clone().unwrap();
|
||||
assert_eq!(body.body.autofit, Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 }));
|
||||
// A scale the file saved round-trips as itself.
|
||||
let mut body = TextBody::from_text("Overflowing");
|
||||
body.body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.62), line_reduction: 0.2 });
|
||||
let sh = deckcraft_model::Shape { text: Some(body), ..Default::default() };
|
||||
Arc::make_mut(&mut p.slides[0]).shapes.push(sh);
|
||||
let back = load(&save(&p).unwrap()).unwrap_or_else(|e| panic!("{e}"));
|
||||
let body = back.slides[0].shapes.last().unwrap().text.clone().unwrap();
|
||||
assert_eq!(body.body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.62), line_reduction: 0.2 }));
|
||||
}
|
||||
|
||||
/// Only `null`s where numbers belong are patched: other damage still fails.
|
||||
#[test]
|
||||
fn other_damage_is_still_reported() {
|
||||
|
||||
@@ -203,13 +203,13 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
let title_box = |b: &mut B| {
|
||||
let (x, y, ww, hh) = r(60.0, 29.0, 840.0, 104.0);
|
||||
let mut s = b.shape("Title 1", PhType::Title, 0, x, y, ww, hh);
|
||||
s.text = Some(body_with(Some(Anchor::Middle), Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 })));
|
||||
s.text = Some(body_with(Some(Anchor::Middle), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })));
|
||||
s
|
||||
};
|
||||
let body_box = |b: &mut B, name: &str, idx: u32, x: f64, y: f64, ww: f64, hh: f64| {
|
||||
let (x, y, ww, hh) = r(x, y, ww, hh);
|
||||
let mut s = b.shape(name, PhType::Body, idx, x, y, ww, hh);
|
||||
s.text = Some(body_with(Some(Anchor::Top), Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 })));
|
||||
s.text = Some(body_with(Some(Anchor::Top), Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 })));
|
||||
s
|
||||
};
|
||||
|
||||
@@ -263,7 +263,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
t.text = Some(TextBody {
|
||||
body: BodyProps {
|
||||
anchor: Some(Anchor::Bottom),
|
||||
autofit: Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }),
|
||||
autofit: Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }),
|
||||
..Default::default()
|
||||
},
|
||||
list_style: ls,
|
||||
@@ -288,7 +288,7 @@ pub fn build_master(size: Size, theme: Theme, first_id: u32) -> (Master, u32) {
|
||||
st.text = Some(TextBody {
|
||||
body: BodyProps {
|
||||
anchor: Some(Anchor::Top),
|
||||
autofit: Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 }),
|
||||
autofit: Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }),
|
||||
..Default::default()
|
||||
},
|
||||
list_style: ls,
|
||||
|
||||
@@ -41,8 +41,14 @@ impl Anchor {
|
||||
pub enum AutoFit {
|
||||
/// Do not autofit.
|
||||
None,
|
||||
/// Shrink text on overflow: font scale and line-spacing reduction (0–1).
|
||||
Shrink { font_scale: f64, line_reduction: f64 },
|
||||
/// Shrink text on overflow: font scale and line-spacing reduction (0–1). `font_scale` is
|
||||
/// `None` for a file's `<a:normAutofit/>` with no `fontScale`: the schema default 100 %,
|
||||
/// which PowerPoint draws as it is and only fits again when the text changes (#724).
|
||||
Shrink {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
font_scale: Option<f64>,
|
||||
line_reduction: f64,
|
||||
},
|
||||
/// Resize shape to fit text.
|
||||
Shape,
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ pub fn body_pr(b: &El) -> BodyProps {
|
||||
match e.local() {
|
||||
"normAutofit" => {
|
||||
p.autofit = Some(AutoFit::Shrink {
|
||||
font_scale: pct(e, "fontScale").unwrap_or(1.0).clamp(0.01, 1.0),
|
||||
font_scale: pct(e, "fontScale").map(|v| v.clamp(0.01, 1.0)),
|
||||
line_reduction: pct(e, "lnSpcReduction").unwrap_or(0.0).clamp(0.0, 1.0),
|
||||
})
|
||||
}
|
||||
@@ -645,8 +645,14 @@ mod tests {
|
||||
assert_eq!(b.wrap, Some(false));
|
||||
assert_eq!(b.vert, Some(TextDir::Vertical270));
|
||||
assert_eq!(b.columns, Some(2));
|
||||
assert_eq!(b.autofit, Some(AutoFit::Shrink { font_scale: 0.625, line_reduction: 0.2 }));
|
||||
assert_eq!(b.autofit, Some(AutoFit::Shrink { font_scale: Some(0.625), line_reduction: 0.2 }));
|
||||
assert_eq!(b.space_first_last, None);
|
||||
// A bare `<a:normAutofit/>` keeps no scale: the schema default 100 % is drawn as it is,
|
||||
// not the same as a saved 100 % (`fontScale="100000"`) (#724).
|
||||
let d = parse(br#"<a:bodyPr><a:normAutofit/></a:bodyPr>"#).unwrap();
|
||||
assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 }));
|
||||
let d = parse(br#"<a:bodyPr><a:normAutofit fontScale="100000"/></a:bodyPr>"#).unwrap();
|
||||
assert_eq!(body_pr(&d.root).autofit, Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 }));
|
||||
let d = parse(br#"<a:bodyPr spcFirstLastPara="1"/>"#).unwrap();
|
||||
assert_eq!(body_pr(&d.root).space_first_last, Some(true));
|
||||
// mongolianVert is its own direction (PowerPoint runs its lines left to right) and is
|
||||
|
||||
@@ -489,9 +489,11 @@ pub fn body_pr(w: &mut W, b: &BodyProps) {
|
||||
Some(AutoFit::None) => w.empty0("a:noAutofit"),
|
||||
Some(AutoFit::Shape) => w.empty0("a:spAutoFit"),
|
||||
Some(AutoFit::Shrink { font_scale, line_reduction }) => {
|
||||
let fs = pct(font_scale.clamp(0.01, 1.0));
|
||||
// A scale the model holds is written, 100 % included: reopening then keeps the same
|
||||
// "draw the saved scale" state instead of becoming a bare `<a:normAutofit/>` (#724).
|
||||
let fs = font_scale.map(|s| pct(s.clamp(0.01, 1.0)));
|
||||
let lr = pct(line_reduction.clamp(0.0, 1.0));
|
||||
w.empty("a:normAutofit", A::new().o("fontScale", (fs != 100_000).then_some(fs)).o("lnSpcReduction", (lr != 0).then_some(lr)));
|
||||
w.empty("a:normAutofit", A::new().o("fontScale", fs).o("lnSpcReduction", (lr != 0).then_some(lr)));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fn corpus() -> Presentation {
|
||||
let mut columns = TextBody::from_text("Two columns\nof vertical text");
|
||||
columns.body.columns = Some(3);
|
||||
columns.body.col_spacing = Some(18.0);
|
||||
columns.body.autofit = Some(AutoFit::Shrink { font_scale: 0.85, line_reduction: 0.2 });
|
||||
columns.body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.85), line_reduction: 0.2 });
|
||||
columns.body.vert = Some(TextDir::Vertical270);
|
||||
let mut col = shape(&mut p, "Columns", 480.0, 40.0, 200.0, 160.0);
|
||||
col.text = Some(columns);
|
||||
@@ -176,7 +176,7 @@ fn chart_table_and_text_corpus_round_trips() {
|
||||
assert_eq!(plain(cols), "Two columns\nof vertical text");
|
||||
assert_eq!(cols.body.columns, Some(3));
|
||||
assert_eq!(cols.body.col_spacing, Some(18.0));
|
||||
assert_eq!(cols.body.autofit, Some(AutoFit::Shrink { font_scale: 0.85, line_reduction: 0.2 }));
|
||||
assert_eq!(cols.body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.85), line_reduction: 0.2 }));
|
||||
assert_eq!(cols.body.vert, Some(TextDir::Vertical270));
|
||||
|
||||
let warp = by_name(slide, "Warp").text.as_ref().expect("warp");
|
||||
|
||||
@@ -79,7 +79,7 @@ fn style_deck() -> Presentation {
|
||||
let mut word = shape(&mut p, "Styled text", 40.0, 40.0, 400.0, 180.0);
|
||||
word.text = Some(TextBody {
|
||||
body: BodyProps {
|
||||
autofit: Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 }),
|
||||
autofit: Some(AutoFit::Shrink { font_scale: Some(0.8), line_reduction: 0.2 }),
|
||||
columns: Some(2),
|
||||
col_spacing: Some(12.0),
|
||||
vert: Some(TextDir::Vertical270),
|
||||
@@ -167,7 +167,7 @@ fn style_kinds_round_trip() {
|
||||
let twice = round(&once);
|
||||
|
||||
let body = &by_name(&once, "Styled text").text.as_ref().expect("text").body;
|
||||
assert_eq!(body.autofit, Some(AutoFit::Shrink { font_scale: 0.8, line_reduction: 0.2 }));
|
||||
assert_eq!(body.autofit, Some(AutoFit::Shrink { font_scale: Some(0.8), line_reduction: 0.2 }));
|
||||
assert_eq!(body.columns, Some(2));
|
||||
assert_eq!(body.col_spacing, Some(12.0));
|
||||
assert_eq!(body.vert, Some(TextDir::Vertical270));
|
||||
|
||||
@@ -1549,18 +1549,21 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay
|
||||
let autofit = bp.autofit.unwrap_or(AutoFit::None);
|
||||
// Shrink on overflow starts from the scale the file was saved with (`fontScale`,
|
||||
// `lnSpcReduction`), which is what PowerPoint draws; editing the text resets it.
|
||||
let (scale0, reduce0) = match (autofit, opts.no_shrink) {
|
||||
let (scale0, reduce0, no_saved_scale) = match (autofit, opts.no_shrink) {
|
||||
(AutoFit::Shrink { font_scale, line_reduction }, false) => (
|
||||
if font_scale.is_finite() { font_scale.clamp(0.1, 1.0) } else { 1.0 },
|
||||
font_scale.filter(|s| s.is_finite()).map(|s| s.clamp(0.1, 1.0)).unwrap_or(1.0),
|
||||
if line_reduction.is_finite() { line_reduction.clamp(0.0, 0.2) } else { 0.0 },
|
||||
font_scale.is_none(),
|
||||
),
|
||||
_ => (1.0, 0.0),
|
||||
_ => (1.0, 0.0, false),
|
||||
};
|
||||
let mut l = layout_scaled(ctx, shape, body, &bp, opts, scale0, reduce0);
|
||||
// A scale PowerPoint saved is drawn as it is: PowerPoint fits the text again only when it
|
||||
// changes (and editing resets the saved scale), not when a file opens, even where our
|
||||
// metrics would call it overflowing (trailing empty paragraphs, substituted fonts).
|
||||
let saved = scale0 < 1.0 || reduce0 > 0.0;
|
||||
// A body whose file has no `fontScale` at all (`<a:normAutofit/>`) is the schema default
|
||||
// 100 % and is drawn as it is too, not refitted on open (#724).
|
||||
let saved = no_saved_scale || scale0 < 1.0 || reduce0 > 0.0;
|
||||
if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink)
|
||||
&& l.overflow
|
||||
&& !saved
|
||||
|
||||
@@ -186,26 +186,38 @@ fn saved_shrink_scale_is_drawn() {
|
||||
// A file saved with fontScale 62.5% and lnSpcReduction 20% draws at that scale even though
|
||||
// the text would fit at full size, as PowerPoint draws it.
|
||||
let (p, mut sh) = setup("Short", 600.0, 300.0);
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.625, line_reduction: 0.2 });
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.625), line_reduction: 0.2 });
|
||||
let l = lay(&p, &sh, 600.0, 300.0);
|
||||
assert_eq!((l.font_scale, l.line_reduction), (0.625, 0.2));
|
||||
// Text our metrics find overflowing at the saved scale is still drawn at it, as PowerPoint
|
||||
// draws it until the text changes; with no saved scale it shrinks to fit.
|
||||
let text = (0..30).map(|i| format!("Line number {i}")).collect::<Vec<_>>().join("\n");
|
||||
let (p, mut sh) = setup(&text, 600.0, 200.0);
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 0.9, line_reduction: 0.1 });
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(0.9), line_reduction: 0.1 });
|
||||
let l = lay(&p, &sh, 600.0, 200.0);
|
||||
assert_eq!((l.font_scale, l.line_reduction), (0.9, 0.1));
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(1.0), line_reduction: 0.0 });
|
||||
let l = lay(&p, &sh, 600.0, 200.0);
|
||||
assert!(l.font_scale < 0.9, "{}", l.font_scale);
|
||||
// Nonsense saved values are ignored.
|
||||
let (p, mut sh) = setup("Short", 600.0, 300.0);
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: f64::NAN, line_reduction: 5.0 });
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: Some(f64::NAN), line_reduction: 5.0 });
|
||||
let l = lay(&p, &sh, 600.0, 300.0);
|
||||
assert_eq!((l.font_scale, l.line_reduction), (1.0, 0.2));
|
||||
}
|
||||
|
||||
/// A body whose file has `<a:normAutofit/>` with no `fontScale` is the schema default 100 %:
|
||||
/// PowerPoint draws it as it is, even overflowing. DeckCraft draws it the same (#724).
|
||||
#[test]
|
||||
fn bare_normautofit_draws_at_full_size() {
|
||||
let text = (0..30).map(|i| format!("Line number {i}")).collect::<Vec<_>>().join("\n");
|
||||
let (p, mut sh) = setup(&text, 600.0, 200.0);
|
||||
sh.text.as_mut().unwrap().body.autofit = Some(AutoFit::Shrink { font_scale: None, line_reduction: 0.0 });
|
||||
let l = lay(&p, &sh, 600.0, 200.0);
|
||||
assert_eq!(l.font_scale, 1.0, "a body with no saved fontScale draws at 100%");
|
||||
assert!(l.overflow, "it overflows, as PowerPoint draws it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_line_spacing_ends_at_the_last_descent() {
|
||||
// 12 pt text at exactly 26.59 pt per line (a table cell PowerPoint sizes to 24.3 pt of text):
|
||||
|
||||
Reference in New Issue
Block a user