mirror of
https://github.com/typst/typst
synced 2025-06-28 00:03:17 +08:00
Set Rules Episode IV: A New Fold
This commit is contained in:
parent
fe21c4d399
commit
ae38be9097
@ -181,11 +181,11 @@ impl Eval for MarkupNode {
|
|||||||
Self::Linebreak => Node::Linebreak,
|
Self::Linebreak => Node::Linebreak,
|
||||||
Self::Parbreak => Node::Parbreak,
|
Self::Parbreak => Node::Parbreak,
|
||||||
Self::Strong => {
|
Self::Strong => {
|
||||||
ctx.styles.set(TextNode::STRONG, !ctx.styles.get(TextNode::STRONG));
|
ctx.styles.toggle(TextNode::STRONG);
|
||||||
Node::new()
|
Node::new()
|
||||||
}
|
}
|
||||||
Self::Emph => {
|
Self::Emph => {
|
||||||
ctx.styles.set(TextNode::EMPH, !ctx.styles.get(TextNode::EMPH));
|
ctx.styles.toggle(TextNode::EMPH);
|
||||||
Node::new()
|
Node::new()
|
||||||
}
|
}
|
||||||
Self::Text(text) => Node::Text(text.clone()),
|
Self::Text(text) => Node::Text(text.clone()),
|
||||||
@ -216,7 +216,7 @@ impl Eval for MathNode {
|
|||||||
type Output = Node;
|
type Output = Node;
|
||||||
|
|
||||||
fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> {
|
fn eval(&self, _: &mut EvalContext) -> TypResult<Self::Output> {
|
||||||
let text = Node::Text(self.formula.clone()).monospaced();
|
let text = Node::Text(self.formula.trim().into()).monospaced();
|
||||||
Ok(if self.display {
|
Ok(if self.display {
|
||||||
Node::Block(text.into_block())
|
Node::Block(text.into_block())
|
||||||
} else {
|
} else {
|
||||||
@ -229,11 +229,10 @@ impl Eval for HeadingNode {
|
|||||||
type Output = Node;
|
type Output = Node;
|
||||||
|
|
||||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||||
// TODO(set): Relative font size.
|
|
||||||
let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75);
|
let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75);
|
||||||
let mut styles = Styles::new();
|
let mut styles = Styles::new();
|
||||||
styles.set(TextNode::STRONG, true);
|
styles.set(TextNode::STRONG, true);
|
||||||
styles.set(TextNode::SIZE, upscale * Length::pt(11.0));
|
styles.set(TextNode::SIZE, Relative::new(upscale).into());
|
||||||
Ok(Node::Block(
|
Ok(Node::Block(
|
||||||
self.body().eval(ctx)?.into_block().styled(styles),
|
self.body().eval(ctx)?.into_block().styled(styles),
|
||||||
))
|
))
|
||||||
@ -266,7 +265,7 @@ fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult<Node
|
|||||||
// TODO: Switch to em units for gutter once available.
|
// TODO: Switch to em units for gutter once available.
|
||||||
Ok(Node::block(GridNode {
|
Ok(Node::block(GridNode {
|
||||||
tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]),
|
tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]),
|
||||||
gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(6.0).into())], vec![]),
|
gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(5.0).into())], vec![]),
|
||||||
children: vec![Node::Text(label).into_block(), body.into_block()],
|
children: vec![Node::Text(label).into_block(), body.into_block()],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
295
src/eval/node.rs
295
src/eval/node.rs
@ -9,8 +9,8 @@ use crate::diag::StrResult;
|
|||||||
use crate::geom::SpecAxis;
|
use crate::geom::SpecAxis;
|
||||||
use crate::layout::{Layout, PackedNode};
|
use crate::layout::{Layout, PackedNode};
|
||||||
use crate::library::{
|
use crate::library::{
|
||||||
Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, Spacing,
|
DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode,
|
||||||
TextNode,
|
SpacingKind, SpacingNode, TextNode,
|
||||||
};
|
};
|
||||||
use crate::util::EcoString;
|
use crate::util::EcoString;
|
||||||
|
|
||||||
@ -18,8 +18,7 @@ use crate::util::EcoString;
|
|||||||
///
|
///
|
||||||
/// A node is a composable intermediate representation that can be converted
|
/// A node is a composable intermediate representation that can be converted
|
||||||
/// into a proper layout node by lifting it to a block-level or document node.
|
/// into a proper layout node by lifting it to a block-level or document node.
|
||||||
// TODO(set): Fix Debug impl leaking into user-facing repr.
|
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Node {
|
pub enum Node {
|
||||||
/// A word space.
|
/// A word space.
|
||||||
Space,
|
Space,
|
||||||
@ -32,7 +31,7 @@ pub enum Node {
|
|||||||
/// Plain text.
|
/// Plain text.
|
||||||
Text(EcoString),
|
Text(EcoString),
|
||||||
/// Spacing.
|
/// Spacing.
|
||||||
Spacing(SpecAxis, Spacing),
|
Spacing(SpecAxis, SpacingKind),
|
||||||
/// An inline node.
|
/// An inline node.
|
||||||
Inline(PackedNode),
|
Inline(PackedNode),
|
||||||
/// A block node.
|
/// A block node.
|
||||||
@ -77,18 +76,12 @@ impl Node {
|
|||||||
self.styled(Styles::one(TextNode::MONOSPACE, true))
|
self.styled(Styles::one(TextNode::MONOSPACE, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decorate this node.
|
|
||||||
pub fn decorated(self, _: Decoration) -> Self {
|
|
||||||
// TODO(set): Actually decorate.
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lift to a type-erased block-level node.
|
/// Lift to a type-erased block-level node.
|
||||||
pub fn into_block(self) -> PackedNode {
|
pub fn into_block(self) -> PackedNode {
|
||||||
if let Node::Block(packed) = self {
|
if let Node::Block(packed) = self {
|
||||||
packed
|
packed
|
||||||
} else {
|
} else {
|
||||||
let mut packer = NodePacker::new();
|
let mut packer = NodePacker::new(true);
|
||||||
packer.walk(self, Styles::new());
|
packer.walk(self, Styles::new());
|
||||||
packer.into_block()
|
packer.into_block()
|
||||||
}
|
}
|
||||||
@ -96,7 +89,7 @@ impl Node {
|
|||||||
|
|
||||||
/// Lift to a document node, the root of the layout tree.
|
/// Lift to a document node, the root of the layout tree.
|
||||||
pub fn into_document(self) -> DocumentNode {
|
pub fn into_document(self) -> DocumentNode {
|
||||||
let mut packer = NodePacker::new();
|
let mut packer = NodePacker::new(false);
|
||||||
packer.walk(self, Styles::new());
|
packer.walk(self, Styles::new());
|
||||||
packer.into_document()
|
packer.into_document()
|
||||||
}
|
}
|
||||||
@ -117,13 +110,6 @@ impl Default for Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Node {
|
|
||||||
fn eq(&self, _: &Self) -> bool {
|
|
||||||
// TODO(set): Figure out what to do here.
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Add for Node {
|
impl Add for Node {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@ -141,92 +127,89 @@ impl AddAssign for Node {
|
|||||||
|
|
||||||
/// Packs a [`Node`] into a flow or whole document.
|
/// Packs a [`Node`] into a flow or whole document.
|
||||||
struct NodePacker {
|
struct NodePacker {
|
||||||
|
/// Whether packing should produce a block-level node.
|
||||||
|
block: bool,
|
||||||
/// The accumulated page nodes.
|
/// The accumulated page nodes.
|
||||||
document: Vec<PageNode>,
|
pages: Vec<PageNode>,
|
||||||
/// The common style properties of all items on the current page.
|
|
||||||
page_styles: Styles,
|
|
||||||
/// The accumulated flow children.
|
/// The accumulated flow children.
|
||||||
flow: Vec<FlowChild>,
|
flow: Vec<FlowChild>,
|
||||||
|
/// The common style properties of all items on the current flow.
|
||||||
|
flow_styles: Styles,
|
||||||
|
/// The kind of thing that was last added to the current flow.
|
||||||
|
flow_last: Last<FlowChild>,
|
||||||
/// The accumulated paragraph children.
|
/// The accumulated paragraph children.
|
||||||
par: Vec<ParChild>,
|
par: Vec<ParChild>,
|
||||||
/// The common style properties of all items in the current paragraph.
|
/// The common style properties of all items in the current paragraph.
|
||||||
par_styles: Styles,
|
par_styles: Styles,
|
||||||
/// The kind of thing that was last added to the current paragraph.
|
/// The kind of thing that was last added to the current paragraph.
|
||||||
last: Last,
|
par_last: Last<ParChild>,
|
||||||
}
|
|
||||||
|
|
||||||
/// The type of the last thing that was pushed into the paragraph.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
||||||
enum Last {
|
|
||||||
None,
|
|
||||||
Spacing,
|
|
||||||
Newline,
|
|
||||||
Space,
|
|
||||||
Other,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodePacker {
|
impl NodePacker {
|
||||||
/// Start a new node-packing session.
|
/// Start a new node-packing session.
|
||||||
fn new() -> Self {
|
fn new(block: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
document: vec![],
|
block,
|
||||||
page_styles: Styles::new(),
|
pages: vec![],
|
||||||
flow: vec![],
|
flow: vec![],
|
||||||
|
flow_styles: Styles::new(),
|
||||||
|
flow_last: Last::None,
|
||||||
par: vec![],
|
par: vec![],
|
||||||
par_styles: Styles::new(),
|
par_styles: Styles::new(),
|
||||||
last: Last::None,
|
par_last: Last::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish up and return the resulting flow.
|
/// Finish up and return the resulting flow.
|
||||||
fn into_block(mut self) -> PackedNode {
|
fn into_block(mut self) -> PackedNode {
|
||||||
self.parbreak();
|
self.finish_par();
|
||||||
FlowNode(self.flow).pack()
|
FlowNode(self.flow).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish up and return the resulting document.
|
/// Finish up and return the resulting document.
|
||||||
fn into_document(mut self) -> DocumentNode {
|
fn into_document(mut self) -> DocumentNode {
|
||||||
self.pagebreak(true);
|
self.pagebreak(true);
|
||||||
DocumentNode(self.document)
|
DocumentNode(self.pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consider a node with the given styles.
|
/// Consider a node with the given styles.
|
||||||
fn walk(&mut self, node: Node, styles: Styles) {
|
fn walk(&mut self, node: Node, styles: Styles) {
|
||||||
match node {
|
match node {
|
||||||
Node::Space => {
|
Node::Space => {
|
||||||
// Only insert a space if the previous thing was actual content.
|
if self.is_flow_compatible(&styles) && self.is_par_compatible(&styles) {
|
||||||
if self.last == Last::Other {
|
self.par_last.soft(ParChild::text(' ', styles));
|
||||||
self.push_text(' '.into(), styles);
|
|
||||||
self.last = Last::Space;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Node::Linebreak => {
|
Node::Linebreak => {
|
||||||
self.trim();
|
self.par_last.hard();
|
||||||
self.push_text('\n'.into(), styles);
|
self.push_inline(ParChild::text('\n', styles));
|
||||||
self.last = Last::Newline;
|
self.par_last.hard();
|
||||||
}
|
}
|
||||||
Node::Parbreak => {
|
Node::Parbreak => {
|
||||||
self.parbreak();
|
self.parbreak(Some(styles));
|
||||||
}
|
}
|
||||||
Node::Pagebreak => {
|
Node::Pagebreak => {
|
||||||
self.pagebreak(true);
|
self.pagebreak(true);
|
||||||
self.page_styles = styles;
|
self.flow_styles = styles;
|
||||||
}
|
}
|
||||||
Node::Text(text) => {
|
Node::Text(text) => {
|
||||||
self.push_text(text, styles);
|
self.push_inline(ParChild::text(text, styles));
|
||||||
}
|
}
|
||||||
Node::Spacing(SpecAxis::Horizontal, amount) => {
|
Node::Spacing(SpecAxis::Horizontal, kind) => {
|
||||||
self.push_inline(ParChild::Spacing(amount), styles);
|
self.par_last.hard();
|
||||||
self.last = Last::Spacing;
|
self.push_inline(ParChild::Spacing(SpacingNode { kind, styles }));
|
||||||
|
self.par_last.hard();
|
||||||
}
|
}
|
||||||
Node::Spacing(SpecAxis::Vertical, amount) => {
|
Node::Spacing(SpecAxis::Vertical, kind) => {
|
||||||
self.push_block(FlowChild::Spacing(amount), styles);
|
self.finish_par();
|
||||||
|
self.flow.push(FlowChild::Spacing(SpacingNode { kind, styles }));
|
||||||
|
self.flow_last.hard();
|
||||||
}
|
}
|
||||||
Node::Inline(inline) => {
|
Node::Inline(inline) => {
|
||||||
self.push_inline(ParChild::Node(inline), styles);
|
self.push_inline(ParChild::Node(inline.styled(styles)));
|
||||||
}
|
}
|
||||||
Node::Block(block) => {
|
Node::Block(block) => {
|
||||||
self.push_block(FlowChild::Node(block), styles);
|
self.push_block(block.styled(styles));
|
||||||
}
|
}
|
||||||
Node::Sequence(list) => {
|
Node::Sequence(list) => {
|
||||||
for (node, mut inner) in list {
|
for (node, mut inner) in list {
|
||||||
@ -237,82 +220,120 @@ impl NodePacker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a trailing space.
|
/// Insert an inline-level element into the current paragraph.
|
||||||
fn trim(&mut self) {
|
fn push_inline(&mut self, child: ParChild) {
|
||||||
if self.last == Last::Space {
|
if let Some(child) = self.par_last.any() {
|
||||||
self.par.pop();
|
self.push_inline_impl(child);
|
||||||
self.last = Last::Other;
|
}
|
||||||
|
|
||||||
|
// The node must be both compatible with the current page and the
|
||||||
|
// current paragraph.
|
||||||
|
self.make_flow_compatible(child.styles());
|
||||||
|
self.make_par_compatible(child.styles());
|
||||||
|
self.push_inline_impl(child);
|
||||||
|
self.par_last = Last::Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a paragraph child, coalescing text nodes with compatible styles.
|
||||||
|
fn push_inline_impl(&mut self, child: ParChild) {
|
||||||
|
if let ParChild::Text(right) = &child {
|
||||||
|
if let Some(ParChild::Text(left)) = self.par.last_mut() {
|
||||||
|
if left.styles.compatible(&right.styles, TextNode::has_property) {
|
||||||
|
left.text.push_str(&right.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.par.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a block-level element into the current flow.
|
||||||
|
fn push_block(&mut self, node: PackedNode) {
|
||||||
|
let mut is_placed = false;
|
||||||
|
if let Some(placed) = node.downcast::<PlacedNode>() {
|
||||||
|
is_placed = true;
|
||||||
|
|
||||||
|
// This prevents paragraph spacing after the placed node if it
|
||||||
|
// is completely out-of-flow.
|
||||||
|
if placed.out_of_flow() {
|
||||||
|
self.flow_last = Last::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.parbreak(None);
|
||||||
|
self.make_flow_compatible(&node.styles);
|
||||||
|
|
||||||
|
if let Some(child) = self.flow_last.any() {
|
||||||
|
self.flow.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flow.push(FlowChild::Node(node));
|
||||||
|
self.parbreak(None);
|
||||||
|
|
||||||
|
// This prevents paragraph spacing between the placed node and
|
||||||
|
// the paragraph below it.
|
||||||
|
if is_placed {
|
||||||
|
self.flow_last = Last::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance to the next paragraph.
|
/// Advance to the next paragraph.
|
||||||
fn parbreak(&mut self) {
|
fn parbreak(&mut self, break_styles: Option<Styles>) {
|
||||||
self.trim();
|
self.finish_par();
|
||||||
|
|
||||||
let children = mem::take(&mut self.par);
|
// Insert paragraph spacing.
|
||||||
|
self.flow_last
|
||||||
|
.soft(FlowChild::Parbreak(break_styles.unwrap_or_default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_par(&mut self) {
|
||||||
|
let mut children = mem::take(&mut self.par);
|
||||||
let styles = mem::take(&mut self.par_styles);
|
let styles = mem::take(&mut self.par_styles);
|
||||||
|
self.par_last = Last::None;
|
||||||
|
|
||||||
|
// No empty paragraphs.
|
||||||
if !children.is_empty() {
|
if !children.is_empty() {
|
||||||
|
// Erase any styles that will be inherited anyway.
|
||||||
|
for child in &mut children {
|
||||||
|
child.styles_mut().erase(&styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(child) = self.flow_last.any() {
|
||||||
|
self.flow.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
// The paragraph's children are all compatible with the page, so the
|
// The paragraph's children are all compatible with the page, so the
|
||||||
// paragraph is too, meaning we don't need to check or intersect
|
// paragraph is too, meaning we don't need to check or intersect
|
||||||
// anything here.
|
// anything here.
|
||||||
let node = ParNode(children).pack().styled(styles);
|
let node = ParNode(children).pack().styled(styles);
|
||||||
self.flow.push(FlowChild::Node(node));
|
self.flow.push(FlowChild::Node(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last = Last::None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance to the next page.
|
/// Advance to the next page.
|
||||||
fn pagebreak(&mut self, keep: bool) {
|
fn pagebreak(&mut self, keep: bool) {
|
||||||
self.parbreak();
|
if self.block {
|
||||||
let children = mem::take(&mut self.flow);
|
return;
|
||||||
let styles = mem::take(&mut self.page_styles);
|
}
|
||||||
|
|
||||||
|
self.finish_par();
|
||||||
|
|
||||||
|
let styles = mem::take(&mut self.flow_styles);
|
||||||
|
let mut children = mem::take(&mut self.flow);
|
||||||
|
self.flow_last = Last::None;
|
||||||
|
|
||||||
if keep || !children.is_empty() {
|
if keep || !children.is_empty() {
|
||||||
|
// Erase any styles that will be inherited anyway.
|
||||||
|
for child in &mut children {
|
||||||
|
child.styles_mut().erase(&styles);
|
||||||
|
}
|
||||||
|
|
||||||
let node = PageNode { node: FlowNode(children).pack(), styles };
|
let node = PageNode { node: FlowNode(children).pack(), styles };
|
||||||
self.document.push(node);
|
self.pages.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert text into the current paragraph.
|
|
||||||
fn push_text(&mut self, text: EcoString, styles: Styles) {
|
|
||||||
// TODO(set): Join compatible text nodes. Take care with space
|
|
||||||
// coalescing.
|
|
||||||
let node = TextNode { text, styles: Styles::new() };
|
|
||||||
self.push_inline(ParChild::Text(node), styles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert an inline-level element into the current paragraph.
|
|
||||||
fn push_inline(&mut self, mut child: ParChild, styles: Styles) {
|
|
||||||
match &mut child {
|
|
||||||
ParChild::Spacing(_) => {}
|
|
||||||
ParChild::Text(node) => node.styles.apply(&styles),
|
|
||||||
ParChild::Node(node) => node.styles.apply(&styles),
|
|
||||||
ParChild::Decorate(_) => {}
|
|
||||||
ParChild::Undecorate => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The node must be both compatible with the current page and the
|
|
||||||
// current paragraph.
|
|
||||||
self.make_page_compatible(&styles);
|
|
||||||
self.make_par_compatible(&styles);
|
|
||||||
self.par.push(child);
|
|
||||||
self.last = Last::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a block-level element into the current flow.
|
|
||||||
fn push_block(&mut self, mut child: FlowChild, styles: Styles) {
|
|
||||||
self.parbreak();
|
|
||||||
|
|
||||||
match &mut child {
|
|
||||||
FlowChild::Spacing(_) => {}
|
|
||||||
FlowChild::Node(node) => node.styles.apply(&styles),
|
|
||||||
}
|
|
||||||
|
|
||||||
// The node must be compatible with the current page.
|
|
||||||
self.make_page_compatible(&styles);
|
|
||||||
self.flow.push(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Break to a new paragraph if the `styles` contain paragraph styles that
|
/// Break to a new paragraph if the `styles` contain paragraph styles that
|
||||||
/// are incompatible with the current paragraph.
|
/// are incompatible with the current paragraph.
|
||||||
fn make_par_compatible(&mut self, styles: &Styles) {
|
fn make_par_compatible(&mut self, styles: &Styles) {
|
||||||
@ -321,8 +342,8 @@ impl NodePacker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.par_styles.compatible(&styles, ParNode::has_property) {
|
if !self.is_par_compatible(styles) {
|
||||||
self.parbreak();
|
self.parbreak(None);
|
||||||
self.par_styles = styles.clone();
|
self.par_styles = styles.clone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -331,19 +352,55 @@ impl NodePacker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Break to a new page if the `styles` contain page styles that are
|
/// Break to a new page if the `styles` contain page styles that are
|
||||||
/// incompatible with the current page.
|
/// incompatible with the current flow.
|
||||||
fn make_page_compatible(&mut self, styles: &Styles) {
|
fn make_flow_compatible(&mut self, styles: &Styles) {
|
||||||
if self.flow.is_empty() && self.par.is_empty() {
|
if self.flow.is_empty() && self.par.is_empty() {
|
||||||
self.page_styles = styles.clone();
|
self.flow_styles = styles.clone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.page_styles.compatible(&styles, PageNode::has_property) {
|
if !self.is_flow_compatible(styles) {
|
||||||
self.pagebreak(false);
|
self.pagebreak(false);
|
||||||
self.page_styles = styles.clone();
|
self.flow_styles = styles.clone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.page_styles.intersect(styles);
|
self.flow_styles.intersect(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the given styles are compatible with the current page.
|
||||||
|
fn is_par_compatible(&self, styles: &Styles) -> bool {
|
||||||
|
self.par_styles.compatible(&styles, ParNode::has_property)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the given styles are compatible with the current flow.
|
||||||
|
fn is_flow_compatible(&self, styles: &Styles) -> bool {
|
||||||
|
self.block || self.flow_styles.compatible(&styles, PageNode::has_property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finite state machine for spacing coalescing.
|
||||||
|
enum Last<N> {
|
||||||
|
None,
|
||||||
|
Any,
|
||||||
|
Soft(N),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N> Last<N> {
|
||||||
|
fn any(&mut self) -> Option<N> {
|
||||||
|
match mem::replace(self, Self::Any) {
|
||||||
|
Self::Soft(soft) => Some(soft),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn soft(&mut self, soft: N) {
|
||||||
|
if let Self::Any = self {
|
||||||
|
*self = Self::Soft(soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hard(&mut self) {
|
||||||
|
*self = Self::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ use std::rc::Rc;
|
|||||||
/// A map of style properties.
|
/// A map of style properties.
|
||||||
#[derive(Default, Clone, Hash)]
|
#[derive(Default, Clone, Hash)]
|
||||||
pub struct Styles {
|
pub struct Styles {
|
||||||
pub(crate) map: Vec<(StyleId, Entry)>,
|
map: Vec<(StyleId, Entry)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styles {
|
impl Styles {
|
||||||
@ -21,10 +21,7 @@ impl Styles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a style map with a single property-value pair.
|
/// Create a style map with a single property-value pair.
|
||||||
pub fn one<P: Property>(key: P, value: P::Value) -> Self
|
pub fn one<P: Property>(key: P, value: P::Value) -> Self {
|
||||||
where
|
|
||||||
P::Value: Debug + Hash + PartialEq + 'static,
|
|
||||||
{
|
|
||||||
let mut styles = Self::new();
|
let mut styles = Self::new();
|
||||||
styles.set(key, value);
|
styles.set(key, value);
|
||||||
styles
|
styles
|
||||||
@ -36,21 +33,31 @@ impl Styles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the value for a style property.
|
/// Set the value for a style property.
|
||||||
pub fn set<P: Property>(&mut self, key: P, value: P::Value)
|
pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
|
||||||
where
|
|
||||||
P::Value: Debug + Hash + PartialEq + 'static,
|
|
||||||
{
|
|
||||||
let id = StyleId::of::<P>();
|
let id = StyleId::of::<P>();
|
||||||
let entry = Entry::new(key, value);
|
|
||||||
|
|
||||||
for pair in &mut self.map {
|
for pair in &mut self.map {
|
||||||
if pair.0 == id {
|
if pair.0 == id {
|
||||||
pair.1 = entry;
|
let prev = pair.1.downcast::<P::Value>().unwrap();
|
||||||
|
let folded = P::combine(value, prev.clone());
|
||||||
|
pair.1 = Entry::new(key, folded);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.map.push((id, entry));
|
self.map.push((id, Entry::new(key, value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle a boolean style property.
|
||||||
|
pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) {
|
||||||
|
let id = StyleId::of::<P>();
|
||||||
|
for (i, pair) in self.map.iter_mut().enumerate() {
|
||||||
|
if pair.0 == id {
|
||||||
|
self.map.swap_remove(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.map.push((id, Entry::new(key, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the value of a copyable style property.
|
/// Get the value of a copyable style property.
|
||||||
@ -84,10 +91,15 @@ impl Styles {
|
|||||||
///
|
///
|
||||||
/// Properties from `self` take precedence over the ones from `outer`.
|
/// Properties from `self` take precedence over the ones from `outer`.
|
||||||
pub fn apply(&mut self, outer: &Self) {
|
pub fn apply(&mut self, outer: &Self) {
|
||||||
for pair in &outer.map {
|
'outer: for pair in &outer.map {
|
||||||
if self.map.iter().all(|&(id, _)| pair.0 != id) {
|
for (id, entry) in &mut self.map {
|
||||||
self.map.push(pair.clone());
|
if pair.0 == *id {
|
||||||
|
entry.apply(&pair.1);
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.map.push(pair.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,12 +117,18 @@ impl Styles {
|
|||||||
self.map.retain(|a| other.map.iter().any(|b| a == b));
|
self.map.retain(|a| other.map.iter().any(|b| a == b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep only those styles that are not also in `other`.
|
||||||
|
pub fn erase(&mut self, other: &Self) {
|
||||||
|
self.map.retain(|a| other.map.iter().all(|b| a != b));
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether two style maps are equal when filtered down to the given
|
/// Whether two style maps are equal when filtered down to the given
|
||||||
/// properties.
|
/// properties.
|
||||||
pub fn compatible<F>(&self, other: &Self, filter: F) -> bool
|
pub fn compatible<F>(&self, other: &Self, filter: F) -> bool
|
||||||
where
|
where
|
||||||
F: Fn(StyleId) -> bool,
|
F: Fn(StyleId) -> bool,
|
||||||
{
|
{
|
||||||
|
// TODO(set): Filtered length + one direction equal should suffice.
|
||||||
let f = |e: &&(StyleId, Entry)| filter(e.0);
|
let f = |e: &&(StyleId, Entry)| filter(e.0);
|
||||||
self.map.iter().filter(f).all(|pair| other.map.contains(pair))
|
self.map.iter().filter(f).all(|pair| other.map.contains(pair))
|
||||||
&& other.map.iter().filter(f).all(|pair| self.map.contains(pair))
|
&& other.map.iter().filter(f).all(|pair| self.map.contains(pair))
|
||||||
@ -119,73 +137,88 @@ impl Styles {
|
|||||||
|
|
||||||
impl Debug for Styles {
|
impl Debug for Styles {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
f.write_str("Styles ")?;
|
if f.alternate() {
|
||||||
f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish()
|
for pair in &self.map {
|
||||||
|
writeln!(f, "{:#?}", pair.1)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
f.write_str("Styles ")?;
|
||||||
|
f.debug_set().entries(self.map.iter().map(|pair| &pair.1)).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Styles {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.compatible(other, |_| true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An entry for a single style property.
|
/// An entry for a single style property.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Entry {
|
pub(crate) struct Entry(Rc<dyn Bounds>);
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
name: &'static str,
|
|
||||||
value: Rc<dyn Bounds>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
fn new<P: Property>(_: P, value: P::Value) -> Self
|
fn new<P: Property>(key: P, value: P::Value) -> Self {
|
||||||
where
|
Self(Rc::new((key, value)))
|
||||||
P::Value: Debug + Hash + PartialEq + 'static,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
name: P::NAME,
|
|
||||||
value: Rc::new(value),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn downcast<T: 'static>(&self) -> Option<&T> {
|
fn downcast<T: 'static>(&self) -> Option<&T> {
|
||||||
self.value.as_any().downcast_ref()
|
self.0.as_any().downcast_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, outer: &Self) {
|
||||||
|
*self = self.0.combine(outer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Entry {
|
impl Debug for Entry {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
#[cfg(debug_assertions)]
|
self.0.fmt(f)
|
||||||
write!(f, "{}: ", self.name)?;
|
|
||||||
write!(f, "{:?}", &self.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Entry {
|
impl PartialEq for Entry {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.value.dyn_eq(other)
|
self.0.dyn_eq(other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Entry {
|
impl Hash for Entry {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
state.write_u64(self.value.hash64());
|
state.write_u64(self.0.hash64());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait Bounds: Debug + 'static {
|
trait Bounds: 'static {
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result;
|
||||||
fn dyn_eq(&self, other: &Entry) -> bool;
|
fn dyn_eq(&self, other: &Entry) -> bool;
|
||||||
fn hash64(&self) -> u64;
|
fn hash64(&self) -> u64;
|
||||||
|
fn combine(&self, outer: &Entry) -> Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Bounds for T
|
impl<P> Bounds for (P, P::Value)
|
||||||
where
|
where
|
||||||
T: Debug + Hash + PartialEq + 'static,
|
P: Property,
|
||||||
|
P::Value: Debug + Hash + PartialEq + 'static,
|
||||||
{
|
{
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
self
|
&self.1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
write!(f, "#[{} = {:?}]", P::NAME, self.1)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}: {:?}", P::NAME, self.1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_eq(&self, other: &Entry) -> bool {
|
fn dyn_eq(&self, other: &Entry) -> bool {
|
||||||
if let Some(other) = other.downcast::<Self>() {
|
if let Some(other) = other.downcast::<P::Value>() {
|
||||||
self == other
|
&self.1 == other
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -194,7 +227,13 @@ where
|
|||||||
fn hash64(&self) -> u64 {
|
fn hash64(&self) -> u64 {
|
||||||
// No need to hash the TypeId since there's only one
|
// No need to hash the TypeId since there's only one
|
||||||
// valid value type per property.
|
// valid value type per property.
|
||||||
fxhash::hash64(self)
|
fxhash::hash64(&self.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combine(&self, outer: &Entry) -> Entry {
|
||||||
|
let outer = outer.downcast::<P::Value>().unwrap();
|
||||||
|
let combined = P::combine(self.1.clone(), outer.clone());
|
||||||
|
Entry::new(self.0, combined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,14 +241,17 @@ where
|
|||||||
///
|
///
|
||||||
/// This trait is not intended to be implemented manually, but rather through
|
/// This trait is not intended to be implemented manually, but rather through
|
||||||
/// the `properties!` macro.
|
/// the `properties!` macro.
|
||||||
pub trait Property: 'static {
|
pub trait Property: Copy + 'static {
|
||||||
/// The type of this property, for example, this could be
|
/// The type of this property, for example, this could be
|
||||||
/// [`Length`](crate::geom::Length) for a `WIDTH` property.
|
/// [`Length`](crate::geom::Length) for a `WIDTH` property.
|
||||||
type Value;
|
type Value: Debug + Clone + Hash + PartialEq + 'static;
|
||||||
|
|
||||||
/// The name of the property, used for debug printing.
|
/// The name of the property, used for debug printing.
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
|
|
||||||
|
/// Combine the property with an outer value.
|
||||||
|
fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value;
|
||||||
|
|
||||||
/// The default value of the property.
|
/// The default value of the property.
|
||||||
fn default() -> Self::Value;
|
fn default() -> Self::Value;
|
||||||
|
|
||||||
@ -235,7 +277,8 @@ impl StyleId {
|
|||||||
/// Generate the property keys for a node.
|
/// Generate the property keys for a node.
|
||||||
macro_rules! properties {
|
macro_rules! properties {
|
||||||
($node:ty, $(
|
($node:ty, $(
|
||||||
$(#[$attr:meta])*
|
$(#[doc = $doc:expr])*
|
||||||
|
$(#[fold($combine:expr)])?
|
||||||
$name:ident: $type:ty = $default:expr
|
$name:ident: $type:ty = $default:expr
|
||||||
),* $(,)?) => {
|
),* $(,)?) => {
|
||||||
// TODO(set): Fix possible name clash.
|
// TODO(set): Fix possible name clash.
|
||||||
@ -250,6 +293,13 @@ macro_rules! properties {
|
|||||||
|
|
||||||
pub struct Key<T>(pub PhantomData<T>);
|
pub struct Key<T>(pub PhantomData<T>);
|
||||||
|
|
||||||
|
impl<T> Copy for Key<T> {}
|
||||||
|
impl<T> Clone for Key<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Property for Key<$type> {
|
impl Property for Key<$type> {
|
||||||
type Value = $type;
|
type Value = $type;
|
||||||
|
|
||||||
@ -257,6 +307,15 @@ macro_rules! properties {
|
|||||||
stringify!($node), "::", stringify!($name)
|
stringify!($node), "::", stringify!($name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[allow(unused_mut, unused_variables)]
|
||||||
|
fn combine(mut inner: Self::Value, outer: Self::Value) -> Self::Value {
|
||||||
|
$(
|
||||||
|
let combine: fn(Self::Value, Self::Value) -> Self::Value = $combine;
|
||||||
|
inner = combine(inner, outer);
|
||||||
|
)?
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
|
||||||
fn default() -> Self::Value {
|
fn default() -> Self::Value {
|
||||||
$default
|
$default
|
||||||
}
|
}
|
||||||
@ -275,7 +334,7 @@ macro_rules! properties {
|
|||||||
false || $(id == StyleId::of::<$name::Key<$type>>())||*
|
false || $(id == StyleId::of::<$name::Key<$type>>())||*
|
||||||
}
|
}
|
||||||
|
|
||||||
$($(#[$attr])* pub const $name: $name::Key<$type>
|
$($(#[doc = $doc])* pub const $name: $name::Key<$type>
|
||||||
= $name::Key(PhantomData);)*
|
= $name::Key(PhantomData);)*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -284,9 +343,9 @@ macro_rules! properties {
|
|||||||
|
|
||||||
/// Set a style property to a value if the value is `Some`.
|
/// Set a style property to a value if the value is `Some`.
|
||||||
macro_rules! set {
|
macro_rules! set {
|
||||||
($ctx:expr, $target:expr => $value:expr) => {
|
($styles:expr, $target:expr => $value:expr) => {
|
||||||
if let Some(v) = $value {
|
if let Some(v) = $value {
|
||||||
$ctx.styles.set($target, v);
|
$styles.set($target, v);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ pub enum Value {
|
|||||||
Float(f64),
|
Float(f64),
|
||||||
/// A length: `12pt`, `3cm`.
|
/// A length: `12pt`, `3cm`.
|
||||||
Length(Length),
|
Length(Length),
|
||||||
/// An angle: `1.5rad`, `90deg`.
|
/// An angle: `1.5rad`, `90deg`.
|
||||||
Angle(Angle),
|
Angle(Angle),
|
||||||
/// A relative value: `50%`.
|
/// A relative value: `50%`.
|
||||||
Relative(Relative),
|
Relative(Relative),
|
||||||
@ -146,7 +146,7 @@ impl Debug for Value {
|
|||||||
Self::Str(v) => Debug::fmt(v, f),
|
Self::Str(v) => Debug::fmt(v, f),
|
||||||
Self::Array(v) => Debug::fmt(v, f),
|
Self::Array(v) => Debug::fmt(v, f),
|
||||||
Self::Dict(v) => Debug::fmt(v, f),
|
Self::Dict(v) => Debug::fmt(v, f),
|
||||||
Self::Node(v) => Debug::fmt(v, f),
|
Self::Node(_) => f.pad("<template>"),
|
||||||
Self::Func(v) => Debug::fmt(v, f),
|
Self::Func(v) => Debug::fmt(v, f),
|
||||||
Self::Dyn(v) => Debug::fmt(v, f),
|
Self::Dyn(v) => Debug::fmt(v, f),
|
||||||
}
|
}
|
||||||
@ -386,13 +386,13 @@ primitive! { f64: "float", Float, Int(v) => v as f64 }
|
|||||||
primitive! { Length: "length", Length }
|
primitive! { Length: "length", Length }
|
||||||
primitive! { Angle: "angle", Angle }
|
primitive! { Angle: "angle", Angle }
|
||||||
primitive! { Relative: "relative", Relative }
|
primitive! { Relative: "relative", Relative }
|
||||||
primitive! { Linear: "linear", Linear, Length(v) => v.into(), Relative(v) => v.into() }
|
primitive! { Linear: "relative length", Linear, Length(v) => v.into(), Relative(v) => v.into() }
|
||||||
primitive! { Fractional: "fractional", Fractional }
|
primitive! { Fractional: "fractional length", Fractional }
|
||||||
primitive! { Color: "color", Color }
|
primitive! { Color: "color", Color }
|
||||||
primitive! { EcoString: "string", Str }
|
primitive! { EcoString: "string", Str }
|
||||||
primitive! { Array: "array", Array }
|
primitive! { Array: "array", Array }
|
||||||
primitive! { Dict: "dictionary", Dict }
|
primitive! { Dict: "dictionary", Dict }
|
||||||
primitive! { Node: "node", Node }
|
primitive! { Node: "template", Node }
|
||||||
primitive! { Function: "function", Func }
|
primitive! { Function: "function", Func }
|
||||||
|
|
||||||
impl Cast<Value> for Value {
|
impl Cast<Value> for Value {
|
||||||
|
@ -283,11 +283,11 @@ impl Face {
|
|||||||
/// Look up a vertical metric at the given font size.
|
/// Look up a vertical metric at the given font size.
|
||||||
pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length {
|
pub fn vertical_metric(&self, metric: VerticalFontMetric, size: Length) -> Length {
|
||||||
match metric {
|
match metric {
|
||||||
VerticalFontMetric::Ascender => self.ascender.to_length(size),
|
VerticalFontMetric::Ascender => self.ascender.resolve(size),
|
||||||
VerticalFontMetric::CapHeight => self.cap_height.to_length(size),
|
VerticalFontMetric::CapHeight => self.cap_height.resolve(size),
|
||||||
VerticalFontMetric::XHeight => self.x_height.to_length(size),
|
VerticalFontMetric::XHeight => self.x_height.resolve(size),
|
||||||
VerticalFontMetric::Baseline => Length::zero(),
|
VerticalFontMetric::Baseline => Length::zero(),
|
||||||
VerticalFontMetric::Descender => self.descender.to_length(size),
|
VerticalFontMetric::Descender => self.descender.resolve(size),
|
||||||
VerticalFontMetric::Linear(v) => v.resolve(size),
|
VerticalFontMetric::Linear(v) => v.resolve(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,11 @@ impl Frame {
|
|||||||
wrapper.push(Point::zero(), Element::Group(group));
|
wrapper.push(Point::zero(), Element::Group(group));
|
||||||
*self = wrapper;
|
*self = wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Link the whole frame to a resource.
|
||||||
|
pub fn link(&mut self, url: impl Into<String>) {
|
||||||
|
self.push(Point::zero(), Element::Link(url.into(), self.size));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Frame {
|
impl Debug for Frame {
|
||||||
@ -180,7 +185,7 @@ pub struct Text {
|
|||||||
impl Text {
|
impl Text {
|
||||||
/// The width of the text run.
|
/// The width of the text run.
|
||||||
pub fn width(&self) -> Length {
|
pub fn width(&self) -> Length {
|
||||||
self.glyphs.iter().map(|g| g.x_advance.to_length(self.size)).sum()
|
self.glyphs.iter().map(|g| g.x_advance.resolve(self.size)).sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ impl Em {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert to a length at the given font size.
|
/// Convert to a length at the given font size.
|
||||||
pub fn to_length(self, font_size: Length) -> Length {
|
pub fn resolve(self, font_size: Length) -> Length {
|
||||||
self.get() * font_size
|
self.get() * font_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,16 @@ impl Linear {
|
|||||||
self.rel.resolve(length) + self.abs
|
self.rel.resolve(length) + self.abs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compose with another linear.
|
||||||
|
///
|
||||||
|
/// The resulting linear is (self ∘ inner)(x) = self(inner(x)).
|
||||||
|
pub fn compose(self, inner: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
rel: self.rel * inner.rel,
|
||||||
|
abs: self.rel.resolve(inner.abs) + self.abs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether both parts are zero.
|
/// Whether both parts are zero.
|
||||||
pub fn is_zero(self) -> bool {
|
pub fn is_zero(self) -> bool {
|
||||||
self.rel.is_zero() && self.abs.is_zero()
|
self.rel.is_zero() && self.abs.is_zero()
|
||||||
@ -155,10 +165,7 @@ impl Mul<Linear> for f64 {
|
|||||||
type Output = Linear;
|
type Output = Linear;
|
||||||
|
|
||||||
fn mul(self, other: Linear) -> Linear {
|
fn mul(self, other: Linear) -> Linear {
|
||||||
Linear {
|
other * self
|
||||||
rel: self * other.rel,
|
|
||||||
abs: self * other.abs,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,14 +91,19 @@ pub trait Layout {
|
|||||||
pub struct PackedNode {
|
pub struct PackedNode {
|
||||||
/// The type-erased node.
|
/// The type-erased node.
|
||||||
node: Rc<dyn Bounds>,
|
node: Rc<dyn Bounds>,
|
||||||
|
/// A precomputed hash for the node.
|
||||||
|
#[cfg(feature = "layout-cache")]
|
||||||
|
hash: u64,
|
||||||
/// The node's styles.
|
/// The node's styles.
|
||||||
pub styles: Styles,
|
pub styles: Styles,
|
||||||
#[cfg(feature = "layout-cache")]
|
|
||||||
/// A precomputed hash.
|
|
||||||
hash: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackedNode {
|
impl PackedNode {
|
||||||
|
/// Check whether the contained node is a specific layout node.
|
||||||
|
pub fn is<T: 'static>(&self) -> bool {
|
||||||
|
self.node.as_any().is::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to downcast to a specific layout node.
|
/// Try to downcast to a specific layout node.
|
||||||
pub fn downcast<T>(&self) -> Option<&T>
|
pub fn downcast<T>(&self) -> Option<&T>
|
||||||
where
|
where
|
||||||
@ -173,7 +178,15 @@ impl Layout for PackedNode {
|
|||||||
return self.layout_impl(ctx, regions);
|
return self.layout_impl(ctx, regions);
|
||||||
|
|
||||||
#[cfg(feature = "layout-cache")]
|
#[cfg(feature = "layout-cache")]
|
||||||
ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
|
let hash = {
|
||||||
|
let mut state = fxhash::FxHasher64::default();
|
||||||
|
self.hash(&mut state);
|
||||||
|
ctx.styles.hash(&mut state);
|
||||||
|
state.finish()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-cache")]
|
||||||
|
ctx.layouts.get(hash, regions).unwrap_or_else(|| {
|
||||||
ctx.level += 1;
|
ctx.level += 1;
|
||||||
let frames = self.layout_impl(ctx, regions);
|
let frames = self.layout_impl(ctx, regions);
|
||||||
ctx.level -= 1;
|
ctx.level -= 1;
|
||||||
@ -191,7 +204,7 @@ impl Layout for PackedNode {
|
|||||||
panic!("constraints did not match regions they were created for");
|
panic!("constraints did not match regions they were created for");
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.layouts.insert(self.hash, entry);
|
ctx.layouts.insert(hash, entry);
|
||||||
frames
|
frames
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -219,18 +232,31 @@ impl PackedNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for PackedNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
self.styles.fmt(f)?;
|
||||||
|
}
|
||||||
|
self.node.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for PackedNode {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::as_ptr(&self.node) as *const () == Rc::as_ptr(&other.node) as *const ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Hash for PackedNode {
|
impl Hash for PackedNode {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
// Hash the node.
|
||||||
#[cfg(feature = "layout-cache")]
|
#[cfg(feature = "layout-cache")]
|
||||||
state.write_u64(self.hash);
|
state.write_u64(self.hash);
|
||||||
#[cfg(not(feature = "layout-cache"))]
|
#[cfg(not(feature = "layout-cache"))]
|
||||||
state.write_u64(self.hash64());
|
state.write_u64(self.hash64());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for PackedNode {
|
// Hash the styles.
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
self.styles.hash(state);
|
||||||
self.node.fmt(f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ use super::ParNode;
|
|||||||
|
|
||||||
/// `align`: Configure the alignment along the layouting axes.
|
/// `align`: Configure the alignment along the layouting axes.
|
||||||
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
let aligns = args.expect::<Spec<_>>("alignment")?;
|
let aligns: Spec<_> = args.expect("alignment")?;
|
||||||
let body = args.expect::<Node>("body")?;
|
let body: Node = args.expect("body")?;
|
||||||
|
|
||||||
let mut styles = Styles::new();
|
let mut styles = Styles::new();
|
||||||
if let Some(align) = aligns.x {
|
if let Some(align) = aligns.x {
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
use super::prelude::*;
|
|
||||||
use crate::util::EcoString;
|
|
||||||
|
|
||||||
/// `strike`: Typeset striken-through text.
|
|
||||||
pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
||||||
line_impl(args, LineKind::Strikethrough)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `underline`: Typeset underlined text.
|
|
||||||
pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
||||||
line_impl(args, LineKind::Underline)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `overline`: Typeset text with an overline.
|
|
||||||
pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
||||||
line_impl(args, LineKind::Overline)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
|
|
||||||
let stroke = args.named("stroke")?.or_else(|| args.find());
|
|
||||||
let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
|
|
||||||
let offset = args.named("offset")?;
|
|
||||||
let extent = args.named("extent")?.unwrap_or_default();
|
|
||||||
let body: Node = args.expect("body")?;
|
|
||||||
Ok(Value::Node(body.decorated(Decoration::Line(
|
|
||||||
LineDecoration { kind, stroke, thickness, offset, extent },
|
|
||||||
))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `link`: Typeset text as a link.
|
|
||||||
pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
||||||
let url = args.expect::<EcoString>("url")?;
|
|
||||||
let body = args.find().unwrap_or_else(|| {
|
|
||||||
let mut text = url.as_str();
|
|
||||||
for prefix in ["mailto:", "tel:"] {
|
|
||||||
text = text.trim_start_matches(prefix);
|
|
||||||
}
|
|
||||||
Node::Text(text.into())
|
|
||||||
});
|
|
||||||
Ok(Value::Node(body.decorated(Decoration::Link(url))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A decoration for a frame.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub enum Decoration {
|
|
||||||
/// A link to an external resource.
|
|
||||||
Link(EcoString),
|
|
||||||
/// An underline/strikethrough/overline decoration.
|
|
||||||
Line(LineDecoration),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Decoration {
|
|
||||||
/// Apply a decoration to a child's frame.
|
|
||||||
pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
|
|
||||||
match self {
|
|
||||||
Decoration::Link(href) => {
|
|
||||||
let link = Element::Link(href.to_string(), frame.size);
|
|
||||||
frame.push(Point::zero(), link);
|
|
||||||
}
|
|
||||||
Decoration::Line(line) => {
|
|
||||||
line.apply(ctx, frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines a line that is positioned over, under or on top of text.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct LineDecoration {
|
|
||||||
/// The kind of line.
|
|
||||||
pub kind: LineKind,
|
|
||||||
/// Stroke color of the line, defaults to the text color if `None`.
|
|
||||||
pub stroke: Option<Paint>,
|
|
||||||
/// Thickness of the line's strokes (dependent on scaled font size), read
|
|
||||||
/// from the font tables if `None`.
|
|
||||||
pub thickness: Option<Linear>,
|
|
||||||
/// Position of the line relative to the baseline (dependent on scaled font
|
|
||||||
/// size), read from the font tables if `None`.
|
|
||||||
pub offset: Option<Linear>,
|
|
||||||
/// Amount that the line will be longer or shorter than its associated text
|
|
||||||
/// (dependent on scaled font size).
|
|
||||||
pub extent: Linear,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The kind of line decoration.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub enum LineKind {
|
|
||||||
/// A line under text.
|
|
||||||
Underline,
|
|
||||||
/// A line through text.
|
|
||||||
Strikethrough,
|
|
||||||
/// A line over text.
|
|
||||||
Overline,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineDecoration {
|
|
||||||
/// Apply a line decoration to a all text elements in a frame.
|
|
||||||
pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
|
|
||||||
for i in 0 .. frame.elements.len() {
|
|
||||||
let (pos, child) = &frame.elements[i];
|
|
||||||
if let Element::Text(text) = child {
|
|
||||||
let face = ctx.fonts.get(text.face_id);
|
|
||||||
let metrics = match self.kind {
|
|
||||||
LineKind::Underline => face.underline,
|
|
||||||
LineKind::Strikethrough => face.strikethrough,
|
|
||||||
LineKind::Overline => face.overline,
|
|
||||||
};
|
|
||||||
|
|
||||||
let thickness = self
|
|
||||||
.thickness
|
|
||||||
.map(|s| s.resolve(text.size))
|
|
||||||
.unwrap_or(metrics.thickness.to_length(text.size));
|
|
||||||
|
|
||||||
let stroke = Stroke {
|
|
||||||
paint: self.stroke.unwrap_or(text.fill),
|
|
||||||
thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
let offset = self
|
|
||||||
.offset
|
|
||||||
.map(|s| s.resolve(text.size))
|
|
||||||
.unwrap_or(-metrics.position.to_length(text.size));
|
|
||||||
|
|
||||||
let extent = self.extent.resolve(text.size);
|
|
||||||
|
|
||||||
let subpos = Point::new(pos.x - extent, pos.y + offset);
|
|
||||||
let target = Point::new(text.width() + 2.0 * extent, Length::zero());
|
|
||||||
let shape = Shape::stroked(Geometry::Line(target), stroke);
|
|
||||||
frame.push(subpos, Element::Shape(shape));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ use super::prelude::*;
|
|||||||
use super::PageNode;
|
use super::PageNode;
|
||||||
|
|
||||||
/// The root layout node, a document consisting of top-level page runs.
|
/// The root layout node, a document consisting of top-level page runs.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Hash)]
|
||||||
pub struct DocumentNode(pub Vec<PageNode>);
|
pub struct DocumentNode(pub Vec<PageNode>);
|
||||||
|
|
||||||
impl DocumentNode {
|
impl DocumentNode {
|
||||||
@ -11,3 +11,10 @@ impl DocumentNode {
|
|||||||
self.0.iter().flat_map(|node| node.layout(ctx)).collect()
|
self.0.iter().flat_map(|node| node.layout(ctx)).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for DocumentNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str("Document ")?;
|
||||||
|
f.debug_list().entries(&self.0).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,41 +1,13 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
use super::{AlignNode, ParNode, PlacedNode, Spacing};
|
use super::{AlignNode, ParNode, PlacedNode, SpacingKind, SpacingNode, TextNode};
|
||||||
|
|
||||||
/// `flow`: A vertical flow of paragraphs and other layout nodes.
|
|
||||||
pub fn flow(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
||||||
enum Child {
|
|
||||||
Spacing(Spacing),
|
|
||||||
Any(Node),
|
|
||||||
}
|
|
||||||
|
|
||||||
castable! {
|
|
||||||
Child,
|
|
||||||
Expected: "linear, fractional or template",
|
|
||||||
Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
|
|
||||||
Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
|
|
||||||
Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
|
|
||||||
Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
|
|
||||||
Value::Node(v) => Self::Any(v),
|
|
||||||
}
|
|
||||||
|
|
||||||
let children = args
|
|
||||||
.all()
|
|
||||||
.map(|child| match child {
|
|
||||||
Child::Spacing(spacing) => FlowChild::Spacing(spacing),
|
|
||||||
Child::Any(node) => FlowChild::Node(node.into_block()),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(Value::block(FlowNode(children)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A vertical flow of content consisting of paragraphs and other layout nodes.
|
/// A vertical flow of content consisting of paragraphs and other layout nodes.
|
||||||
///
|
///
|
||||||
/// This node is reponsible for layouting both the top-level content flow and
|
/// This node is reponsible for layouting both the top-level content flow and
|
||||||
/// the contents of boxes.
|
/// the contents of boxes.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Hash)]
|
||||||
pub struct FlowNode(pub Vec<FlowChild>);
|
pub struct FlowNode(pub Vec<FlowChild>);
|
||||||
|
|
||||||
impl Layout for FlowNode {
|
impl Layout for FlowNode {
|
||||||
@ -48,20 +20,55 @@ impl Layout for FlowNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for FlowNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str("Flow ")?;
|
||||||
|
f.debug_list().entries(&self.0).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A child of a flow node.
|
/// A child of a flow node.
|
||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub enum FlowChild {
|
pub enum FlowChild {
|
||||||
/// Vertical spacing between other children.
|
/// Vertical spacing between other children.
|
||||||
Spacing(Spacing),
|
Spacing(SpacingNode),
|
||||||
/// An arbitrary node.
|
/// An arbitrary node.
|
||||||
Node(PackedNode),
|
Node(PackedNode),
|
||||||
|
/// A paragraph break.
|
||||||
|
Parbreak(Styles),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlowChild {
|
||||||
|
/// A reference to the child's styles.
|
||||||
|
pub fn styles(&self) -> &Styles {
|
||||||
|
match self {
|
||||||
|
Self::Spacing(node) => &node.styles,
|
||||||
|
Self::Node(node) => &node.styles,
|
||||||
|
Self::Parbreak(styles) => styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mutable reference to the child's styles.
|
||||||
|
pub fn styles_mut(&mut self) -> &mut Styles {
|
||||||
|
match self {
|
||||||
|
Self::Spacing(node) => &mut node.styles,
|
||||||
|
Self::Node(node) => &mut node.styles,
|
||||||
|
Self::Parbreak(styles) => styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for FlowChild {
|
impl Debug for FlowChild {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(spacing) => spacing.fmt(f),
|
Self::Spacing(node) => node.fmt(f),
|
||||||
Self::Node(node) => node.fmt(f),
|
Self::Node(node) => node.fmt(f),
|
||||||
|
Self::Parbreak(styles) => {
|
||||||
|
if f.alternate() {
|
||||||
|
styles.fmt(f)?;
|
||||||
|
}
|
||||||
|
write!(f, "Parbreak")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,21 +131,28 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
/// Layout all children.
|
/// Layout all children.
|
||||||
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
|
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
for child in self.children {
|
for child in self.children {
|
||||||
match *child {
|
match child {
|
||||||
FlowChild::Spacing(Spacing::Linear(v)) => {
|
FlowChild::Spacing(node) => match node.kind {
|
||||||
self.layout_absolute(v);
|
SpacingKind::Linear(v) => self.layout_absolute(v),
|
||||||
}
|
SpacingKind::Fractional(v) => {
|
||||||
FlowChild::Spacing(Spacing::Fractional(v)) => {
|
self.items.push(FlowItem::Fractional(v));
|
||||||
self.items.push(FlowItem::Fractional(v));
|
self.fr += v;
|
||||||
self.fr += v;
|
}
|
||||||
}
|
},
|
||||||
FlowChild::Node(ref node) => {
|
FlowChild::Node(node) => {
|
||||||
if self.regions.is_full() {
|
if self.regions.is_full() {
|
||||||
self.finish_region();
|
self.finish_region();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layout_node(ctx, node);
|
self.layout_node(ctx, node);
|
||||||
}
|
}
|
||||||
|
FlowChild::Parbreak(styles) => {
|
||||||
|
let chain = styles.chain(&ctx.styles);
|
||||||
|
let amount = chain
|
||||||
|
.get(ParNode::SPACING)
|
||||||
|
.resolve(chain.get(TextNode::SIZE).abs);
|
||||||
|
self.layout_absolute(amount.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,24 +172,25 @@ impl<'a> FlowLayouter<'a> {
|
|||||||
|
|
||||||
/// Layout a node.
|
/// Layout a node.
|
||||||
fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) {
|
fn layout_node(&mut self, ctx: &mut LayoutContext, node: &PackedNode) {
|
||||||
// Add paragraph spacing.
|
// Placed nodes that are out of flow produce placed items which aren't
|
||||||
// TODO(set): Handle edge cases.
|
// aligned later.
|
||||||
if !self.items.is_empty() {
|
|
||||||
let spacing = node.styles.chain(&ctx.styles).get(ParNode::SPACING);
|
|
||||||
self.layout_absolute(spacing.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(placed) = node.downcast::<PlacedNode>() {
|
if let Some(placed) = node.downcast::<PlacedNode>() {
|
||||||
let frame = node.layout(ctx, &self.regions).remove(0);
|
|
||||||
if placed.out_of_flow() {
|
if placed.out_of_flow() {
|
||||||
|
let frame = node.layout(ctx, &self.regions).remove(0);
|
||||||
self.items.push(FlowItem::Placed(frame.item));
|
self.items.push(FlowItem::Placed(frame.item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How to align the node.
|
||||||
let aligns = Spec::new(
|
let aligns = Spec::new(
|
||||||
// TODO(set): Align paragraph according to its internal alignment.
|
// For non-expanding paragraphs it is crucial that we align the
|
||||||
Align::Left,
|
// whole paragraph according to its internal alignment.
|
||||||
|
if node.is::<ParNode>() {
|
||||||
|
node.styles.chain(&ctx.styles).get(ParNode::ALIGN)
|
||||||
|
} else {
|
||||||
|
Align::Left
|
||||||
|
},
|
||||||
// Vertical align node alignment is respected by the flow node.
|
// Vertical align node alignment is respected by the flow node.
|
||||||
node.downcast::<AlignNode>()
|
node.downcast::<AlignNode>()
|
||||||
.and_then(|aligned| aligned.aligns.y)
|
.and_then(|aligned| aligned.aligns.y)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
|
use super::LinkNode;
|
||||||
use crate::diag::Error;
|
use crate::diag::Error;
|
||||||
use crate::image::ImageId;
|
use crate::image::ImageId;
|
||||||
|
|
||||||
@ -85,6 +86,11 @@ impl Layout for ImageNode {
|
|||||||
frame.clip();
|
frame.clip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply link if it exists.
|
||||||
|
if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
|
||||||
|
frame.link(url);
|
||||||
|
}
|
||||||
|
|
||||||
vec![frame.constrain(Constraints::tight(regions))]
|
vec![frame.constrain(Constraints::tight(regions))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
src/library/link.rs
Normal file
29
src/library/link.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use super::prelude::*;
|
||||||
|
use crate::util::EcoString;
|
||||||
|
|
||||||
|
/// `link`: Link text or other elements.
|
||||||
|
pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
|
let url: String = args.expect::<EcoString>("url")?.into();
|
||||||
|
let body = args.find().unwrap_or_else(|| {
|
||||||
|
let mut text = url.as_str();
|
||||||
|
for prefix in ["mailto:", "tel:"] {
|
||||||
|
text = text.trim_start_matches(prefix);
|
||||||
|
}
|
||||||
|
Node::Text(text.into())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Value::Node(
|
||||||
|
body.styled(Styles::one(LinkNode::URL, Some(url))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Host for link styles.
|
||||||
|
#[derive(Debug, Hash)]
|
||||||
|
pub struct LinkNode;
|
||||||
|
|
||||||
|
properties! {
|
||||||
|
LinkNode,
|
||||||
|
|
||||||
|
/// An URL to link to.
|
||||||
|
URL: Option<String> = None,
|
||||||
|
}
|
@ -4,11 +4,11 @@
|
|||||||
//! definitions.
|
//! definitions.
|
||||||
|
|
||||||
mod align;
|
mod align;
|
||||||
mod deco;
|
|
||||||
mod document;
|
mod document;
|
||||||
mod flow;
|
mod flow;
|
||||||
mod grid;
|
mod grid;
|
||||||
mod image;
|
mod image;
|
||||||
|
mod link;
|
||||||
mod pad;
|
mod pad;
|
||||||
mod page;
|
mod page;
|
||||||
mod par;
|
mod par;
|
||||||
@ -23,6 +23,7 @@ mod utility;
|
|||||||
|
|
||||||
/// Helpful imports for creating library functionality.
|
/// Helpful imports for creating library functionality.
|
||||||
mod prelude {
|
mod prelude {
|
||||||
|
pub use std::fmt::{self, Debug, Formatter};
|
||||||
pub use std::rc::Rc;
|
pub use std::rc::Rc;
|
||||||
|
|
||||||
pub use crate::diag::{At, TypResult};
|
pub use crate::diag::{At, TypResult};
|
||||||
@ -36,10 +37,10 @@ mod prelude {
|
|||||||
|
|
||||||
pub use self::image::*;
|
pub use self::image::*;
|
||||||
pub use align::*;
|
pub use align::*;
|
||||||
pub use deco::*;
|
|
||||||
pub use document::*;
|
pub use document::*;
|
||||||
pub use flow::*;
|
pub use flow::*;
|
||||||
pub use grid::*;
|
pub use grid::*;
|
||||||
|
pub use link::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
pub use page::*;
|
pub use page::*;
|
||||||
pub use par::*;
|
pub use par::*;
|
||||||
@ -62,6 +63,7 @@ pub fn new() -> Scope {
|
|||||||
// Text.
|
// Text.
|
||||||
std.def_func("font", font);
|
std.def_func("font", font);
|
||||||
std.def_func("par", par);
|
std.def_func("par", par);
|
||||||
|
std.def_func("parbreak", parbreak);
|
||||||
std.def_func("strike", strike);
|
std.def_func("strike", strike);
|
||||||
std.def_func("underline", underline);
|
std.def_func("underline", underline);
|
||||||
std.def_func("overline", overline);
|
std.def_func("overline", overline);
|
||||||
@ -74,7 +76,6 @@ pub fn new() -> Scope {
|
|||||||
std.def_func("v", v);
|
std.def_func("v", v);
|
||||||
std.def_func("box", box_);
|
std.def_func("box", box_);
|
||||||
std.def_func("block", block);
|
std.def_func("block", block);
|
||||||
std.def_func("flow", flow);
|
|
||||||
std.def_func("stack", stack);
|
std.def_func("stack", stack);
|
||||||
std.def_func("grid", grid);
|
std.def_func("grid", grid);
|
||||||
std.def_func("pad", pad);
|
std.def_func("pad", pad);
|
||||||
|
@ -14,32 +14,43 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
|
Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let body: Option<Node> = args.find();
|
||||||
|
|
||||||
|
let mut map = Styles::new();
|
||||||
|
let styles = match body {
|
||||||
|
Some(_) => &mut map,
|
||||||
|
None => &mut ctx.styles,
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
|
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
|
||||||
ctx.styles.set(PageNode::CLASS, paper.class());
|
styles.set(PageNode::CLASS, paper.class());
|
||||||
ctx.styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
|
styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
|
||||||
ctx.styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
|
styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(width) = args.named("width")? {
|
if let Some(width) = args.named("width")? {
|
||||||
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
|
styles.set(PageNode::CLASS, PaperClass::Custom);
|
||||||
ctx.styles.set(PageNode::WIDTH, width);
|
styles.set(PageNode::WIDTH, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(height) = args.named("height")? {
|
if let Some(height) = args.named("height")? {
|
||||||
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
|
styles.set(PageNode::CLASS, PaperClass::Custom);
|
||||||
ctx.styles.set(PageNode::HEIGHT, height);
|
styles.set(PageNode::HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
let margins = args.named("margins")?;
|
let margins = args.named("margins")?;
|
||||||
|
|
||||||
set!(ctx, PageNode::FLIPPED => args.named("flipped")?);
|
set!(styles, PageNode::FLIPPED => args.named("flipped")?);
|
||||||
set!(ctx, PageNode::LEFT => args.named("left")?.or(margins));
|
set!(styles, PageNode::LEFT => args.named("left")?.or(margins));
|
||||||
set!(ctx, PageNode::TOP => args.named("top")?.or(margins));
|
set!(styles, PageNode::TOP => args.named("top")?.or(margins));
|
||||||
set!(ctx, PageNode::RIGHT => args.named("right")?.or(margins));
|
set!(styles, PageNode::RIGHT => args.named("right")?.or(margins));
|
||||||
set!(ctx, PageNode::BOTTOM => args.named("bottom")?.or(margins));
|
set!(styles, PageNode::BOTTOM => args.named("bottom")?.or(margins));
|
||||||
set!(ctx, PageNode::FILL => args.named("fill")?);
|
set!(styles, PageNode::FILL => args.named("fill")?);
|
||||||
|
|
||||||
Ok(Value::None)
|
Ok(match body {
|
||||||
|
Some(body) => Value::block(body.into_block().styled(map)),
|
||||||
|
None => Value::None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `pagebreak`: Start a new page.
|
/// `pagebreak`: Start a new page.
|
||||||
@ -48,7 +59,7 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Layouts its child onto one or multiple pages.
|
/// Layouts its child onto one or multiple pages.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Hash)]
|
||||||
pub struct PageNode {
|
pub struct PageNode {
|
||||||
/// The node producing the content.
|
/// The node producing the content.
|
||||||
pub node: PackedNode,
|
pub node: PackedNode,
|
||||||
@ -82,9 +93,8 @@ properties! {
|
|||||||
impl PageNode {
|
impl PageNode {
|
||||||
/// Layout the page run into a sequence of frames, one per page.
|
/// Layout the page run into a sequence of frames, one per page.
|
||||||
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||||
// TODO(set): Use chaining.
|
let prev = ctx.styles.clone();
|
||||||
let prev = std::mem::replace(&mut ctx.styles, self.styles.clone());
|
ctx.styles = self.styles.chain(&ctx.styles);
|
||||||
ctx.styles.apply(&prev);
|
|
||||||
|
|
||||||
// When one of the lengths is infinite the page fits its content along
|
// When one of the lengths is infinite the page fits its content along
|
||||||
// that axis.
|
// that axis.
|
||||||
@ -127,6 +137,17 @@ impl PageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for PageNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
self.styles.fmt(f)?;
|
||||||
|
}
|
||||||
|
f.write_str("Page(")?;
|
||||||
|
self.node.fmt(f)?;
|
||||||
|
f.write_str(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Specification of a paper.
|
/// Specification of a paper.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct Paper {
|
pub struct Paper {
|
||||||
|
@ -6,7 +6,7 @@ use unicode_bidi::{BidiInfo, Level};
|
|||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
use super::{shape, Decoration, ShapedText, Spacing, TextNode};
|
use super::{shape, ShapedText, SpacingKind, SpacingNode, TextNode};
|
||||||
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
|
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
|
||||||
|
|
||||||
/// `par`: Configure paragraphs.
|
/// `par`: Configure paragraphs.
|
||||||
@ -41,16 +41,21 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
|
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
|
||||||
}
|
}
|
||||||
|
|
||||||
set!(ctx, ParNode::DIR => dir);
|
set!(ctx.styles, ParNode::DIR => dir);
|
||||||
set!(ctx, ParNode::ALIGN => align);
|
set!(ctx.styles, ParNode::ALIGN => align);
|
||||||
set!(ctx, ParNode::LEADING => leading);
|
set!(ctx.styles, ParNode::LEADING => leading);
|
||||||
set!(ctx, ParNode::SPACING => spacing);
|
set!(ctx.styles, ParNode::SPACING => spacing);
|
||||||
|
|
||||||
Ok(Value::None)
|
Ok(Value::None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `parbreak`: Start a new paragraph.
|
||||||
|
pub fn parbreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
|
||||||
|
Ok(Value::Node(Node::Parbreak))
|
||||||
|
}
|
||||||
|
|
||||||
/// A node that arranges its children into a paragraph.
|
/// A node that arranges its children into a paragraph.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Hash)]
|
||||||
pub struct ParNode(pub Vec<ParChild>);
|
pub struct ParNode(pub Vec<ParChild>);
|
||||||
|
|
||||||
properties! {
|
properties! {
|
||||||
@ -60,11 +65,10 @@ properties! {
|
|||||||
DIR: Dir = Dir::LTR,
|
DIR: Dir = Dir::LTR,
|
||||||
/// How to align text and inline objects in their line.
|
/// How to align text and inline objects in their line.
|
||||||
ALIGN: Align = Align::Left,
|
ALIGN: Align = Align::Left,
|
||||||
// TODO(set): Make relative to font size.
|
|
||||||
/// The spacing between lines (dependent on scaled font size).
|
/// The spacing between lines (dependent on scaled font size).
|
||||||
LEADING: Length = Length::pt(6.5),
|
LEADING: Linear = Relative::new(0.65).into(),
|
||||||
/// The spacing between paragraphs (dependent on scaled font size).
|
/// The spacing between paragraphs (dependent on scaled font size).
|
||||||
SPACING: Length = Length::pt(12.0),
|
SPACING: Linear = Relative::new(1.2).into(),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for ParNode {
|
impl Layout for ParNode {
|
||||||
@ -118,34 +122,59 @@ impl ParNode {
|
|||||||
ParChild::Spacing(_) => " ",
|
ParChild::Spacing(_) => " ",
|
||||||
ParChild::Text(ref node) => &node.text,
|
ParChild::Text(ref node) => &node.text,
|
||||||
ParChild::Node(_) => "\u{FFFC}",
|
ParChild::Node(_) => "\u{FFFC}",
|
||||||
ParChild::Decorate(_) | ParChild::Undecorate => "",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for ParNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str("Par ")?;
|
||||||
|
f.debug_list().entries(&self.0).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A child of a paragraph node.
|
/// A child of a paragraph node.
|
||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub enum ParChild {
|
pub enum ParChild {
|
||||||
/// Spacing between other nodes.
|
/// Spacing between other nodes.
|
||||||
Spacing(Spacing),
|
Spacing(SpacingNode),
|
||||||
/// A run of text and how to align it in its line.
|
/// A run of text and how to align it in its line.
|
||||||
Text(TextNode),
|
Text(TextNode),
|
||||||
/// Any child node and how to align it in its line.
|
/// Any child node and how to align it in its line.
|
||||||
Node(PackedNode),
|
Node(PackedNode),
|
||||||
/// A decoration that applies until a matching `Undecorate`.
|
}
|
||||||
Decorate(Decoration),
|
|
||||||
/// The end of a decoration.
|
impl ParChild {
|
||||||
Undecorate,
|
/// Create a text child.
|
||||||
|
pub fn text(text: impl Into<EcoString>, styles: Styles) -> Self {
|
||||||
|
Self::Text(TextNode { text: text.into(), styles })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to the child's styles.
|
||||||
|
pub fn styles(&self) -> &Styles {
|
||||||
|
match self {
|
||||||
|
Self::Spacing(node) => &node.styles,
|
||||||
|
Self::Text(node) => &node.styles,
|
||||||
|
Self::Node(node) => &node.styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mutable reference to the child's styles.
|
||||||
|
pub fn styles_mut(&mut self) -> &mut Styles {
|
||||||
|
match self {
|
||||||
|
Self::Spacing(node) => &mut node.styles,
|
||||||
|
Self::Text(node) => &mut node.styles,
|
||||||
|
Self::Node(node) => &mut node.styles,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for ParChild {
|
impl Debug for ParChild {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(v) => write!(f, "Spacing({:?})", v),
|
Self::Spacing(node) => node.fmt(f),
|
||||||
Self::Text(node) => write!(f, "Text({:?})", node.text),
|
Self::Text(node) => node.fmt(f),
|
||||||
Self::Node(node) => node.fmt(f),
|
Self::Node(node) => node.fmt(f),
|
||||||
Self::Decorate(deco) => write!(f, "Decorate({:?})", deco),
|
|
||||||
Self::Undecorate => write!(f, "Undecorate"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,8 +192,6 @@ struct ParLayouter<'a> {
|
|||||||
items: Vec<ParItem<'a>>,
|
items: Vec<ParItem<'a>>,
|
||||||
/// The ranges of the items in `bidi.text`.
|
/// The ranges of the items in `bidi.text`.
|
||||||
ranges: Vec<Range>,
|
ranges: Vec<Range>,
|
||||||
/// The decorations and the ranges they span.
|
|
||||||
decos: Vec<(Range, &'a Decoration)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Range of a substring of text.
|
/// Range of a substring of text.
|
||||||
@ -192,22 +219,22 @@ impl<'a> ParLayouter<'a> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
let mut ranges = vec![];
|
let mut ranges = vec![];
|
||||||
let mut starts = vec![];
|
|
||||||
let mut decos = vec![];
|
|
||||||
|
|
||||||
// Layout the children and collect them into items.
|
// Layout the children and collect them into items.
|
||||||
for (range, child) in par.ranges().zip(&par.0) {
|
for (range, child) in par.ranges().zip(&par.0) {
|
||||||
match *child {
|
match child {
|
||||||
ParChild::Spacing(Spacing::Linear(v)) => {
|
ParChild::Spacing(node) => match node.kind {
|
||||||
let resolved = v.resolve(regions.current.x);
|
SpacingKind::Linear(v) => {
|
||||||
items.push(ParItem::Absolute(resolved));
|
let resolved = v.resolve(regions.current.x);
|
||||||
ranges.push(range);
|
items.push(ParItem::Absolute(resolved));
|
||||||
}
|
ranges.push(range);
|
||||||
ParChild::Spacing(Spacing::Fractional(v)) => {
|
}
|
||||||
items.push(ParItem::Fractional(v));
|
SpacingKind::Fractional(v) => {
|
||||||
ranges.push(range);
|
items.push(ParItem::Fractional(v));
|
||||||
}
|
ranges.push(range);
|
||||||
ParChild::Text(ref node) => {
|
}
|
||||||
|
},
|
||||||
|
ParChild::Text(node) => {
|
||||||
// TODO: Also split by language and script.
|
// TODO: Also split by language and script.
|
||||||
let mut cursor = range.start;
|
let mut cursor = range.start;
|
||||||
for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) {
|
for (level, group) in bidi.levels[range].group_by_key(|&lvl| lvl) {
|
||||||
@ -221,36 +248,23 @@ impl<'a> ParLayouter<'a> {
|
|||||||
ranges.push(subrange);
|
ranges.push(subrange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParChild::Node(ref node) => {
|
ParChild::Node(node) => {
|
||||||
let size = Size::new(regions.current.x, regions.base.y);
|
let size = Size::new(regions.current.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).remove(0);
|
let frame = node.layout(ctx, &pod).remove(0);
|
||||||
items.push(ParItem::Frame(Rc::take(frame.item)));
|
items.push(ParItem::Frame(Rc::take(frame.item)));
|
||||||
ranges.push(range);
|
ranges.push(range);
|
||||||
}
|
}
|
||||||
ParChild::Decorate(ref deco) => {
|
|
||||||
starts.push((range.start, deco));
|
|
||||||
}
|
|
||||||
ParChild::Undecorate => {
|
|
||||||
if let Some((start, deco)) = starts.pop() {
|
|
||||||
decos.push((start .. range.end, deco));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (start, deco) in starts {
|
let align = ctx.styles.get(ParNode::ALIGN);
|
||||||
decos.push((start .. bidi.text.len(), deco));
|
let leading = ctx
|
||||||
}
|
.styles
|
||||||
|
.get(ParNode::LEADING)
|
||||||
|
.resolve(ctx.styles.get(TextNode::SIZE).abs);
|
||||||
|
|
||||||
Self {
|
Self { align, leading, bidi, items, ranges }
|
||||||
align: ctx.styles.get(ParNode::ALIGN),
|
|
||||||
leading: ctx.styles.get(ParNode::LEADING),
|
|
||||||
bidi,
|
|
||||||
items,
|
|
||||||
ranges,
|
|
||||||
decos,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find first-fit line breaks and build the paragraph.
|
/// Find first-fit line breaks and build the paragraph.
|
||||||
@ -496,28 +510,19 @@ impl<'a> LineLayout<'a> {
|
|||||||
let mut output = Frame::new(size);
|
let mut output = Frame::new(size);
|
||||||
output.baseline = Some(self.baseline);
|
output.baseline = Some(self.baseline);
|
||||||
|
|
||||||
for (range, item) in self.reordered() {
|
for item in self.reordered() {
|
||||||
let mut position = |mut frame: Frame| {
|
let mut position = |frame: Frame| {
|
||||||
// Decorate.
|
|
||||||
for (deco_range, deco) in &self.par.decos {
|
|
||||||
if deco_range.contains(&range.start) {
|
|
||||||
deco.apply(ctx, &mut frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let x = offset + self.par.align.resolve(remaining);
|
let x = offset + self.par.align.resolve(remaining);
|
||||||
let y = self.baseline - frame.baseline();
|
let y = self.baseline - frame.baseline();
|
||||||
offset += frame.size.x;
|
offset += frame.size.x;
|
||||||
|
|
||||||
// Add to the line's frame.
|
|
||||||
output.merge_frame(Point::new(x, y), frame);
|
output.merge_frame(Point::new(x, y), frame);
|
||||||
};
|
};
|
||||||
|
|
||||||
match *item {
|
match item {
|
||||||
ParItem::Absolute(v) => offset += v,
|
ParItem::Absolute(v) => offset += *v,
|
||||||
ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining),
|
ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining),
|
||||||
ParItem::Text(ref shaped) => position(shaped.build()),
|
ParItem::Text(shaped) => position(shaped.build(&ctx.fonts)),
|
||||||
ParItem::Frame(ref frame) => position(frame.clone()),
|
ParItem::Frame(frame) => position(frame.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,7 +530,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate through the line's items in visual order.
|
/// Iterate through the line's items in visual order.
|
||||||
fn reordered(&self) -> impl Iterator<Item = (Range, &ParItem<'a>)> {
|
fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> {
|
||||||
// The bidi crate doesn't like empty lines.
|
// The bidi crate doesn't like empty lines.
|
||||||
let (levels, runs) = if !self.line.is_empty() {
|
let (levels, runs) = if !self.line.is_empty() {
|
||||||
// Find the paragraph that contains the line.
|
// Find the paragraph that contains the line.
|
||||||
@ -557,7 +562,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
Either::Right(range.rev())
|
Either::Right(range.rev())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(move |idx| (self.ranges[idx].clone(), self.get(idx).unwrap()))
|
.map(move |idx| self.get(idx).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the index of the item whose range contains the `text_offset`.
|
/// Find the index of the item whose range contains the `text_offset`.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
|
use super::LinkNode;
|
||||||
|
|
||||||
/// `rect`: A rectangle with optional content.
|
/// `rect`: A rectangle with optional content.
|
||||||
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
@ -76,17 +77,14 @@ fn shape_impl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The shape's contents.
|
// The shape's contents.
|
||||||
let body = args.find::<Node>();
|
let child = args
|
||||||
|
.find()
|
||||||
|
.map(|body: Node| body.into_block().padded(Sides::splat(padding)));
|
||||||
|
|
||||||
Ok(Value::inline(
|
Ok(Value::inline(
|
||||||
ShapeNode {
|
ShapeNode { kind, fill, stroke, child }
|
||||||
kind,
|
.pack()
|
||||||
fill,
|
.sized(Spec::new(width, height)),
|
||||||
stroke,
|
|
||||||
child: body.map(|body| body.into_block().padded(Sides::splat(padding))),
|
|
||||||
}
|
|
||||||
.pack()
|
|
||||||
.sized(Spec::new(width, height)),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,9 +150,10 @@ impl Layout for ShapeNode {
|
|||||||
frames = vec![Frame::new(size).constrain(Constraints::tight(regions))];
|
frames = vec![Frame::new(size).constrain(Constraints::tight(regions))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let frame = Rc::make_mut(&mut frames[0].item);
|
||||||
|
|
||||||
// Add fill and/or stroke.
|
// Add fill and/or stroke.
|
||||||
if self.fill.is_some() || self.stroke.is_some() {
|
if self.fill.is_some() || self.stroke.is_some() {
|
||||||
let frame = Rc::make_mut(&mut frames[0].item);
|
|
||||||
let geometry = match self.kind {
|
let geometry = match self.kind {
|
||||||
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
|
ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size),
|
||||||
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
|
ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size),
|
||||||
@ -169,6 +168,11 @@ impl Layout for ShapeNode {
|
|||||||
frame.prepend(Point::zero(), Element::Shape(shape));
|
frame.prepend(Point::zero(), Element::Shape(shape));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply link if it exists.
|
||||||
|
if let Some(url) = ctx.styles.get_ref(LinkNode::URL) {
|
||||||
|
frame.link(url);
|
||||||
|
}
|
||||||
|
|
||||||
frames
|
frames
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,27 @@ pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single run of text with the same style.
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct SpacingNode {
|
||||||
|
/// The kind of spacing.
|
||||||
|
pub kind: SpacingKind,
|
||||||
|
/// The rspacing's styles.
|
||||||
|
pub styles: Styles,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for SpacingNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
self.styles.fmt(f)?;
|
||||||
|
}
|
||||||
|
write!(f, "{:?}", self.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Kinds of spacing.
|
/// Kinds of spacing.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum Spacing {
|
pub enum SpacingKind {
|
||||||
/// A length stated in absolute values and/or relative to the parent's size.
|
/// A length stated in absolute values and/or relative to the parent's size.
|
||||||
Linear(Linear),
|
Linear(Linear),
|
||||||
/// A length that is the fraction of the remaining free space in the parent.
|
/// A length that is the fraction of the remaining free space in the parent.
|
||||||
@ -26,7 +44,7 @@ pub enum Spacing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
castable! {
|
castable! {
|
||||||
Spacing,
|
SpacingKind,
|
||||||
Expected: "linear or fractional",
|
Expected: "linear or fractional",
|
||||||
Value::Length(v) => Self::Linear(v.into()),
|
Value::Length(v) => Self::Linear(v.into()),
|
||||||
Value::Relative(v) => Self::Linear(v.into()),
|
Value::Relative(v) => Self::Linear(v.into()),
|
||||||
|
@ -1,25 +1,10 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
use super::{AlignNode, Spacing};
|
use super::{AlignNode, SpacingKind, SpacingNode};
|
||||||
|
|
||||||
/// `stack`: Stack children along an axis.
|
/// `stack`: Stack children along an axis.
|
||||||
pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
enum Child {
|
|
||||||
Spacing(Spacing),
|
|
||||||
Any(Node),
|
|
||||||
}
|
|
||||||
|
|
||||||
castable! {
|
|
||||||
Child,
|
|
||||||
Expected: "linear, fractional or template",
|
|
||||||
Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
|
|
||||||
Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
|
|
||||||
Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
|
|
||||||
Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
|
|
||||||
Value::Node(v) => Self::Any(v),
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir = args.named("dir")?.unwrap_or(Dir::TTB);
|
let dir = args.named("dir")?.unwrap_or(Dir::TTB);
|
||||||
let spacing = args.named("spacing")?;
|
let spacing = args.named("spacing")?;
|
||||||
|
|
||||||
@ -29,19 +14,15 @@ pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|||||||
// Build the list of stack children.
|
// Build the list of stack children.
|
||||||
for child in args.all() {
|
for child in args.all() {
|
||||||
match child {
|
match child {
|
||||||
Child::Spacing(v) => {
|
StackChild::Spacing(_) => delayed = None,
|
||||||
children.push(StackChild::Spacing(v));
|
StackChild::Node(_) => {
|
||||||
delayed = None;
|
|
||||||
}
|
|
||||||
Child::Any(child) => {
|
|
||||||
if let Some(v) = delayed {
|
if let Some(v) = delayed {
|
||||||
children.push(StackChild::Spacing(v));
|
children.push(StackChild::spacing(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
children.push(StackChild::Node(child.into_block()));
|
|
||||||
delayed = spacing;
|
delayed = spacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
children.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Value::block(StackNode { dir, children }))
|
Ok(Value::block(StackNode { dir, children }))
|
||||||
@ -70,20 +51,37 @@ impl Layout for StackNode {
|
|||||||
#[derive(Hash)]
|
#[derive(Hash)]
|
||||||
pub enum StackChild {
|
pub enum StackChild {
|
||||||
/// Spacing between other nodes.
|
/// Spacing between other nodes.
|
||||||
Spacing(Spacing),
|
Spacing(SpacingNode),
|
||||||
/// An arbitrary node.
|
/// An arbitrary node.
|
||||||
Node(PackedNode),
|
Node(PackedNode),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StackChild {
|
||||||
|
/// Create a spacing node from a spacing kind.
|
||||||
|
pub fn spacing(kind: SpacingKind) -> Self {
|
||||||
|
Self::Spacing(SpacingNode { kind, styles: Styles::new() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for StackChild {
|
impl Debug for StackChild {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(spacing) => spacing.fmt(f),
|
Self::Spacing(node) => node.fmt(f),
|
||||||
Self::Node(node) => node.fmt(f),
|
Self::Node(node) => node.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
castable! {
|
||||||
|
StackChild,
|
||||||
|
Expected: "linear, fractional or template",
|
||||||
|
Value::Length(v) => Self::spacing(SpacingKind::Linear(v.into())),
|
||||||
|
Value::Relative(v) => Self::spacing(SpacingKind::Linear(v.into())),
|
||||||
|
Value::Linear(v) => Self::spacing(SpacingKind::Linear(v)),
|
||||||
|
Value::Fractional(v) => Self::spacing(SpacingKind::Fractional(v)),
|
||||||
|
Value::Node(v) => Self::Node(v.into_block()),
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs stack layout.
|
/// Performs stack layout.
|
||||||
struct StackLayouter<'a> {
|
struct StackLayouter<'a> {
|
||||||
/// The stack node to layout.
|
/// The stack node to layout.
|
||||||
@ -144,15 +142,15 @@ impl<'a> StackLayouter<'a> {
|
|||||||
/// Layout all children.
|
/// Layout all children.
|
||||||
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
|
fn layout(mut self, ctx: &mut LayoutContext) -> Vec<Constrained<Rc<Frame>>> {
|
||||||
for child in &self.stack.children {
|
for child in &self.stack.children {
|
||||||
match *child {
|
match child {
|
||||||
StackChild::Spacing(Spacing::Linear(v)) => {
|
StackChild::Spacing(node) => match node.kind {
|
||||||
self.layout_absolute(v);
|
SpacingKind::Linear(v) => self.layout_absolute(v),
|
||||||
}
|
SpacingKind::Fractional(v) => {
|
||||||
StackChild::Spacing(Spacing::Fractional(v)) => {
|
self.items.push(StackItem::Fractional(v));
|
||||||
self.items.push(StackItem::Fractional(v));
|
self.fr += v;
|
||||||
self.fr += v;
|
}
|
||||||
}
|
},
|
||||||
StackChild::Node(ref node) => {
|
StackChild::Node(node) => {
|
||||||
if self.regions.is_full() {
|
if self.regions.is_full() {
|
||||||
self.finish_region();
|
self.finish_region();
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::ops::Range;
|
use std::ops::{BitXor, Range};
|
||||||
|
|
||||||
use rustybuzz::{Feature, UnicodeBuffer};
|
use rustybuzz::{Feature, UnicodeBuffer};
|
||||||
use ttf_parser::Tag;
|
use ttf_parser::Tag;
|
||||||
|
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
|
use super::LinkNode;
|
||||||
use crate::font::{
|
use crate::font::{
|
||||||
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
|
Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight,
|
||||||
VerticalFontMetric,
|
VerticalFontMetric,
|
||||||
@ -16,43 +17,81 @@ use crate::util::{EcoString, SliceExt};
|
|||||||
|
|
||||||
/// `font`: Configure the font.
|
/// `font`: Configure the font.
|
||||||
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
|
let body = args.find::<Node>();
|
||||||
|
|
||||||
|
let mut map = Styles::new();
|
||||||
|
let styles = match body {
|
||||||
|
Some(_) => &mut map,
|
||||||
|
None => &mut ctx.styles,
|
||||||
|
};
|
||||||
|
|
||||||
let list = args.named("family")?.or_else(|| {
|
let list = args.named("family")?.or_else(|| {
|
||||||
let families: Vec<_> = args.all().collect();
|
let families: Vec<_> = args.all().collect();
|
||||||
(!families.is_empty()).then(|| families)
|
(!families.is_empty()).then(|| families)
|
||||||
});
|
});
|
||||||
|
|
||||||
set!(ctx, TextNode::FAMILY_LIST => list);
|
set!(styles, TextNode::FAMILY_LIST => list);
|
||||||
set!(ctx, TextNode::SERIF_LIST => args.named("serif")?);
|
set!(styles, TextNode::SERIF_LIST => args.named("serif")?);
|
||||||
set!(ctx, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?);
|
set!(styles, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?);
|
||||||
set!(ctx, TextNode::MONOSPACE_LIST => args.named("monospace")?);
|
set!(styles, TextNode::MONOSPACE_LIST => args.named("monospace")?);
|
||||||
set!(ctx, TextNode::FALLBACK => args.named("fallback")?);
|
set!(styles, TextNode::FALLBACK => args.named("fallback")?);
|
||||||
set!(ctx, TextNode::STYLE => args.named("style")?);
|
set!(styles, TextNode::STYLE => args.named("style")?);
|
||||||
set!(ctx, TextNode::WEIGHT => args.named("weight")?);
|
set!(styles, TextNode::WEIGHT => args.named("weight")?);
|
||||||
set!(ctx, TextNode::STRETCH => args.named("stretch")?);
|
set!(styles, TextNode::STRETCH => args.named("stretch")?);
|
||||||
set!(ctx, TextNode::FILL => args.named("fill")?.or_else(|| args.find()));
|
set!(styles, TextNode::FILL => args.named("fill")?.or_else(|| args.find()));
|
||||||
set!(ctx, TextNode::SIZE => args.named("size")?.or_else(|| args.find()));
|
set!(styles, TextNode::SIZE => args.named("size")?.or_else(|| args.find()));
|
||||||
set!(ctx, TextNode::TRACKING => args.named("tracking")?.map(Em::new));
|
set!(styles, TextNode::TRACKING => args.named("tracking")?.map(Em::new));
|
||||||
set!(ctx, TextNode::TOP_EDGE => args.named("top-edge")?);
|
set!(styles, TextNode::TOP_EDGE => args.named("top-edge")?);
|
||||||
set!(ctx, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?);
|
set!(styles, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?);
|
||||||
set!(ctx, TextNode::KERNING => args.named("kerning")?);
|
set!(styles, TextNode::KERNING => args.named("kerning")?);
|
||||||
set!(ctx, TextNode::SMALLCAPS => args.named("smallcaps")?);
|
set!(styles, TextNode::SMALLCAPS => args.named("smallcaps")?);
|
||||||
set!(ctx, TextNode::ALTERNATES => args.named("alternates")?);
|
set!(styles, TextNode::ALTERNATES => args.named("alternates")?);
|
||||||
set!(ctx, TextNode::STYLISTIC_SET => args.named("stylistic-set")?);
|
set!(styles, TextNode::STYLISTIC_SET => args.named("stylistic-set")?);
|
||||||
set!(ctx, TextNode::LIGATURES => args.named("ligatures")?);
|
set!(styles, TextNode::LIGATURES => args.named("ligatures")?);
|
||||||
set!(ctx, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
|
set!(styles, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
|
||||||
set!(ctx, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
|
set!(styles, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
|
||||||
set!(ctx, TextNode::NUMBER_TYPE => args.named("number-type")?);
|
set!(styles, TextNode::NUMBER_TYPE => args.named("number-type")?);
|
||||||
set!(ctx, TextNode::NUMBER_WIDTH => args.named("number-width")?);
|
set!(styles, TextNode::NUMBER_WIDTH => args.named("number-width")?);
|
||||||
set!(ctx, TextNode::NUMBER_POSITION => args.named("number-position")?);
|
set!(styles, TextNode::NUMBER_POSITION => args.named("number-position")?);
|
||||||
set!(ctx, TextNode::SLASHED_ZERO => args.named("slashed-zero")?);
|
set!(styles, TextNode::SLASHED_ZERO => args.named("slashed-zero")?);
|
||||||
set!(ctx, TextNode::FRACTIONS => args.named("fractions")?);
|
set!(styles, TextNode::FRACTIONS => args.named("fractions")?);
|
||||||
set!(ctx, TextNode::FEATURES => args.named("features")?);
|
set!(styles, TextNode::FEATURES => args.named("features")?);
|
||||||
|
|
||||||
Ok(Value::None)
|
Ok(match body {
|
||||||
|
Some(body) => Value::Node(body.styled(map)),
|
||||||
|
None => Value::None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `strike`: Typeset striken-through text.
|
||||||
|
pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
|
line_impl(args, LineKind::Strikethrough)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `underline`: Typeset underlined text.
|
||||||
|
pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
|
line_impl(args, LineKind::Underline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `overline`: Typeset text with an overline.
|
||||||
|
pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||||
|
line_impl(args, LineKind::Overline)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
|
||||||
|
let stroke = args.named("stroke")?.or_else(|| args.find());
|
||||||
|
let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
|
||||||
|
let offset = args.named("offset")?;
|
||||||
|
let extent = args.named("extent")?.unwrap_or_default();
|
||||||
|
let body: Node = args.expect("body")?;
|
||||||
|
let deco = LineDecoration { kind, stroke, thickness, offset, extent };
|
||||||
|
Ok(Value::Node(
|
||||||
|
body.styled(Styles::one(TextNode::LINES, vec![deco])),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single run of text with the same style.
|
/// A single run of text with the same style.
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Hash)]
|
||||||
pub struct TextNode {
|
pub struct TextNode {
|
||||||
/// The run's text.
|
/// The run's text.
|
||||||
pub text: EcoString,
|
pub text: EcoString,
|
||||||
@ -82,17 +121,22 @@ properties! {
|
|||||||
/// The width of the glyphs.
|
/// The width of the glyphs.
|
||||||
STRETCH: FontStretch = FontStretch::NORMAL,
|
STRETCH: FontStretch = FontStretch::NORMAL,
|
||||||
/// Whether the font weight should be increased by 300.
|
/// Whether the font weight should be increased by 300.
|
||||||
|
#[fold(bool::bitxor)]
|
||||||
STRONG: bool = false,
|
STRONG: bool = false,
|
||||||
/// Whether the the font style should be inverted.
|
/// Whether the the font style should be inverted.
|
||||||
|
#[fold(bool::bitxor)]
|
||||||
EMPH: bool = false,
|
EMPH: bool = false,
|
||||||
/// Whether a monospace font should be preferred.
|
/// Whether a monospace font should be preferred.
|
||||||
MONOSPACE: bool = false,
|
MONOSPACE: bool = false,
|
||||||
/// The glyph fill color.
|
/// The glyph fill color.
|
||||||
FILL: Paint = RgbaColor::BLACK.into(),
|
FILL: Paint = RgbaColor::BLACK.into(),
|
||||||
|
/// Decorative lines.
|
||||||
|
#[fold(|a, b| a.into_iter().chain(b).collect())]
|
||||||
|
LINES: Vec<LineDecoration> = vec![],
|
||||||
|
|
||||||
/// The size of the glyphs.
|
/// The size of the glyphs.
|
||||||
// TODO(set): Resolve relative to outer font size.
|
#[fold(Linear::compose)]
|
||||||
SIZE: Length = Length::pt(11.0),
|
SIZE: Linear = Length::pt(11.0).into(),
|
||||||
/// The amount of space that should be added between characters.
|
/// The amount of space that should be added between characters.
|
||||||
TRACKING: Em = Em::zero(),
|
TRACKING: Em = Em::zero(),
|
||||||
/// The top end of the text bounding box.
|
/// The top end of the text bounding box.
|
||||||
@ -128,6 +172,15 @@ properties! {
|
|||||||
FEATURES: Vec<(Tag, u32)> = vec![],
|
FEATURES: Vec<(Tag, u32)> = vec![],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for TextNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
self.styles.fmt(f)?;
|
||||||
|
}
|
||||||
|
write!(f, "Text({:?})", self.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A generic or named font family.
|
/// A generic or named font family.
|
||||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum FontFamily {
|
pub enum FontFamily {
|
||||||
@ -332,6 +385,35 @@ castable! {
|
|||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines a line that is positioned over, under or on top of text.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct LineDecoration {
|
||||||
|
/// The kind of line.
|
||||||
|
pub kind: LineKind,
|
||||||
|
/// Stroke color of the line, defaults to the text color if `None`.
|
||||||
|
pub stroke: Option<Paint>,
|
||||||
|
/// Thickness of the line's strokes (dependent on scaled font size), read
|
||||||
|
/// from the font tables if `None`.
|
||||||
|
pub thickness: Option<Linear>,
|
||||||
|
/// Position of the line relative to the baseline (dependent on scaled font
|
||||||
|
/// size), read from the font tables if `None`.
|
||||||
|
pub offset: Option<Linear>,
|
||||||
|
/// Amount that the line will be longer or shorter than its associated text
|
||||||
|
/// (dependent on scaled font size).
|
||||||
|
pub extent: Linear,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of line decoration.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum LineKind {
|
||||||
|
/// A line under text.
|
||||||
|
Underline,
|
||||||
|
/// A line through text.
|
||||||
|
Strikethrough,
|
||||||
|
/// A line over text.
|
||||||
|
Overline,
|
||||||
|
}
|
||||||
|
|
||||||
/// Shape text into [`ShapedText`].
|
/// Shape text into [`ShapedText`].
|
||||||
pub fn shape<'a>(
|
pub fn shape<'a>(
|
||||||
fonts: &mut FontStore,
|
fonts: &mut FontStore,
|
||||||
@ -520,7 +602,7 @@ fn measure(
|
|||||||
let mut top = Length::zero();
|
let mut top = Length::zero();
|
||||||
let mut bottom = Length::zero();
|
let mut bottom = Length::zero();
|
||||||
|
|
||||||
let size = styles.get(TextNode::SIZE);
|
let size = styles.get(TextNode::SIZE).abs;
|
||||||
let top_edge = styles.get(TextNode::TOP_EDGE);
|
let top_edge = styles.get(TextNode::TOP_EDGE);
|
||||||
let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
|
let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
|
||||||
|
|
||||||
@ -545,7 +627,7 @@ fn measure(
|
|||||||
expand(face);
|
expand(face);
|
||||||
|
|
||||||
for glyph in group {
|
for glyph in group {
|
||||||
width += glyph.x_advance.to_length(size);
|
width += glyph.x_advance.resolve(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -553,7 +635,7 @@ fn measure(
|
|||||||
(Size::new(width, top + bottom), top)
|
(Size::new(width, top + bottom), top)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved the font variant with `STRONG` and `EMPH` factored in.
|
/// Resolve the font variant with `STRONG` and `EMPH` factored in.
|
||||||
fn variant(styles: &Styles) -> FontVariant {
|
fn variant(styles: &Styles) -> FontVariant {
|
||||||
let mut variant = FontVariant::new(
|
let mut variant = FontVariant::new(
|
||||||
styles.get(TextNode::STYLE),
|
styles.get(TextNode::STYLE),
|
||||||
@ -721,7 +803,7 @@ pub struct ShapedGlyph {
|
|||||||
|
|
||||||
impl<'a> ShapedText<'a> {
|
impl<'a> ShapedText<'a> {
|
||||||
/// Build the shaped text's frame.
|
/// Build the shaped text's frame.
|
||||||
pub fn build(&self) -> Frame {
|
pub fn build(&self, fonts: &FontStore) -> Frame {
|
||||||
let mut offset = Length::zero();
|
let mut offset = Length::zero();
|
||||||
let mut frame = Frame::new(self.size);
|
let mut frame = Frame::new(self.size);
|
||||||
frame.baseline = Some(self.baseline);
|
frame.baseline = Some(self.baseline);
|
||||||
@ -729,23 +811,56 @@ impl<'a> ShapedText<'a> {
|
|||||||
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
||||||
let pos = Point::new(offset, self.baseline);
|
let pos = Point::new(offset, self.baseline);
|
||||||
|
|
||||||
let mut text = Text {
|
let size = self.styles.get(TextNode::SIZE).abs;
|
||||||
face_id,
|
let fill = self.styles.get(TextNode::FILL);
|
||||||
size: self.styles.get(TextNode::SIZE),
|
let glyphs = group
|
||||||
fill: self.styles.get(TextNode::FILL),
|
.iter()
|
||||||
glyphs: vec![],
|
.map(|glyph| Glyph {
|
||||||
};
|
|
||||||
|
|
||||||
for glyph in group {
|
|
||||||
text.glyphs.push(Glyph {
|
|
||||||
id: glyph.glyph_id,
|
id: glyph.glyph_id,
|
||||||
x_advance: glyph.x_advance,
|
x_advance: glyph.x_advance,
|
||||||
x_offset: glyph.x_offset,
|
x_offset: glyph.x_offset,
|
||||||
});
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let text = Text { face_id, size, fill, glyphs };
|
||||||
|
let width = text.width();
|
||||||
|
frame.push(pos, Element::Text(text));
|
||||||
|
|
||||||
|
// Apply line decorations.
|
||||||
|
for line in self.styles.get_ref(TextNode::LINES) {
|
||||||
|
let face = fonts.get(face_id);
|
||||||
|
let metrics = match line.kind {
|
||||||
|
LineKind::Underline => face.underline,
|
||||||
|
LineKind::Strikethrough => face.strikethrough,
|
||||||
|
LineKind::Overline => face.overline,
|
||||||
|
};
|
||||||
|
|
||||||
|
let extent = line.extent.resolve(size);
|
||||||
|
let offset = line
|
||||||
|
.offset
|
||||||
|
.map(|s| s.resolve(size))
|
||||||
|
.unwrap_or(-metrics.position.resolve(size));
|
||||||
|
|
||||||
|
let stroke = Stroke {
|
||||||
|
paint: line.stroke.unwrap_or(fill),
|
||||||
|
thickness: line
|
||||||
|
.thickness
|
||||||
|
.map(|s| s.resolve(size))
|
||||||
|
.unwrap_or(metrics.thickness.resolve(size)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let subpos = Point::new(pos.x - extent, pos.y + offset);
|
||||||
|
let target = Point::new(width + 2.0 * extent, Length::zero());
|
||||||
|
let shape = Shape::stroked(Geometry::Line(target), stroke);
|
||||||
|
frame.push(subpos, Element::Shape(shape));
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += text.width();
|
offset += width;
|
||||||
frame.push(pos, Element::Text(text));
|
}
|
||||||
|
|
||||||
|
// Apply link if it exists.
|
||||||
|
if let Some(url) = self.styles.get_ref(LinkNode::URL) {
|
||||||
|
frame.link(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
frame
|
frame
|
||||||
|
@ -71,7 +71,7 @@ fn main() {
|
|||||||
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
|
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
|
||||||
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
|
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
|
||||||
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
|
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
|
||||||
styles.set(TextNode::SIZE, Length::pt(10.0));
|
styles.set(TextNode::SIZE, Length::pt(10.0).into());
|
||||||
|
|
||||||
// Hook up an assert function into the global scope.
|
// Hook up an assert function into the global scope.
|
||||||
let mut std = typst::library::new();
|
let mut std = typst::library::new();
|
||||||
@ -92,7 +92,7 @@ fn main() {
|
|||||||
let mut ctx = Context::builder().std(std).styles(styles).build(loader);
|
let mut ctx = Context::builder().std(std).styles(styles).build(loader);
|
||||||
|
|
||||||
// Run all the tests.
|
// Run all the tests.
|
||||||
let mut ok = true;
|
let mut ok = 0;
|
||||||
for src_path in filtered {
|
for src_path in filtered {
|
||||||
let path = src_path.strip_prefix(TYP_DIR).unwrap();
|
let path = src_path.strip_prefix(TYP_DIR).unwrap();
|
||||||
let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
|
let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
|
||||||
@ -100,16 +100,20 @@ fn main() {
|
|||||||
let pdf_path =
|
let pdf_path =
|
||||||
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
|
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
|
||||||
|
|
||||||
ok &= test(
|
ok += test(
|
||||||
&mut ctx,
|
&mut ctx,
|
||||||
&src_path,
|
&src_path,
|
||||||
&png_path,
|
&png_path,
|
||||||
&ref_path,
|
&ref_path,
|
||||||
pdf_path.as_deref(),
|
pdf_path.as_deref(),
|
||||||
);
|
) as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if len > 1 {
|
||||||
|
println!("{} / {} tests passed.", ok, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok < len {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,6 +240,7 @@ fn test_part(
|
|||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
let (frames, mut errors) = match ctx.execute(id) {
|
let (frames, mut errors) = match ctx.execute(id) {
|
||||||
Ok(document) => {
|
Ok(document) => {
|
||||||
|
// dbg!(&document);
|
||||||
let mut frames = layout(ctx, &document);
|
let mut frames = layout(ctx, &document);
|
||||||
|
|
||||||
#[cfg(feature = "layout-cache")]
|
#[cfg(feature = "layout-cache")]
|
||||||
@ -321,7 +326,10 @@ fn test_incremental(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cached != frames {
|
if cached != frames {
|
||||||
println!(" Subtest {} relayout differs from clean pass ❌", i);
|
println!(
|
||||||
|
" Subtest {} relayout differs from clean pass on level {} ❌",
|
||||||
|
i, level
|
||||||
|
);
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,7 +514,7 @@ fn draw_text(
|
|||||||
let mut x = 0.0;
|
let mut x = 0.0;
|
||||||
for glyph in &text.glyphs {
|
for glyph in &text.glyphs {
|
||||||
let glyph_id = GlyphId(glyph.id);
|
let glyph_id = GlyphId(glyph.id);
|
||||||
let offset = x + glyph.x_offset.to_length(text.size).to_f32();
|
let offset = x + glyph.x_offset.resolve(text.size).to_f32();
|
||||||
let ts = ts.pre_translate(offset, 0.0);
|
let ts = ts.pre_translate(offset, 0.0);
|
||||||
|
|
||||||
if let Some(tree) = ttf
|
if let Some(tree) = ttf
|
||||||
@ -559,7 +567,7 @@ fn draw_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
x += glyph.x_advance.to_length(text.size).to_f32();
|
x += glyph.x_advance.resolve(text.size).to_f32();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user