Automatic frame merging

This commit is contained in:
Laurenz 2022-04-19 13:54:04 +02:00
parent f27f7a05ab
commit 255d4c620f
8 changed files with 193 additions and 57 deletions

View File

@ -1,6 +1,6 @@
//! Finished layouts. //! Finished layouts.
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter, Write};
use std::sync::Arc; use std::sync::Arc;
use crate::font::FaceId; use crate::font::FaceId;
@ -8,6 +8,7 @@ use crate::geom::{
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform, Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
}; };
use crate::image::ImageId; use crate::image::ImageId;
use crate::util::{EcoString, MaybeShared};
/// A finished layout with elements at fixed positions. /// A finished layout with elements at fixed positions.
#[derive(Default, Clone, Eq, PartialEq)] #[derive(Default, Clone, Eq, PartialEq)]
@ -57,20 +58,15 @@ impl Frame {
self.elements.insert(layer, (pos, element)); self.elements.insert(layer, (pos, element));
} }
/// Add a group element. /// Add a frame.
pub fn push_frame(&mut self, pos: Point, frame: Arc<Self>) { ///
self.elements.push((pos, Element::Group(Group::new(frame)))); /// Automatically decides whether to inline the frame or to include it as a
} /// group based on the number of elements in the frame.
pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) {
/// Add all elements of another frame, placing them relative to the given if self.elements.is_empty() || frame.as_ref().elements.len() <= 5 {
/// position. frame.inline(self, pos);
pub fn merge_frame(&mut self, pos: Point, subframe: Self) {
if pos == Point::zero() && self.elements.is_empty() {
self.elements = subframe.elements;
} else { } else {
for (subpos, child) in subframe.elements { self.elements.push((pos, Element::Group(Group::new(frame.share()))));
self.elements.push((pos + subpos, child));
}
} }
} }
@ -122,30 +118,86 @@ impl Frame {
} }
/// Link the whole frame to a resource. /// Link the whole frame to a resource.
pub fn link(&mut self, url: impl Into<String>) { pub fn link(&mut self, url: EcoString) {
self.push(Point::zero(), Element::Link(url.into(), self.size)); self.push(Point::zero(), Element::Link(url, self.size));
} }
} }
impl Debug for Frame { impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Frame") f.debug_list()
.field("size", &self.size) .entries(self.elements.iter().map(|(_, element)| element))
.field("baseline", &self.baseline)
.field(
"children",
&crate::util::debug(|f| {
f.debug_map()
.entries(self.elements.iter().map(|(k, v)| (k, v)))
.finish()
}),
)
.finish() .finish()
} }
} }
impl AsRef<Frame> for Frame {
fn as_ref(&self) -> &Frame {
self
}
}
/// A representational form of a frame (owned, shared or maybe shared).
pub trait FrameRepr: AsRef<Frame> {
/// Transform into a shared representation.
fn share(self) -> Arc<Frame>;
/// Inline `self` into the sink frame.
fn inline(self, sink: &mut Frame, offset: Point);
}
impl FrameRepr for Frame {
fn share(self) -> Arc<Frame> {
Arc::new(self)
}
fn inline(self, sink: &mut Frame, offset: Point) {
if offset.is_zero() {
if sink.elements.is_empty() {
sink.elements = self.elements;
} else {
sink.elements.extend(self.elements);
}
} else {
sink.elements
.extend(self.elements.into_iter().map(|(p, e)| (p + offset, e)));
}
}
}
impl FrameRepr for Arc<Frame> {
fn share(self) -> Arc<Frame> {
self
}
fn inline(self, sink: &mut Frame, offset: Point) {
match Arc::try_unwrap(self) {
Ok(frame) => frame.inline(sink, offset),
Err(rc) => sink
.elements
.extend(rc.elements.iter().cloned().map(|(p, e)| (p + offset, e))),
}
}
}
impl FrameRepr for MaybeShared<Frame> {
fn share(self) -> Arc<Frame> {
match self {
Self::Owned(owned) => owned.share(),
Self::Shared(shared) => shared.share(),
}
}
fn inline(self, sink: &mut Frame, offset: Point) {
match self {
Self::Owned(owned) => owned.inline(sink, offset),
Self::Shared(shared) => shared.inline(sink, offset),
}
}
}
/// The building block frames are composed of. /// The building block frames are composed of.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub enum Element { pub enum Element {
/// A group of elements. /// A group of elements.
Group(Group), Group(Group),
@ -156,11 +208,23 @@ pub enum Element {
/// An image and its size. /// An image and its size.
Image(ImageId, Size), Image(ImageId, Size),
/// A link to an external resource and its trigger region. /// A link to an external resource and its trigger region.
Link(String, Size), Link(EcoString, Size),
}
impl Debug for Element {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Group(group) => group.fmt(f),
Self::Text(text) => write!(f, "{text:?}"),
Self::Shape(shape) => write!(f, "{shape:?}"),
Self::Image(image, _) => write!(f, "{image:?}"),
Self::Link(url, _) => write!(f, "Link({url:?})"),
}
}
} }
/// A group of elements with optional clipping. /// A group of elements with optional clipping.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct Group { pub struct Group {
/// The group's frame. /// The group's frame.
pub frame: Arc<Frame>, pub frame: Arc<Frame>,
@ -181,8 +245,15 @@ impl Group {
} }
} }
impl Debug for Group {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Group ")?;
self.frame.fmt(f)
}
}
/// A run of shaped text. /// A run of shaped text.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct Text { pub struct Text {
/// The font face the glyphs are contained in. /// The font face the glyphs are contained in.
pub face_id: FaceId, pub face_id: FaceId,
@ -201,6 +272,19 @@ impl Text {
} }
} }
impl Debug for Text {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
// This is only a rough approxmiation of the source text.
f.write_str("Text(\"")?;
for glyph in &self.glyphs {
for c in glyph.c.escape_debug() {
f.write_char(c)?;
}
}
f.write_str("\")")
}
}
/// A glyph in a run of shaped text. /// A glyph in a run of shaped text.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Glyph { pub struct Glyph {
@ -210,6 +294,8 @@ pub struct Glyph {
pub x_advance: Em, pub x_advance: Em,
/// The horizontal offset of the glyph. /// The horizontal offset of the glyph.
pub x_offset: Em, pub x_offset: Em,
/// The first character of the glyph's cluster.
pub c: char,
} }
/// A geometric shape with optional fill and stroke. /// A geometric shape with optional fill and stroke.

