diff --git a/src/eval/class.rs b/src/eval/class.rs index 15e1f2499..5682cb4d3 100644 --- a/src/eval/class.rs +++ b/src/eval/class.rs @@ -84,7 +84,7 @@ impl Class { impl Debug for Class { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("') } } diff --git a/src/eval/styles.rs b/src/eval/styles.rs index f8413feb1..d7c17b92f 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -70,8 +70,8 @@ impl StyleMap { /// `outer`. The ones from `self` take precedence over the ones from /// `outer`. For folded properties `self` contributes the inner value. pub fn chain<'a>(&'a self, outer: &'a StyleChain<'a>) -> StyleChain<'a> { + // No need to chain an empty map. if self.is_empty() { - // No need to chain an empty map. *outer } else { StyleChain { inner: self, outer: Some(outer) } @@ -86,12 +86,12 @@ impl StyleMap { /// style maps, whereas `chain` would be used during layouting to combine /// immutable style maps from different levels of the hierarchy. pub fn apply(&mut self, outer: &Self) { - 'outer: for outer in &outer.0 { - for inner in &mut self.0 { - if inner.style_id() == outer.style_id() { - inner.fold(outer); - continue 'outer; - } + for outer in &outer.0 { + if let Some(inner) = + self.0.iter_mut().find(|inner| inner.style_id() == outer.style_id()) + { + *inner = inner.fold(outer); + continue; } self.0.push(outer.clone()); @@ -158,16 +158,16 @@ impl<'a> StyleChain<'a> { /// Get the (folded) value of a copyable style property. /// + /// This is the method you should reach for first. If it doesn't work + /// because your property is not copyable, use `get_ref`. If that doesn't + /// work either because your property needs folding, use `get_cloned`. + /// /// Returns the property's default value if no map in the chain contains an /// entry for it. - pub fn get

(self, key: P) -> P::Value + pub fn get(self, key: P) -> P::Value where - P: Property, P::Value: Copy, { - // This exists separately to `get_cloned` for `Copy` types so that - // people don't just naively use `get` / `get_cloned` where they should - // use `get_ref`. self.get_cloned(key) } @@ -177,13 +177,13 @@ impl<'a> StyleChain<'a> { /// Prefer `get` if possible or resort to `get_cloned` for non-`Copy` /// properties that need folding. /// - /// Returns a reference to the property's default value if no map in the - /// chain contains an entry for it. - pub fn get_ref

(self, key: P) -> &'a P::Value + /// Returns a lazily-initialized reference to the property's default value + /// if no map in the chain contains an entry for it. + pub fn get_ref(self, key: P) -> &'a P::Value where - P: Property + Nonfolding, + P: Nonfolding, { - if let Some(value) = self.get_locally(key) { + if let Some(value) = self.find(key) { value } else if let Some(outer) = self.outer { outer.get_ref(key) @@ -197,11 +197,11 @@ impl<'a> StyleChain<'a> { /// While this works for all properties, you should prefer `get` or /// `get_ref` where possible. This is only needed for non-`Copy` properties /// that need folding. - pub fn get_cloned

(self, key: P) -> P::Value - where - P: Property, - { - if let Some(value) = self.get_locally(key).cloned() { + /// + /// Returns the property's default value if no map in the chain contains an + /// entry for it. + pub fn get_cloned(self, key: P) -> P::Value { + if let Some(value) = self.find(key).cloned() { if P::FOLDABLE { if let Some(outer) = self.outer { P::fold(value, outer.get_cloned(key)) @@ -218,8 +218,8 @@ impl<'a> StyleChain<'a> { } } - /// Find a property directly in the most local map. - fn get_locally(&self, _: P) -> Option<&'a P::Value> { + /// Find a property directly in the localmost map. + fn find(self, _: P) -> Option<&'a P::Value> { self.inner .0 .iter() @@ -309,8 +309,8 @@ impl Entry { self.0.as_any().downcast_ref() } - fn fold(&mut self, outer: &Self) { - *self = self.0.fold(outer); + fn fold(&self, outer: &Self) -> Self { + self.0.fold(outer) } } diff --git a/src/geom/spec.rs b/src/geom/spec.rs index 40d7386bb..cf75f42d9 100644 --- a/src/geom/spec.rs +++ b/src/geom/spec.rs @@ -85,10 +85,19 @@ impl Spec { } } -impl Spec -where - T: Ord, -{ +impl Spec { + /// Create a new instance with y set to its default value. + pub fn with_x(x: T) -> Self { + Self { x, y: T::default() } + } + + /// Create a new instance with x set to its default value. + pub fn with_y(y: T) -> Self { + Self { x: T::default(), y } + } +} + +impl Spec { /// The component-wise minimum of this and another instance. pub fn min(self, other: Self) -> Self { Self { diff --git a/src/library/align.rs b/src/library/align.rs index 327352445..e8dfabb1b 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -7,33 +7,7 @@ use super::ParNode; pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult { let aligns: Spec<_> = args.find().unwrap_or_default(); let body: PackedNode = args.expect("body")?; - - let mut styles = StyleMap::new(); - if let Some(align) = aligns.x { - styles.set(ParNode::ALIGN, align); - } - - Ok(Value::block(body.styled(styles).aligned(aligns))) -} - -dynamic! { - Align: "alignment", -} - -dynamic! { - Spec: "2d alignment", -} - -castable! { - Spec>, - Expected: "1d or 2d alignment", - @align: Align => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec => aligns.map(Some), - + Ok(Value::block(body.aligned(aligns))) } /// A node that aligns its child. @@ -56,8 +30,14 @@ impl Layout for AlignNode { let mut pod = regions.clone(); pod.expand &= self.aligns.map_is_none(); + // Align paragraphs inside the child. + let mut passed = StyleMap::new(); + if let Some(align) = self.aligns.x { + passed.set(ParNode::ALIGN, align); + } + // Layout the child. - let mut frames = self.child.layout(ctx, &pod, styles); + let mut frames = self.child.layout(ctx, &pod, passed.chain(&styles)); for ((current, base), Constrained { item: frame, cts }) in regions.iter().zip(&mut frames) @@ -78,3 +58,23 @@ impl Layout for AlignNode { frames } } + +dynamic! { + Align: "alignment", +} + +dynamic! { + Spec: "2d alignment", +} + +castable! { + Spec>, + Expected: "1d or 2d alignment", + @align: Align => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec => aligns.map(Some), + +} diff --git a/src/library/columns.rs b/src/library/columns.rs index 17ae6058f..ce02b5084 100644 --- a/src/library/columns.rs +++ b/src/library/columns.rs @@ -98,7 +98,7 @@ impl Layout for ColumnsNode { // case, the frame is first created with zero height and then // resized. let height = if regions.expand.y { current.y } else { Length::zero() }; - let mut output = Frame::new(Spec::new(regions.current.x, height)); + let mut output = Frame::new(Size::new(regions.current.x, height)); let mut cursor = Length::zero(); for _ in 0 .. columns { diff --git a/src/library/heading.rs b/src/library/heading.rs index 111472f44..3591ea0c0 100644 --- a/src/library/heading.rs +++ b/src/library/heading.rs @@ -6,18 +6,18 @@ use super::{FontFamily, TextNode}; /// A section heading. #[derive(Debug, Hash)] pub struct HeadingNode { - /// The node that produces the heading's contents. - pub child: PackedNode, /// The logical nesting depth of the section, starting from one. In the /// default style, this controls the text size of the heading. pub level: usize, + /// The node that produces the heading's contents. + pub child: PackedNode, } #[properties] impl HeadingNode { /// The heading's font family. pub const FAMILY: Smart = Smart::Auto; - /// The fill color of heading in the text. Just the surrounding text color + /// The fill color of text in the heading. Just the surrounding text color /// if `auto`. pub const FILL: Smart = Smart::Auto; } @@ -48,12 +48,12 @@ impl Layout for HeadingNode { ) -> Vec>> { let upscale = (1.6 - 0.1 * self.level as f64).max(0.75); - let mut local = StyleMap::new(); - local.set(TextNode::STRONG, true); - local.set(TextNode::SIZE, Relative::new(upscale).into()); + let mut passed = StyleMap::new(); + passed.set(TextNode::STRONG, true); + passed.set(TextNode::SIZE, Relative::new(upscale).into()); if let Smart::Custom(family) = styles.get_ref(Self::FAMILY) { - local.set( + passed.set( TextNode::FAMILY_LIST, std::iter::once(family) .chain(styles.get_ref(TextNode::FAMILY_LIST)) @@ -63,9 +63,9 @@ impl Layout for HeadingNode { } if let Smart::Custom(fill) = styles.get(Self::FILL) { - local.set(TextNode::FILL, fill); + passed.set(TextNode::FILL, fill); } - self.child.layout(ctx, regions, local.chain(&styles)) + self.child.layout(ctx, regions, passed.chain(&styles)) } } diff --git a/src/library/image.rs b/src/library/image.rs index 2b91b2fca..c5cb9aebf 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -3,18 +3,13 @@ use std::io; use super::prelude::*; -use super::LinkNode; use crate::diag::Error; use crate::image::ImageId; /// `image`: An image. pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { - let path = args.expect::>("path to image file")?; - let width = args.named("width")?; - let height = args.named("height")?; - let fit = args.named("fit")?.unwrap_or_default(); - // Load the image. + let path = args.expect::>("path to image file")?; let full = ctx.make_path(&path.v); let id = ctx.images.load(&full).map_err(|err| { Error::boxed(path.span, match err.kind() { @@ -23,6 +18,10 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult { }) })?; + let width = args.named("width")?; + let height = args.named("height")?; + let fit = args.named("fit")?.unwrap_or_default(); + Ok(Value::inline( ImageNode { id, fit }.pack().sized(Spec::new(width, height)), )) @@ -37,6 +36,12 @@ pub struct ImageNode { pub fit: ImageFit, } +#[properties] +impl ImageNode { + /// An URL the image should link to. + pub const LINK: Option = None; +} + impl Layout for ImageNode { fn layout( &self, @@ -90,7 +95,7 @@ impl Layout for ImageNode { } // Apply link if it exists. - if let Some(url) = styles.get_ref(LinkNode::URL) { + if let Some(url) = styles.get_ref(Self::LINK) { frame.link(url); } diff --git a/src/library/link.rs b/src/library/link.rs index d1d7842df..1050963c6 100644 --- a/src/library/link.rs +++ b/src/library/link.rs @@ -1,9 +1,10 @@ //! Hyperlinking. use super::prelude::*; +use super::{ImageNode, ShapeNode, TextNode}; use crate::util::EcoString; -/// `link`: Link text or other elements. +/// `link`: Link text and other elements to an URL. pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult { let url: String = args.expect::("url")?.into(); let body = args.find().unwrap_or_else(|| { @@ -14,17 +15,9 @@ pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult { Node::Text(text.into()) }); - Ok(Value::Node( - body.styled(StyleMap::with(LinkNode::URL, Some(url))), - )) -} - -/// Host for link styles. -#[derive(Debug, Hash)] -pub struct LinkNode; - -#[properties] -impl LinkNode { - /// An URL to link to. - pub const URL: Option = None; + let mut passed = StyleMap::new(); + passed.set(TextNode::LINK, Some(url.clone())); + passed.set(ImageNode::LINK, Some(url.clone())); + passed.set(ShapeNode::LINK, Some(url)); + Ok(Value::Node(body.styled(passed))) } diff --git a/src/library/list.rs b/src/library/list.rs index a9dfbec50..bbdc400ae 100644 --- a/src/library/list.rs +++ b/src/library/list.rs @@ -50,26 +50,23 @@ impl Layout for ListNode { let label_indent = styles.get(Self::LABEL_INDENT).resolve(em); let body_indent = styles.get(Self::BODY_INDENT).resolve(em); - let columns = vec![ - TrackSizing::Linear(label_indent.into()), - TrackSizing::Auto, - TrackSizing::Linear(body_indent.into()), - TrackSizing::Auto, - ]; - - let children = vec![ - PackedNode::default(), - Node::Text(self.labelling.label()).into_block(), - PackedNode::default(), - self.child.clone(), - ]; - - GridNode { - tracks: Spec::new(columns, vec![]), + let grid = GridNode { + tracks: Spec::with_x(vec![ + TrackSizing::Linear(label_indent.into()), + TrackSizing::Auto, + TrackSizing::Linear(body_indent.into()), + TrackSizing::Auto, + ]), gutter: Spec::default(), - children, - } - .layout(ctx, regions, styles) + children: vec![ + PackedNode::default(), + Node::Text(self.labelling.label()).into_block(), + PackedNode::default(), + self.child.clone(), + ], + }; + + grid.layout(ctx, regions, styles) } } diff --git a/src/library/page.rs b/src/library/page.rs index 69be2bb7b..fa9345f52 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -166,31 +166,6 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult { Ok(Value::Node(Node::Pagebreak)) } -/// Defines default margins for a class of related papers. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PaperClass { - Custom, - Base, - US, - Newspaper, - Book, -} - -impl PaperClass { - /// The default margins for this page class. - fn default_margins(self) -> Sides { - let f = |r| Relative::new(r).into(); - let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b)); - match self { - Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842), - Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842), - Self::US => s(0.1760, 0.1092, 0.1760, 0.0910), - Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294), - Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965), - } - } -} - /// Specification of a paper. #[derive(Debug, Copy, Clone)] pub struct Paper { @@ -413,6 +388,31 @@ castable! { Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?, } +/// Defines default margins for a class of related papers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum PaperClass { + Custom, + Base, + US, + Newspaper, + Book, +} + +impl PaperClass { + /// The default margins for this page class. + fn default_margins(self) -> Sides { + let f = |r| Relative::new(r).into(); + let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b)); + match self { + Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::US => s(0.1760, 0.1092, 0.1760, 0.0910), + Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294), + Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965), + } + } +} + /// The error when parsing a [`Paper`] from a string fails. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct PaperError; diff --git a/src/library/placed.rs b/src/library/placed.rs index 8a4f46af8..e7b173251 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -5,7 +5,7 @@ use super::AlignNode; /// `place`: Place content at an absolute position. pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult { - let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None)); + let aligns = args.find().unwrap_or(Spec::with_x(Some(Align::Left))); let tx = args.named("dx")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default(); let body: PackedNode = args.expect("body")?; diff --git a/src/library/shape.rs b/src/library/shape.rs index 76c03edbd..c47885d20 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -3,7 +3,6 @@ use std::f64::consts::SQRT_2; use super::prelude::*; -use super::LinkNode; /// `rect`: A rectangle with optional content. pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult { @@ -101,6 +100,12 @@ pub struct ShapeNode { pub child: Option, } +#[properties] +impl ShapeNode { + /// An URL the shape should link to. + pub const LINK: Option = None; +} + impl Layout for ShapeNode { fn layout( &self, @@ -170,7 +175,7 @@ impl Layout for ShapeNode { } // Apply link if it exists. - if let Some(url) = styles.get_ref(LinkNode::URL) { + if let Some(url) = styles.get_ref(Self::LINK) { frame.link(url); } diff --git a/src/library/text.rs b/src/library/text.rs index 5fa19cb99..486cb77f7 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -9,7 +9,6 @@ use rustybuzz::{Feature, UnicodeBuffer}; use ttf_parser::Tag; use super::prelude::*; -use super::LinkNode; use crate::font::{ Face, FaceId, FontStore, FontStretch, FontStyle, FontVariant, FontWeight, VerticalFontMetric, @@ -59,6 +58,8 @@ impl TextNode { /// Decorative lines. #[fold(|a, b| a.into_iter().chain(b).collect())] pub const LINES: Vec = vec![]; + /// An URL the text should link to. + pub const LINK: Option = None; /// The size of the glyphs. #[fold(Linear::compose)] @@ -889,7 +890,7 @@ impl<'a> ShapedText<'a> { } // Apply link if it exists. - if let Some(url) = self.styles.get_ref(LinkNode::URL) { + if let Some(url) = self.styles.get_ref(TextNode::LINK) { frame.link(url); } diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png index 20b04a8e4..510c7e98f 100644 Binary files a/tests/ref/text/links.png and b/tests/ref/text/links.png differ