mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-08 03:32:19 +00:00
* Layout: a picture lowered past its height lowers its line as far, as in Word A picture lowered further than its own height had its lowering cut at that height. Word keeps its top on the baseline but lowers the line as far as the position says: a 6 pt picture lowered 30 pt makes a 30 pt line. Exact spacing keeps the line's baseline. Closes the gap noted with #3984: all 156 measured lines now match Word. * Lowered pictures: an exact-spacing test; gaps and progress log An exact-spacing line keeps its baseline when a picture is lowered past its height (the test fails without the cap). Gap 4 and the progress log note the change. --------- Co-authored-by: Daniele Mori <Dinasty@hotmail.it> Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
531 lines
23 KiB
Rust
531 lines
23 KiB
Rust
use super::options::DisplayOptions;
|
||
use super::paint::{Paint, paint_for};
|
||
use super::{BezPath, Document, Draw, FaceRef, Graphic, GraphicItem, GraphicKind, InlineObject, PathSeg, Rect, Rgb, ShapeKind, Spin, TextAlign};
|
||
use wordcraft_doc::connector::ArrowStyle;
|
||
|
||
/// The draws of a chart or diagram's items inside `rect` (page coordinates). The items were built
|
||
/// for the size `g.w` × `g.h` (0: `rect`'s own size), so they are scaled to `rect`; strokes and text
|
||
/// by the mean of the two scales.
|
||
pub(super) fn graphic_draws(doc: &Document, g: &Graphic, rect: Rect, alpha: f32) -> Vec<Draw> {
|
||
let (sx, sy) = (scale_of(rect.w, g.w), scale_of(rect.h, g.h));
|
||
let k = (sx + sy) / 2.0;
|
||
let inside = |r: &[f32; 4]| Rect::new(rect.x + r[0] * sx, rect.y + r[1] * sy, r[2] * sx, r[3] * sy);
|
||
let mut out = Vec::with_capacity(g.items.len());
|
||
for it in &g.items {
|
||
match it {
|
||
GraphicItem::Shape { rect: r, kind, fill, stroke, stroke_width } => out.push(Draw::Shape {
|
||
rect: inside(r),
|
||
kind: *kind,
|
||
fill: fill.map(Paint::solid),
|
||
stroke: *stroke,
|
||
stroke_width: stroke_width * k,
|
||
effects: Default::default(),
|
||
corner: None,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
}),
|
||
GraphicItem::Path { segs, fill, stroke, stroke_width } => {
|
||
let segs = page_segs(segs, |x, y| (rect.x + x * sx, rect.y + y * sy));
|
||
out.push(Draw::Path {
|
||
segs,
|
||
fill: fill.map(Paint::solid),
|
||
stroke: *stroke,
|
||
stroke_width: stroke_width * k,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
})
|
||
}
|
||
GraphicItem::Text { rect: r, .. } => out.extend(text_glyphs(doc, inside(r), it, alpha, k)),
|
||
GraphicItem::Image { rect: r, media } => out.push(Draw::Image {
|
||
rect: inside(r),
|
||
media: media.clone(),
|
||
crop: [0.0; 4],
|
||
alpha,
|
||
alt: String::new(),
|
||
fx: Default::default(),
|
||
decorative: false,
|
||
}),
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Scale from the size `built` points a graphic's items were made for to `len` points; 1 when
|
||
/// the built size is unknown (0).
|
||
fn scale_of(len: f32, built: f32) -> f32 {
|
||
if built.is_finite() && built > 0.0 && len.is_finite() { (len / built).max(0.0) } else { 1.0 }
|
||
}
|
||
|
||
/// Path segments mapped to page coordinates by `map`, cleaned for the rasterizer and PDF: a segment
|
||
/// with a non-finite number is dropped, and so is anything before a move (or after a broken one).
|
||
fn page_segs(segs: &[PathSeg], map: impl Fn(f32, f32) -> (f32, f32)) -> Vec<PathSeg> {
|
||
let fin = |p: (f32, f32)| p.0.is_finite() && p.1.is_finite();
|
||
let mut out = Vec::with_capacity(segs.len());
|
||
let mut open = false;
|
||
for s in segs {
|
||
match *s {
|
||
PathSeg::Move(x, y) => {
|
||
let p = map(x, y);
|
||
open = fin(p);
|
||
if open {
|
||
out.push(PathSeg::Move(p.0, p.1));
|
||
}
|
||
}
|
||
PathSeg::Line(x, y) => {
|
||
let p = map(x, y);
|
||
if open && fin(p) {
|
||
out.push(PathSeg::Line(p.0, p.1));
|
||
}
|
||
}
|
||
PathSeg::Cubic(a, b, c, d, e, f) => {
|
||
let (p1, p2, p3) = (map(a, b), map(c, d), map(e, f));
|
||
if open && fin(p1) && fin(p2) && fin(p3) {
|
||
out.push(PathSeg::Cubic(p1.0, p1.1, p2.0, p2.1, p3.0, p3.1));
|
||
}
|
||
}
|
||
PathSeg::Close => {
|
||
if open {
|
||
out.push(PathSeg::Close);
|
||
}
|
||
open = false;
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// A path of clean segments (see [`page_segs`]) as a kurbo path, for the rasteriser and PDF.
|
||
/// How a shape looks: its preset `kind`, fill, outline, effects, freeform paths and connector extras.
|
||
#[derive(Clone, Copy)]
|
||
pub(super) struct ShapeLook<'a> {
|
||
pub kind: ShapeKind,
|
||
pub fill: Option<&'a wordcraft_doc::fill::Fill>,
|
||
pub stroke: Option<Rgb>,
|
||
pub stroke_width: f32,
|
||
pub effects: wordcraft_doc::effects::ShapeEffects,
|
||
pub freeform: Option<&'a wordcraft_doc::freeform::Freeform>,
|
||
pub extra: &'a wordcraft_doc::connector::ShapeExtra,
|
||
}
|
||
|
||
/// The draws of a shape at `rect`: a preset outline, or a freeform's paths (an ink stroke left out
|
||
/// under Review › Hide Ink).
|
||
pub(super) fn shape_draws(rect: Rect, look: &ShapeLook, opts: &DisplayOptions, out: &mut Vec<Draw>) {
|
||
let ShapeLook { kind, fill, stroke, stroke_width, effects, freeform, extra } = *look;
|
||
// Freeforms are painted across the shape's frame, not each path's bounds.
|
||
let fill = fill.and_then(|f| paint_for(f, rect));
|
||
if kind.is_open() {
|
||
line_draws(rect, kind, stroke, stroke_width, effects, extra, out);
|
||
return;
|
||
}
|
||
let Some(f) = freeform.filter(|_| kind == ShapeKind::Freeform) else {
|
||
out.push(Draw::Shape {
|
||
rect,
|
||
kind,
|
||
fill,
|
||
stroke,
|
||
stroke_width,
|
||
effects,
|
||
corner: extra.corner,
|
||
dash: extra.dash.pattern(stroke_width),
|
||
cap: extra.cap,
|
||
});
|
||
return;
|
||
};
|
||
if f.is_ink() && opts.hide_ink {
|
||
return;
|
||
}
|
||
let width = if stroke_width.is_finite() { stroke_width.clamp(0.0, 200.0) } else { 0.75 };
|
||
for (pts, closed) in f.placed(rect.x, rect.y, rect.w, rect.h) {
|
||
if f.is_ink() || (!closed && fill.is_none()) {
|
||
if let Some(color) = stroke {
|
||
let mut pts: Vec<(f32, f32)> = pts.iter().map(|[x, y]| (*x, *y)).collect();
|
||
if closed && let Some(first) = pts.first().copied() {
|
||
pts.push(first);
|
||
}
|
||
if !closed
|
||
&& extra.arrow_end
|
||
&& let Some(arrow) = path_arrowhead(&pts, color, width, extra.end_style)
|
||
{
|
||
out.push(arrow);
|
||
}
|
||
out.push(Draw::Ink { pts, color, width: width.max(0.25), alpha: f.alpha });
|
||
}
|
||
continue;
|
||
}
|
||
let mut segs: Vec<PathSeg> = Vec::with_capacity(pts.len() + 1);
|
||
for (i, [x, y]) in pts.iter().enumerate() {
|
||
segs.push(if i == 0 { PathSeg::Move(*x, *y) } else { PathSeg::Line(*x, *y) });
|
||
}
|
||
if closed {
|
||
segs.push(PathSeg::Close);
|
||
}
|
||
let width = path_stroke_width(width);
|
||
out.push(Draw::Path {
|
||
segs,
|
||
fill: if closed { fill.clone() } else { None },
|
||
stroke,
|
||
stroke_width: width,
|
||
dash: extra.dash.pattern(width),
|
||
cap: extra.cap,
|
||
});
|
||
if !closed
|
||
&& extra.arrow_end
|
||
&& let Some(color) = stroke
|
||
{
|
||
let points: Vec<(f32, f32)> = pts.iter().map(|[x, y]| (*x, *y)).collect();
|
||
if let Some(arrow) = path_arrowhead(&points, color, width, extra.end_style) {
|
||
out.push(arrow);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dash lengths use the effective width used by both path painters.
|
||
fn path_stroke_width(width: f32) -> f32 {
|
||
if width.is_finite() && width > 0.0 { width.clamp(0.25, 200.0) } else { 0.75 }
|
||
}
|
||
|
||
/// Make a filled triangular arrowhead at the last point, in the points' coordinate space.
|
||
/// The last distinct segment supplies its direction; `style` and the stroke width set its size.
|
||
/// Returns `None` if there is no usable final segment. The triangle has no outline, so the path's
|
||
/// cap setting does not affect its appearance.
|
||
fn path_arrowhead(points: &[(f32, f32)], color: Rgb, stroke_width: f32, style: ArrowStyle) -> Option<Draw> {
|
||
let tip = *points.last()?;
|
||
let (prev_x, prev_y) = points.iter().rev().skip(1).copied().find(|p| *p != tip)?;
|
||
let dx = tip.0 - prev_x;
|
||
let dy = tip.1 - prev_y;
|
||
let length = dx.hypot(dy);
|
||
if !length.is_finite() || length <= f32::EPSILON {
|
||
return None;
|
||
}
|
||
Some(arrowhead(tip, (dx / length, dy / length), stroke_width, style, color))
|
||
}
|
||
|
||
/// A filled triangular arrowhead with its tip at `tip`, pointing along unit vector `dir`: as long
|
||
/// and as wide as `style` makes it for a `stroke_width` line ([`ArrowStyle::dims`]).
|
||
fn arrowhead(tip: (f32, f32), (dx, dy): (f32, f32), stroke_width: f32, style: ArrowStyle, color: Rgb) -> Draw {
|
||
let (len, wide) = style.dims(stroke_width);
|
||
let (bx, by) = (tip.0 - dx * len, tip.1 - dy * len);
|
||
let (nx, ny) = (-dy * wide / 2.0, dx * wide / 2.0);
|
||
let segs = vec![PathSeg::Move(tip.0, tip.1), PathSeg::Line(bx + nx, by + ny), PathSeg::Line(bx - nx, by - ny), PathSeg::Close];
|
||
Draw::Path { segs, fill: Some(Paint::solid(color)), stroke: None, stroke_width: 0.0, dash: Vec::new(), cap: Default::default() }
|
||
}
|
||
|
||
/// A line or connector at `rect` (its unflipped frame: flips are the frame's turn), with its
|
||
/// arrowheads.
|
||
fn line_draws(
|
||
rect: Rect,
|
||
kind: ShapeKind,
|
||
stroke: Option<Rgb>,
|
||
stroke_width: f32,
|
||
effects: wordcraft_doc::effects::ShapeEffects,
|
||
extra: &wordcraft_doc::connector::ShapeExtra,
|
||
out: &mut Vec<Draw>,
|
||
) {
|
||
use wordcraft_doc::connector::{connector_segs, end_direction};
|
||
match connector_segs(kind, rect.w, rect.h) {
|
||
Some(segs) => {
|
||
let segs = segs
|
||
.into_iter()
|
||
.map(|s| match s {
|
||
PathSeg::Move(x, y) => PathSeg::Move(rect.x + x, rect.y + y),
|
||
PathSeg::Line(x, y) => PathSeg::Line(rect.x + x, rect.y + y),
|
||
PathSeg::Cubic(a, b, c, d, e, f) => PathSeg::Cubic(rect.x + a, rect.y + b, rect.x + c, rect.y + d, rect.x + e, rect.y + f),
|
||
PathSeg::Close => PathSeg::Close,
|
||
})
|
||
.collect();
|
||
let width = path_stroke_width(stroke_width);
|
||
out.push(Draw::Path { segs, fill: None, stroke, stroke_width: width, dash: extra.dash.pattern(width), cap: extra.cap });
|
||
}
|
||
None => out.push(Draw::Shape {
|
||
rect,
|
||
kind,
|
||
fill: None,
|
||
stroke,
|
||
stroke_width,
|
||
effects,
|
||
corner: None,
|
||
dash: extra.dash.pattern(stroke_width),
|
||
cap: extra.cap,
|
||
}),
|
||
}
|
||
let Some(color) = stroke else { return };
|
||
// A line runs from the top-left corner to the bottom-right, as it is drawn (DrawingML's
|
||
// `line`), and so do connectors; flips are the frame's turn.
|
||
let (start, end) = ((rect.x, rect.y), (rect.right(), rect.bottom()));
|
||
for (on, tip, at_end, style) in [(extra.arrow_start, start, false, extra.start_style), (extra.arrow_end, end, true, extra.end_style)] {
|
||
if on {
|
||
out.push(arrowhead(tip, end_direction(kind, rect.w, rect.h, at_end), stroke_width, style, color));
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn seg_path(segs: &[PathSeg]) -> BezPath {
|
||
let mut p = BezPath::new();
|
||
for s in segs {
|
||
match *s {
|
||
PathSeg::Move(x, y) => p.move_to((x as f64, y as f64)),
|
||
PathSeg::Line(x, y) => p.line_to((x as f64, y as f64)),
|
||
PathSeg::Cubic(a, b, c, d, e, f) => p.curve_to((a as f64, b as f64), (c as f64, d as f64), (e as f64, f as f64)),
|
||
PathSeg::Close => p.close_path(),
|
||
}
|
||
}
|
||
p
|
||
}
|
||
|
||
/// One line of a `GraphicItem::Text` in `r`, shaped in its font (the document's body font when
|
||
/// unset) and aligned horizontally, centred vertically, at its size times `k`. Characters the font
|
||
/// lacks (Chinese in a chart drawn in Calibri's substitute, #4720) are drawn in a fallback face, as
|
||
/// in body text: one draw per run of a face, in reading order. Empty when there is nothing to draw,
|
||
/// or the item's rectangle or size isn't a finite number (a degenerate scale: `r` itself is always
|
||
/// finite, as [`Rect::new`] makes it).
|
||
fn text_glyphs(doc: &Document, r: Rect, it: &GraphicItem, alpha: f32, k: f32) -> Vec<Draw> {
|
||
let GraphicItem::Text { rect, text, size, color, bold, align, font } = it else { return Vec::new() };
|
||
let size = *size * k;
|
||
if !rect.iter().chain([&size]).all(|v| v.is_finite()) {
|
||
return Vec::new();
|
||
}
|
||
let family = font.as_deref().filter(|f| !f.is_empty()).unwrap_or(&doc.settings.minor_font);
|
||
let size = size.clamp(1.0, 400.0);
|
||
let resolved = wordcraft_fonts::word::resolve(family, *bold, false);
|
||
let primary = resolved.face;
|
||
let scale = |f: FaceRef| size / f.upem.max(1.0) as f32;
|
||
let runs: Vec<_> = face_runs(text, primary, *bold)
|
||
.into_iter()
|
||
.map(|(face, s)| (face, s, wordcraft_fonts::shape(face.get(), s, &[], |c| c)))
|
||
.filter(|(_, _, shaped)| !shaped.is_empty())
|
||
.collect();
|
||
let width: f32 = runs.iter().map(|(f, _, shaped)| shaped.iter().map(|g| g.x_advance as f32).sum::<f32>() * scale(*f)).sum();
|
||
// The font's own line metrics centre the line, whichever faces draw it.
|
||
let (ascent, descent) = wordcraft_fonts::word::line_metrics(primary.get());
|
||
let baseline = r.y + r.h / 2.0 + (ascent - descent) as f32 * scale(primary) / 2.0;
|
||
let mut pen = match align {
|
||
TextAlign::Left => r.x,
|
||
TextAlign::Center => r.x + (r.w - width) / 2.0,
|
||
TextAlign::Right => r.x + r.w - width,
|
||
};
|
||
let mut out = Vec::with_capacity(runs.len());
|
||
for (face, s, shaped) in runs {
|
||
let k = scale(face);
|
||
let mut glyphs = Vec::with_capacity(shaped.len());
|
||
for g in &shaped {
|
||
glyphs.push((g.gid, pen + g.x_offset as f32 * k, baseline - g.y_offset as f32 * k));
|
||
pen += g.x_advance as f32 * k;
|
||
}
|
||
// A fallback face is emboldened when it has no bold of its own, as in body text.
|
||
let synth_bold = if face.id() == primary.id() { resolved.synth_bold } else { *bold && face.weight < 600.0 };
|
||
out.push(Draw::Glyphs {
|
||
face,
|
||
size,
|
||
glyphs,
|
||
color: *color,
|
||
alpha,
|
||
synth_bold,
|
||
synth_italic: false,
|
||
text: s.to_string(),
|
||
link: None,
|
||
ranges: Vec::new(),
|
||
text_bg: None,
|
||
x_scale: 1.0,
|
||
});
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `text` split into runs of the face that draws them: `primary` where it has the character, else a
|
||
/// fallback face. A run the primary lacks takes one face covering all of it when one does (so a
|
||
/// Chinese label doesn't mix serif and sans glyphs), else each character its own fallback. Spaces
|
||
/// and combining marks stay in the run before them; a character no face has stays in `primary`.
|
||
fn face_runs(text: &str, primary: FaceRef, bold: bool) -> Vec<(FaceRef, &str)> {
|
||
let joins = |c: char| c.is_whitespace() || wordcraft_doc::math_linear::is_combining(c);
|
||
// (start, end, drawn by `primary`)
|
||
let mut spans: Vec<(usize, usize, bool)> = Vec::new();
|
||
for (i, c) in text.char_indices() {
|
||
let end = i + c.len_utf8();
|
||
let own = primary.covers(c);
|
||
match spans.last_mut() {
|
||
Some(last) if last.2 == own || joins(c) => last.1 = end,
|
||
_ => spans.push((i, end, own)),
|
||
}
|
||
}
|
||
let db = wordcraft_fonts::FontDb::global();
|
||
let mut out: Vec<(FaceRef, &str)> = Vec::with_capacity(spans.len());
|
||
for (start, end, own) in spans {
|
||
let Some(s) = text.get(start..end) else { continue };
|
||
if own {
|
||
out.push((primary, s));
|
||
continue;
|
||
}
|
||
let chars: Vec<char> = s.chars().filter(|c| !joins(*c)).collect();
|
||
if let Some(f) = db.fallback_for_run(&chars, primary.id(), bold, false) {
|
||
out.push((FaceRef::of(&f), s));
|
||
continue;
|
||
}
|
||
// No one face has them all: each character in its own fallback, neighbours sharing a face
|
||
// in one run.
|
||
let mut run: Option<(FaceRef, usize)> = None;
|
||
for (i, c) in s.char_indices() {
|
||
let face =
|
||
if joins(c) { None } else { Some(db.fallback_styled(c, primary.id(), bold, false).map(|f| FaceRef::of(&f)).unwrap_or(primary)) };
|
||
match (run, face) {
|
||
(Some((cur, from)), Some(f)) if f.id() != cur.id() => {
|
||
out.extend(s.get(from..i).map(|t| (cur, t)));
|
||
run = Some((f, i));
|
||
}
|
||
(None, f) => run = Some((f.unwrap_or(primary), i)),
|
||
_ => {}
|
||
}
|
||
}
|
||
if let Some((cur, from)) = run {
|
||
out.extend(s.get(from..).map(|t| (cur, t)));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// `items` turned by `spin` about the centre of `rect` (as they are, unturned).
|
||
pub(super) fn spun(spin: Spin, rect: Rect, items: Vec<Draw>, out: &mut Vec<Draw>) {
|
||
if spin.is_identity() {
|
||
out.extend(items);
|
||
} else {
|
||
out.push(Draw::Rotated { cx: rect.x + rect.w / 2.0, cy: rect.y + rect.h / 2.0, spin, items });
|
||
}
|
||
}
|
||
|
||
/// A picture, shape or group drawn in `rect` (its unturned frame), turned by its own spin about
|
||
/// the rect's centre; a group's members are turned inside it. `outer` is the spin it is already
|
||
/// drawn inside (its group's). Nothing for anything else.
|
||
pub(super) fn object_draws(o: &InlineObject, rect: Rect, outer: Spin, alpha: f32, opts: &DisplayOptions, out: &mut Vec<Draw>) {
|
||
let own = o.frame().map(|(_, _, f)| f.spin()).unwrap_or_default();
|
||
match o {
|
||
InlineObject::Image { media, crop, alt, fx, .. } => {
|
||
let alpha = alpha * fx.opacity();
|
||
let decorative = o.is_decorative();
|
||
spun(own, rect, vec![Draw::Image { rect, media: media.clone(), crop: *crop, alpha, alt: alt.clone(), fx: *fx, decorative }], out)
|
||
}
|
||
InlineObject::Shape { kind, fill, stroke, stroke_width, effects, freeform, extra, .. } => {
|
||
let effects = effects_in(*effects, own.within(outer));
|
||
let mut items = Vec::new();
|
||
shape_draws(
|
||
rect,
|
||
&ShapeLook {
|
||
kind: *kind,
|
||
fill: fill.as_ref(),
|
||
stroke: *stroke,
|
||
stroke_width: *stroke_width,
|
||
effects,
|
||
freeform: freeform.as_deref(),
|
||
extra,
|
||
},
|
||
opts,
|
||
&mut items,
|
||
);
|
||
match crate::shape_figure_alt(o) {
|
||
// A described shape is one figure with its description (tagged PDF).
|
||
Some(alt) => {
|
||
let mut draws = Vec::new();
|
||
spun(own, rect, items, &mut draws);
|
||
out.push(Draw::Figure { alt: alt.to_string(), kind: None, draws })
|
||
}
|
||
None => spun(own, rect, items, out),
|
||
}
|
||
}
|
||
InlineObject::Group { .. } => {
|
||
let mut members = Vec::new();
|
||
for ([x, y, w, h], c) in o.group_rects(rect.x, rect.y, rect.w, rect.h) {
|
||
if c.frame().is_some_and(|(_, _, f)| f.hidden) {
|
||
continue;
|
||
}
|
||
object_draws(c, Rect::new(x, y, w, h), own.within(outer), alpha, opts, &mut members);
|
||
}
|
||
match o.canvas_style() {
|
||
// A Drawing Canvas: its background and outline, then its members clipped to its
|
||
// frame, all inside its turn.
|
||
Some(c) => spun(own, rect, vec![canvas_background(rect, c), Draw::Clip { rect, items: members }], out),
|
||
None => spun(own, rect, members, out),
|
||
}
|
||
}
|
||
// Native ink in a group (kept opaque): drawn as drawing-level ink is, from its pictures.
|
||
InlineObject::Graphic { graphic, .. } if graphic.kind == GraphicKind::Ink => {
|
||
let (sx, sy) = (scale_of(rect.w, graphic.w), scale_of(rect.h, graphic.h));
|
||
let items = graphic
|
||
.items
|
||
.iter()
|
||
.filter_map(|it| match it {
|
||
GraphicItem::Image { rect: r, media } => Some(Draw::Image {
|
||
rect: Rect::new(rect.x + r[0] * sx, rect.y + r[1] * sy, r[2] * sx, r[3] * sy),
|
||
media: media.clone(),
|
||
crop: [0.0; 4],
|
||
alpha,
|
||
alt: String::new(),
|
||
fx: Default::default(),
|
||
decorative: false,
|
||
}),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
spun(own, rect, items, out)
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// A shape's `effects` as drawn turned by `spin` (all the turns it is drawn inside): glows and
|
||
/// soft edges turn with the shape, and so does a shadow's offset when it rotates with the shape
|
||
/// (`rotWithShape`); one that doesn't keeps its direction on the page, so its angle is turned back.
|
||
pub(crate) fn effects_in(effects: wordcraft_doc::effects::ShapeEffects, spin: Spin) -> wordcraft_doc::effects::ShapeEffects {
|
||
let mut e = effects;
|
||
if let Some(s) = e.shadow.as_mut().filter(|s| !s.rot_with_shape && !spin.is_identity()) {
|
||
let a = s.angle.to_radians();
|
||
let (dx, dy) = spin.unapply(0.0, 0.0, a.cos(), a.sin());
|
||
s.angle = wordcraft_geom::normalize_degrees(dy.atan2(dx).to_degrees());
|
||
}
|
||
e
|
||
}
|
||
|
||
/// Where inline object `obj` is drawn, given its cluster's box (`adv` wide at `cx`, reaching
|
||
/// `obj_h` above the baseline and `obj_d` below it): inside the room kept for its effects.
|
||
/// A rotated one's unrotated frame, centred in that room.
|
||
///
|
||
/// `line` is the room a horizontal rule is placed in, from `cx`.
|
||
pub(crate) fn inline_rect(obj: Option<&InlineObject>, cx: f32, base: f32, adv: f32, (obj_h, obj_d): (f32, f32), line: f32) -> Rect {
|
||
let frame = obj.and_then(InlineObject::frame);
|
||
let [l, t, r, b] = frame.map_or([0.0; 4], |(_, _, float)| float.effect_extent());
|
||
// Lowered further than its height, it is drawn with its top on the baseline (Word).
|
||
let room = Rect::new(cx + l, base - obj_h.max(0.0) + t, (adv - l - r).max(0.0), (obj_h + obj_d - t - b).max(0.0));
|
||
// A horizontal rule is as wide as its share of the paragraph's width, whatever narrows its
|
||
// line, and is placed by its alignment in what the line has left: it runs past the margin
|
||
// beside a float or a first-line indent, as in Word (see `ParaLayout::rule_room`).
|
||
if let Some(InlineObject::Shape { w, extra, .. }) = obj
|
||
&& let Some(hr) = extra.hr
|
||
{
|
||
let rw = hr.width(room.w, *w);
|
||
return Rect::new(hr.left(room.x, line, rw), room.y, rw, room.h);
|
||
}
|
||
let Some((w, h, float)) = frame.filter(|(_, _, f)| f.rot != 0.0) else { return room };
|
||
// The room is the rotated bounds of the frame: undo the rotation.
|
||
let (w, h) = (wordcraft_geom::finite(w).clamp(1.0, 4000.0), wordcraft_geom::finite(h).clamp(1.0, 4000.0));
|
||
let (bw, _) = float.spin().extent(w, h);
|
||
let k = if bw > 0.0 { room.w / bw } else { 1.0 };
|
||
let (fw, fh) = (w * k, h * k);
|
||
Rect::new(room.x + (room.w - fw) / 2.0, room.y + (room.h - fh) / 2.0, fw, fh)
|
||
}
|
||
|
||
/// A Drawing Canvas's background and outline over its frame `rect`.
|
||
fn canvas_background(rect: Rect, c: &wordcraft_doc::para::CanvasStyle) -> Draw {
|
||
Draw::Shape {
|
||
rect,
|
||
kind: ShapeKind::Rectangle,
|
||
fill: c.fill.as_ref().and_then(|f| paint_for(f, rect)),
|
||
stroke: c.stroke,
|
||
stroke_width: c.stroke_width,
|
||
effects: Default::default(),
|
||
corner: None,
|
||
dash: Vec::new(),
|
||
cap: Default::default(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod fill_alpha_tests;
|