View File

@ -83,7 +83,7 @@ impl Layout for ImageNode {
// Apply link if it exists. // Apply link if it exists.
if let Some(url) = styles.get(TextNode::LINK) { if let Some(url) = styles.get(TextNode::LINK) {
frame.link(url); frame.link(url.clone());
} }
Ok(vec![Arc::new(frame)]) Ok(vec![Arc::new(frame)])

View File

@ -132,7 +132,7 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Apply link if it exists. // Apply link if it exists.
if let Some(url) = styles.get(TextNode::LINK) { if let Some(url) = styles.get(TextNode::LINK) {
frame.link(url); frame.link(url.clone());
} }
Ok(frames) Ok(frames)

View File

@ -551,7 +551,7 @@ impl<'a> GridLayouter<'a> {
}; };
let height = frame.size.y; let height = frame.size.y;
output.merge_frame(pos, frame); output.push_frame(pos, frame);
pos.y += height; pos.y += height;
} }

View File

@ -8,7 +8,7 @@ use super::{shape, Lang, Quoter, Quotes, RepeatNode, ShapedText, TextNode};
use crate::font::FontStore; use crate::font::FontStore;
use crate::library::layout::Spacing; use crate::library::layout::Spacing;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::util::{ArcExt, EcoString}; use crate::util::{EcoString, MaybeShared};
/// Arrange text, spacing and inline-level nodes into a paragraph. /// Arrange text, spacing and inline-level nodes into a paragraph.
#[derive(Hash)] #[derive(Hash)]
@ -283,7 +283,7 @@ enum Item<'a> {
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fraction), Fractional(Fraction),
/// A layouted child node. /// A layouted child node.
Frame(Frame), Frame(Arc<Frame>),
/// A repeating node. /// A repeating node.
Repeat(&'a RepeatNode, StyleChain<'a>), Repeat(&'a RepeatNode, StyleChain<'a>),
} }
@ -522,7 +522,7 @@ fn prepare<'a>(
let size = Size::new(regions.first.x, regions.base.y); let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false)); let pod = Regions::one(size, regions.base, Spec::splat(false));
let frame = node.layout(ctx, &pod, styles)?.remove(0); let frame = node.layout(ctx, &pod, styles)?.remove(0);
items.push(Item::Frame(Arc::take(frame))); items.push(Item::Frame(frame));
} }
} }
} }
@ -1041,7 +1041,7 @@ fn stack(
let pos = Point::with_y(output.size.y); let pos = Point::with_y(output.size.y);
output.size.y += height; output.size.y += height;
output.merge_frame(pos, frame); output.push_frame(pos, frame);
regions.first.y -= height + p.leading; regions.first.y -= height + p.leading;
first = false; first = false;
@ -1111,7 +1111,7 @@ fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for item in reordered { for item in reordered {
let mut push = |offset: &mut Length, frame: Frame| { let mut push = |offset: &mut Length, frame: MaybeShared<Frame>| {
let width = frame.size.x; let width = frame.size.x;
top.set_max(frame.baseline()); top.set_max(frame.baseline());
bottom.set_max(frame.size.y - frame.baseline()); bottom.set_max(frame.size.y - frame.baseline());
@ -1127,10 +1127,11 @@ fn commit(
offset += v.share(fr, remaining); offset += v.share(fr, remaining);
} }
Item::Text(shaped) => { Item::Text(shaped) => {
push(&mut offset, shaped.build(&mut ctx.fonts, justification)); let frame = shaped.build(&mut ctx.fonts, justification);
push(&mut offset, MaybeShared::Owned(frame));
} }
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone()); push(&mut offset, MaybeShared::Shared(frame.clone()));
} }
Item::Repeat(node, styles) => { Item::Repeat(node, styles) => {
let before = offset; let before = offset;
@ -1146,7 +1147,7 @@ fn commit(
} }
if frame.size.x > Length::zero() { if frame.size.x > Length::zero() {
for _ in 0 .. (count as usize).min(1000) { for _ in 0 .. (count as usize).min(1000) {
push(&mut offset, frame.as_ref().clone()); push(&mut offset, MaybeShared::Shared(frame.clone()));
offset += apart; offset += apart;
} }
} }
@ -1168,7 +1169,7 @@ fn commit(
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.merge_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
} }
Ok(output) Ok(output)

View File

@ -100,6 +100,7 @@ impl<'a> ShapedText<'a> {
Em::zero() Em::zero()
}, },
x_offset: glyph.x_offset, x_offset: glyph.x_offset,
c: glyph.c,
}) })
.collect(); .collect();
@ -118,7 +119,7 @@ impl<'a> ShapedText<'a> {
// Apply link if it exists. // Apply link if it exists.
if let Some(url) = self.styles.get(TextNode::LINK) { if let Some(url) = self.styles.get(TextNode::LINK) {
frame.link(url); frame.link(url.clone());
} }
frame frame

View File

@ -11,7 +11,7 @@ pub use prehashed::Prehashed;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::Range; use std::ops::{Deref, Range};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@ -101,6 +101,31 @@ where
} }
} }
/// Either owned or shared.
pub enum MaybeShared<T> {
/// Owned data.
Owned(T),
/// Shared data.
Shared(Arc<T>),
}
impl<T> AsRef<T> for MaybeShared<T> {
fn as_ref(&self) -> &T {
self
}
}
impl<T> Deref for MaybeShared<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(owned) => owned,
Self::Shared(shared) => shared,
}
}
}
/// Additional methods for slices. /// Additional methods for slices.
pub trait SliceExt<T> { pub trait SliceExt<T> {
/// Split a slice into consecutive runs with the same key and yield for /// Split a slice into consecutive runs with the same key and yield for

View File

@ -101,7 +101,7 @@ fn main() {
&png_path, &png_path,
&ref_path, &ref_path,
pdf_path.as_deref(), pdf_path.as_deref(),
args.syntax, &args.print,
) as usize; ) as usize;
} }
@ -114,19 +114,27 @@ fn main() {
} }
} }
/// Parsed command line arguments.
struct Args { struct Args {
filter: Vec<String>, filter: Vec<String>,
exact: bool, exact: bool,
syntax: bool,
pdf: bool, pdf: bool,
print: PrintConfig,
}
/// Which things to print out for debugging.
#[derive(Default, Copy, Clone, Eq, PartialEq)]
struct PrintConfig {
syntax: bool,
frames: bool,
} }
impl Args { impl Args {
fn new(args: impl Iterator<Item = String>) -> Self { fn new(args: impl Iterator<Item = String>) -> Self {
let mut filter = Vec::new(); let mut filter = Vec::new();
let mut exact = false; let mut exact = false;
let mut syntax = false;
let mut pdf = false; let mut pdf = false;
let mut print = PrintConfig::default();
for arg in args { for arg in args {
match arg.as_str() { match arg.as_str() {
@ -136,14 +144,16 @@ impl Args {
"--exact" => exact = true, "--exact" => exact = true,
// Generate PDFs. // Generate PDFs.
"--pdf" => pdf = true, "--pdf" => pdf = true,
// Debug print the layout trees. // Debug print the syntax trees.
"--syntax" => syntax = true, "--syntax" => print.syntax = true,
// Debug print the frames.
"--frames" => print.frames = true,
// Everything else is a file filter. // Everything else is a file filter.
_ => filter.push(arg), _ => filter.push(arg),
} }
} }
Self { filter, pdf, syntax, exact } Self { filter, exact, pdf, print }
} }
fn matches(&self, path: &Path) -> bool { fn matches(&self, path: &Path) -> bool {
@ -163,7 +173,7 @@ fn test(
png_path: &Path, png_path: &Path,
ref_path: &Path, ref_path: &Path,
pdf_path: Option<&Path>, pdf_path: Option<&Path>,
syntax: bool, print: &PrintConfig,
) -> bool { ) -> bool {
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path); let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display()); println!("Testing {}", name.display());
@ -199,7 +209,7 @@ fn test(
i, i,
compare_ref, compare_ref,
line, line,
syntax, print,
&mut rng, &mut rng,
); );
ok &= part_ok; ok &= part_ok;
@ -217,12 +227,25 @@ fn test(
fs::write(pdf_path, pdf_data).unwrap(); fs::write(pdf_path, pdf_data).unwrap();
} }
if print.frames {
for frame in &frames {
println!("Frame: {:#?}", frame);
}
}
let canvas = render(ctx, &frames); let canvas = render(ctx, &frames);
fs::create_dir_all(&png_path.parent().unwrap()).unwrap(); fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
canvas.save_png(png_path).unwrap(); canvas.save_png(png_path).unwrap();
if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) { if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
if canvas != ref_pixmap { if canvas.width() != ref_pixmap.width()
|| canvas.height() != ref_pixmap.height()
|| canvas
.data()
.iter()
.zip(ref_pixmap.data())
.any(|(&a, &b)| a.abs_diff(b) > 2)
{
println!(" Does not match reference image. ❌"); println!(" Does not match reference image. ❌");
ok = false; ok = false;
} }
@ -233,7 +256,7 @@ fn test(
} }
if ok { if ok {
if !syntax { if *print == PrintConfig::default() {
print!("\x1b[1A"); print!("\x1b[1A");
} }
println!("Testing {}", name.display()); println!("Testing {}", name.display());
@ -249,14 +272,14 @@ fn test_part(
i: usize, i: usize,
compare_ref: bool, compare_ref: bool,
line: usize, line: usize,
syntax: bool, print: &PrintConfig,
rng: &mut LinearShift, rng: &mut LinearShift,
) -> (bool, bool, Vec<Arc<Frame>>) { ) -> (bool, bool, Vec<Arc<Frame>>) {
let mut ok = true; let mut ok = true;
let id = ctx.sources.provide(src_path, src); let id = ctx.sources.provide(src_path, src);
let source = ctx.sources.get(id); let source = ctx.sources.get(id);
if syntax { if print.syntax {
println!("Syntax Tree: {:#?}", source.root()) println!("Syntax Tree: {:#?}", source.root())
} }