diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 7478c8cb8..09739b05b 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,8 +1,8 @@ extern crate proc_macro; use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use proc_macro2::{TokenStream as TokenStream2, TokenTree}; +use quote::{quote, quote_spanned}; use syn::parse_quote; use syn::punctuated::Punctuated; use syn::spanned::Spanned; @@ -54,7 +54,7 @@ fn expand(stream: TokenStream2, mut impl_block: syn::ItemImpl) -> Result Result, ) -> Result<(Property, syn::ItemMod)> { + let property = parse_property(item)?; + // The display name, e.g. `TextNode::STRONG`. let name = format!("{}::{}", self_name, &item.ident); // The type of the property's value is what the user of our macro wrote // as type of the const ... let value_ty = &item.ty; + let output_ty = if property.fold { + parse_quote!(<#value_ty as eval::Fold>::Output) + } else if property.referenced { + parse_quote!(&'a #value_ty) + } else { + value_ty.clone() + }; // ... but the real type of the const becomes this.. let key = quote! { Key<#value_ty, #self_args> }; @@ -172,50 +173,41 @@ fn process_const( _ => true, }); - // The default value of the property is what the user wrote as - // initialization value of the const. let default = &item.expr; - let mut fold = None; - let mut property = Property { - name: item.ident.clone(), - shorthand: false, - variadic: false, - skip: false, - }; + // Ensure that the type is either `Copy` or that the property is referenced + // or that the property isn't copy but can't be referenced because it needs + // folding. + let get; + let mut copy = None; - for attr in std::mem::take(&mut item.attrs) { - match attr.path.get_ident().map(ToString::to_string).as_deref() { - Some("fold") => { - // Look for a folding function like `#[fold(u64::add)]`. - let func: syn::Expr = attr.parse_args()?; - fold = Some(quote! { - const FOLDING: bool = true; - - fn fold(inner: Self::Value, outer: Self::Value) -> Self::Value { - let f: fn(Self::Value, Self::Value) -> Self::Value = #func; - f(inner, outer) - } - }); + if property.referenced { + get = quote! { + values.next().unwrap_or_else(|| { + static LAZY: Lazy<#value_ty> = Lazy::new(|| #default); + &*LAZY + }) + }; + } else if property.fold { + get = quote! { + match values.next().cloned() { + Some(inner) => eval::Fold::fold(inner, Self::get(values)), + None => #default, } - Some("shorthand") => property.shorthand = true, - Some("variadic") => property.variadic = true, - Some("skip") => property.skip = true, - _ => item.attrs.push(attr), - } - } + }; + } else { + get = quote! { + values.next().copied().unwrap_or(#default) + }; - if property.shorthand && property.variadic { - return Err(Error::new( - property.name.span(), - "shorthand and variadic are mutually exclusive", - )); + copy = Some(quote_spanned! { item.ty.span() => + const _: fn() -> () = || { + fn type_must_be_copy_or_fold_or_referenced() {} + type_must_be_copy_or_fold_or_referenced::<#value_ty>(); + }; + }); } - let referencable = fold.is_none().then(|| { - quote! { impl<#params> eval::Referencable for #key {} } - }); - // Generate the module code. let module_name = &item.ident; let module = parse_quote! { @@ -232,28 +224,21 @@ fn process_const( } } - impl<#params> eval::Key for #key { + impl<'a, #params> eval::Key<'a> for #key { type Value = #value_ty; - + type Output = #output_ty; const NAME: &'static str = #name; - fn node_id() -> TypeId { + fn node() -> TypeId { TypeId::of::<#self_ty>() } - fn default() -> Self::Value { - #default + fn get(mut values: impl Iterator) -> Self::Output { + #get } - - fn default_ref() -> &'static Self::Value { - static LAZY: Lazy<#value_ty> = Lazy::new(|| #default); - &*LAZY - } - - #fold } - #referencable + #copy } }; @@ -263,3 +248,64 @@ fn process_const( Ok((property, module)) } + +/// A style property. +struct Property { + name: Ident, + hidden: bool, + referenced: bool, + shorthand: bool, + variadic: bool, + fold: bool, +} + +/// Parse a style property attribute. +fn parse_property(item: &mut syn::ImplItemConst) -> Result { + let mut property = Property { + name: item.ident.clone(), + hidden: false, + referenced: false, + shorthand: false, + variadic: false, + fold: false, + }; + + if let Some(idx) = item + .attrs + .iter() + .position(|attr| attr.path.get_ident().map_or(false, |name| name == "property")) + { + let attr = item.attrs.remove(idx); + for token in attr.parse_args::()? { + match token { + TokenTree::Ident(ident) => match ident.to_string().as_str() { + "hidden" => property.hidden = true, + "shorthand" => property.shorthand = true, + "referenced" => property.referenced = true, + "variadic" => property.variadic = true, + "fold" => property.fold = true, + _ => return Err(Error::new(ident.span(), "invalid attribute")), + }, + TokenTree::Punct(_) => {} + _ => return Err(Error::new(token.span(), "invalid token")), + } + } + } + + let span = property.name.span(); + if property.shorthand && property.variadic { + return Err(Error::new( + span, + "shorthand and variadic are mutually exclusive", + )); + } + + if property.referenced && property.fold { + return Err(Error::new( + span, + "referenced and fold are mutually exclusive", + )); + } + + Ok(property) +} diff --git a/src/eval/content.rs b/src/eval/content.rs index 1cdd4bb03..01b434737 100644 --- a/src/eval/content.rs +++ b/src/eval/content.rs @@ -102,17 +102,15 @@ impl Content { } /// Style this content with a single style property. - pub fn styled(mut self, key: P, value: P::Value) -> Self { + pub fn styled<'k, K: Key<'k>>(mut self, key: K, value: K::Value) -> Self { if let Self::Styled(styled) = &mut self { if let Some((_, map)) = Arc::get_mut(styled) { - if !map.has_scoped() { - map.set(key, value); - return self; - } + map.apply(key, value); + return self; } } - self.styled_with_map(StyleMap::with(key, value)) + Self::Styled(Arc::new((self, StyleMap::with(key, value)))) } /// Style this content with a full style map. @@ -123,10 +121,8 @@ impl Content { if let Self::Styled(styled) = &mut self { if let Some((_, map)) = Arc::get_mut(styled) { - if !styles.has_scoped() && !map.has_scoped() { - map.apply(&styles); - return self; - } + map.apply_map(&styles); + return self; } } @@ -161,7 +157,7 @@ impl Content { let tpa = Arena::new(); let styles = ctx.styles.clone(); - let styles = StyleChain::new(&styles); + let styles = StyleChain::with_root(&styles); let mut builder = Builder::new(&sya, &tpa, true); builder.process(ctx, self, styles)?; diff --git a/src/eval/func.rs b/src/eval/func.rs index 3eae453e0..fcd19326f 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -1,4 +1,3 @@ -use std::any::TypeId; use std::fmt::{self, Debug, Formatter, Write}; use std::hash::{Hash, Hasher}; use std::sync::Arc; @@ -52,7 +51,7 @@ impl Func { show: if T::SHOWABLE { Some(|recipe, span| { let mut styles = StyleMap::new(); - styles.set_recipe(TypeId::of::(), recipe, span); + styles.set_recipe::(recipe, span); styles }) } else { diff --git a/src/eval/layout.rs b/src/eval/layout.rs index 94375c61e..aecc7ef96 100644 --- a/src/eval/layout.rs +++ b/src/eval/layout.rs @@ -1,12 +1,12 @@ //! Layouting infrastructure. -use std::any::{Any, TypeId}; +use std::any::Any; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::sync::Arc; +use super::{Barrier, StyleChain}; use crate::diag::TypResult; -use crate::eval::StyleChain; use crate::frame::{Element, Frame, Geometry, Shape, Stroke}; use crate::geom::{Align, Length, Linear, Paint, Point, Sides, Size, Spec, Transform}; use crate::library::graphics::MoveNode; @@ -18,7 +18,7 @@ use crate::Context; /// /// Layout return one frame per used region alongside constraints that define /// whether the result is reusable in other regions. -pub trait Layout { +pub trait Layout: 'static { /// Layout this node into the given regions, producing constrained frames. fn layout( &self, @@ -144,12 +144,12 @@ impl LayoutNode { /// Check whether the contained node is a specific layout node. pub fn is(&self) -> bool { - self.0.as_any().is::() + (**self.0).as_any().is::() } - /// The type id of this node. - pub fn id(&self) -> TypeId { - self.0.as_any().type_id() + /// A barrier for the node. + pub fn barrier(&self) -> Barrier { + (**self.0).barrier() } /// Try to downcast to a specific layout node. @@ -157,7 +157,7 @@ impl LayoutNode { where T: Layout + Debug + Hash + 'static, { - self.0.as_any().downcast_ref() + (**self.0).as_any().downcast_ref() } /// Force a size for this node. @@ -223,7 +223,7 @@ impl Layout for LayoutNode { styles: StyleChain, ) -> TypResult>> { ctx.query((self, regions, styles), |ctx, (node, regions, styles)| { - node.0.layout(ctx, regions, styles.barred(node.id())) + node.0.layout(ctx, regions, node.barrier().chain(&styles)) }) .clone() } @@ -253,6 +253,7 @@ impl PartialEq for LayoutNode { trait Bounds: Layout + Debug + Sync + Send + 'static { fn as_any(&self) -> &dyn Any; + fn barrier(&self) -> Barrier; } impl Bounds for T @@ -262,6 +263,10 @@ where fn as_any(&self) -> &dyn Any { self } + + fn barrier(&self) -> Barrier { + Barrier::new::() + } } /// A layout node that produces an empty frame. diff --git a/src/eval/show.rs b/src/eval/show.rs index e85903d23..383497ba0 100644 --- a/src/eval/show.rs +++ b/src/eval/show.rs @@ -9,7 +9,7 @@ use crate::util::Prehashed; use crate::Context; /// A node that can be realized given some styles. -pub trait Show { +pub trait Show: 'static { /// Realize this node in the given styles. fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult; diff --git a/src/eval/styles.rs b/src/eval/styles.rs index a0dc263c3..8bd21612a 100644 --- a/src/eval/styles.rs +++ b/src/eval/styles.rs @@ -1,48 +1,49 @@ use std::any::{Any, TypeId}; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; +use std::marker::PhantomData; use std::sync::Arc; -use super::{Args, Content, Func, Span, Value}; +use super::{Args, Content, Func, Layout, Node, Span, Value}; use crate::diag::{At, TypResult}; use crate::library::layout::PageNode; use crate::library::text::{FontFamily, ParNode, TextNode}; +use crate::util::Prehashed; use crate::Context; /// A map of style properties. #[derive(Default, Clone, PartialEq, Hash)] -pub struct StyleMap { - /// Settable properties. - props: Vec, - /// Show rule recipes. - recipes: Vec, -} +pub struct StyleMap(Vec); impl StyleMap { /// Create a new, empty style map. pub fn new() -> Self { - Self { props: vec![], recipes: vec![] } + Self::default() } /// Whether this map contains no styles. pub fn is_empty(&self) -> bool { - self.props.is_empty() && self.recipes.is_empty() + self.0.is_empty() } /// Create a style map from a single property-value pair. - pub fn with(key: K, value: K::Value) -> Self { + pub fn with<'a, K: Key<'a>>(key: K, value: K::Value) -> Self { let mut styles = Self::new(); styles.set(key, value); styles } - /// Set the value for a style property. - pub fn set(&mut self, key: K, value: K::Value) { - self.props.push(Entry::new(key, value)); + /// Set an inner value for a style property. + /// + /// If the property needs folding and the value is already contained in the + /// style map, `self` contributes the outer values and `value` is the inner + /// one. + pub fn set<'a, K: Key<'a>>(&mut self, key: K, value: K::Value) { + self.0.push(Entry::Property(Property::new(key, value))); } - /// Set a value for a style property if it is `Some(_)`. - pub fn set_opt(&mut self, key: K, value: Option) { + /// Set an inner value for a style property if it is `Some(_)`. + pub fn set_opt<'a, K: Key<'a>>(&mut self, key: K, value: Option) { if let Some(value) = value { self.set(key, value); } @@ -50,81 +51,327 @@ impl StyleMap { /// Set a font family composed of a preferred family and existing families /// from a style chain. - pub fn set_family(&mut self, family: FontFamily, existing: StyleChain) { + pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { self.set( TextNode::FAMILY, - std::iter::once(family) - .chain(existing.get_ref(TextNode::FAMILY).iter().cloned()) + std::iter::once(preferred) + .chain(existing.get(TextNode::FAMILY).iter().cloned()) .collect(), ); } - /// Set a recipe. - pub fn set_recipe(&mut self, node: TypeId, func: Func, span: Span) { - self.recipes.push(Recipe { node, func, span }); + /// Set a show rule recipe for a node. + pub fn set_recipe(&mut self, func: Func, span: Span) { + self.0.push(Entry::Recipe(Recipe::new::(func, span))); + } + + /// Make `self` the first link of the `tail` chain. + /// + /// The resulting style chain contains styles from `self` as well as + /// `tail`. The ones from `self` take precedence over the ones from + /// `tail`. For folded properties `self` contributes the inner value. + pub fn chain<'a>(&'a self, tail: &'a StyleChain<'a>) -> StyleChain<'a> { + if self.is_empty() { + *tail + } else { + StyleChain { head: &self.0, tail: Some(tail) } + } + } + + /// Set an outer value for a style property. + /// + /// If the property needs folding and the value is already contained in the + /// style map, `self` contributes the inner values and `value` is the outer + /// one. + /// + /// Like [`chain`](Self::chain) or [`apply_map`](Self::apply_map), but with + /// only a single property. + pub fn apply<'a, K: Key<'a>>(&mut self, key: K, value: K::Value) { + self.0.insert(0, Entry::Property(Property::new(key, value))); + } + + /// Apply styles from `tail` in-place. The resulting style map is equivalent + /// to the style chain created by `self.chain(StyleChain::new(tail))`. + /// + /// This is useful over `chain` when you want to combine two maps, but you + /// still need an owned map without a lifetime. + pub fn apply_map(&mut self, tail: &Self) { + self.0.splice(0 .. 0, tail.0.iter().cloned()); } /// Mark all contained properties as _scoped_. This means that they only /// apply to the first descendant node (of their type) in the hierarchy and - /// not its children, too. This is used by class constructors. + /// not its children, too. This is used by [constructors](Node::construct). pub fn scoped(mut self) -> Self { - for entry in &mut self.props { - entry.scoped = true; + for entry in &mut self.0 { + if let Entry::Property(property) = entry { + property.scoped = true; + } } self } - /// Whether this map contains scoped styles. - pub fn has_scoped(&self) -> bool { - self.props.iter().any(|e| e.scoped) - } - - /// Make `self` the first link of the style chain `outer`. - /// - /// The resulting style chain contains styles from `self` as well as - /// `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> { - if self.is_empty() { - *outer - } else { - StyleChain { - link: Some(Link::Map(self)), - outer: Some(outer), - } - } - } - - /// Apply styles from `outer` in-place. The resulting style map is - /// equivalent to the style chain created by - /// `self.chain(StyleChain::new(outer))`. - /// - /// This is useful over `chain` when you need an owned map without a - /// lifetime, for example, because you want to store the style map inside a - /// packed node. - pub fn apply(&mut self, outer: &Self) { - self.props.splice(0 .. 0, outer.props.iter().cloned()); - self.recipes.splice(0 .. 0, outer.recipes.iter().cloned()); - } - - /// The highest-level interruption of the map. + /// The highest-level kind of of structure the map interrupts. pub fn interruption(&self) -> Option { - self.props.iter().filter_map(|entry| entry.interruption()).max() + self.0 + .iter() + .filter_map(|entry| entry.property()) + .filter_map(|property| property.interruption()) + .max() } } impl Debug for StyleMap { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - for entry in self.props.iter().rev() { - writeln!(f, "{:#?}", entry)?; - } - for recipe in self.recipes.iter().rev() { - writeln!(f, "#[Recipe for {:?} from {:?}]", recipe.node, recipe.span)?; + for entry in self.0.iter().rev() { + writeln!(f, "{:?}", entry)?; } Ok(()) } } +/// An entry for a single style property, recipe or barrier. +#[derive(Clone, PartialEq, Hash)] +enum Entry { + /// A style property originating from a set rule or constructor. + Property(Property), + /// A barrier for scoped styles. + Barrier(TypeId, &'static str), + /// A show rule recipe. + Recipe(Recipe), +} + +impl Entry { + /// If this is a property, return it. + fn property(&self) -> Option<&Property> { + match self { + Self::Property(property) => Some(property), + _ => None, + } + } + + /// If this is a recipe, return it. + fn recipe(&self) -> Option<&Recipe> { + match self { + Self::Recipe(recipe) => Some(recipe), + _ => None, + } + } +} + +impl Debug for Entry { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("#[")?; + match self { + Self::Property(property) => property.fmt(f)?, + Self::Recipe(recipe) => recipe.fmt(f)?, + Self::Barrier(_, name) => write!(f, "Barrier for {name}")?, + } + f.write_str("]") + } +} + +/// A style property originating from a set rule or constructor. +#[derive(Clone, Hash)] +struct Property { + /// The type id of the property's [key](Key). + key: TypeId, + /// The type id of the node the property belongs to. + node: TypeId, + /// The name of the property. + name: &'static str, + /// The property's value. + value: Arc>, + /// Whether the property should only affects the first node down the + /// hierarchy. Used by constructors. + scoped: bool, +} + +impl Property { + /// Create a new property from a key-value pair. + fn new<'a, K: Key<'a>>(_: K, value: K::Value) -> Self { + Self { + key: TypeId::of::(), + node: K::node(), + name: K::NAME, + value: Arc::new(Prehashed::new(value)), + scoped: false, + } + } + + /// What kind of structure the property interrupts. + fn interruption(&self) -> Option { + if self.is_of::() { + Some(Interruption::Page) + } else if self.is_of::() { + Some(Interruption::Par) + } else { + None + } + } + + /// Access the property's value if it is of the given key. + fn downcast<'a, K: Key<'a>>(&'a self) -> Option<&'a K::Value> { + if self.key == TypeId::of::() { + (**self.value).as_any().downcast_ref() + } else { + None + } + } + + /// Whether this property belongs to the node `T`. + fn is_of(&self) -> bool { + self.node == TypeId::of::() + } +} + +impl Debug for Property { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{} = {:?}", self.name, self.value)?; + if self.scoped { + write!(f, " [scoped]")?; + } + Ok(()) + } +} + +impl PartialEq for Property { + fn eq(&self, other: &Self) -> bool { + self.key == other.key + && self.value.eq(&other.value) + && self.scoped == other.scoped + } +} + +trait Bounds: Debug + Sync + Send + 'static { + fn as_any(&self) -> &dyn Any; +} + +impl Bounds for T +where + T: Debug + Sync + Send + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Style property keys. +/// +/// This trait is not intended to be implemented manually, but rather through +/// the `#[node]` proc-macro. +pub trait Key<'a>: 'static { + /// The unfolded type which this property is stored as in a style map. For + /// example, this is [`Toggle`](crate::geom::Length) for the + /// [`STRONG`](TextNode::STRONG) property. + type Value: Debug + Clone + Hash + Sync + Send + 'static; + + /// The folded type of value that is returned when reading this property + /// from a style chain. For example, this is [`bool`] for the + /// [`STRONG`](TextNode::STRONG) property. For non-copy, non-folding + /// properties this is a reference type. + type Output: 'a; + + /// The name of the property, used for debug printing. + const NAME: &'static str; + + /// The type id of the node this property belongs to. + fn node() -> TypeId; + + /// Compute an output value from a sequence of values belong to this key, + /// folding if necessary. + fn get(values: impl Iterator) -> Self::Output; +} + +/// A property that is folded to determine its final value. +pub trait Fold { + /// The type of the folded output. + type Output; + + /// Fold this inner value with an outer folded value. + fn fold(self, outer: Self::Output) -> Self::Output; +} + +/// A show rule recipe. +#[derive(Clone, PartialEq, Hash)] +struct Recipe { + /// The affected node. + node: TypeId, + /// The name of the affected node. + name: &'static str, + /// The function that defines the recipe. + func: Func, + /// The span to report all erros with. + span: Span, +} + +impl Recipe { + /// Create a new recipe for the node `T`. + fn new(func: Func, span: Span) -> Self { + Self { + node: TypeId::of::(), + name: std::any::type_name::(), + func, + span, + } + } +} + +impl Debug for Recipe { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Recipe for {} from {:?}", self.name, self.span) + } +} + +/// A style chain barrier. +/// +/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style +/// can still be read through a single barrier (the one of the node it +/// _should_ apply to), but a second barrier will make it invisible. +#[derive(Clone, PartialEq, Hash)] +pub struct Barrier(Entry); + +impl Barrier { + /// Create a new barrier for the layout node `T`. + pub fn new() -> Self { + Self(Entry::Barrier( + TypeId::of::(), + std::any::type_name::(), + )) + } + + /// Make this barrier the first link of the `tail` chain. + pub fn chain<'a>(&'a self, tail: &'a StyleChain) -> StyleChain<'a> { + // We have to store a full `Entry` enum inside the barrier because + // otherwise the `slice::from_ref` trick below won't work. + // Unfortunately, that also means we have to somehow extract the id + // here. + let id = match self.0 { + Entry::Barrier(id, _) => id, + _ => unreachable!(), + }; + + if tail + .entries() + .filter_map(Entry::property) + .any(|p| p.scoped && p.node == id) + { + StyleChain { + head: std::slice::from_ref(&self.0), + tail: Some(tail), + } + } else { + *tail + } + } +} + +impl Debug for Barrier { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + /// Determines whether a style could interrupt some composable structure. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum Interruption { @@ -134,406 +381,198 @@ pub enum Interruption { Page, } -/// Style property keys. -/// -/// This trait is not intended to be implemented manually, but rather through -/// the `#[node]` proc-macro. -pub trait Key: Sync + Send + 'static { - /// The type of value that is returned when getting this property from a - /// style map. For example, this could be [`Length`](crate::geom::Length) - /// for a `WIDTH` property. - type Value: Debug + Clone + PartialEq + Hash + Sync + Send + 'static; - - /// The name of the property, used for debug printing. - const NAME: &'static str; - - /// Whether the property needs folding. - const FOLDING: bool = false; - - /// The type id of the node this property belongs to. - fn node_id() -> TypeId; - - /// The default value of the property. - fn default() -> Self::Value; - - /// A static reference to the default value of the property. - /// - /// This is automatically implemented through lazy-initialization in the - /// `#[node]` macro. This way, expensive defaults don't need to be - /// recreated all the time. - fn default_ref() -> &'static Self::Value; - - /// Fold the property with an outer value. - /// - /// For example, this would fold a relative font size with an outer - /// absolute font size. - #[allow(unused_variables)] - fn fold(inner: Self::Value, outer: Self::Value) -> Self::Value { - inner - } -} - -/// Marker trait indicating that a property can be accessed by reference. -/// -/// This is implemented by a key if and only if `K::FOLDING` if false. -/// Unfortunately, Rust's type system doesn't allow use to use an associated -/// constant to bound a function, so we need this trait. -pub trait Referencable {} - -/// An entry for a single style property. -#[derive(Clone)] -struct Entry { - pair: Arc, - scoped: bool, -} - -impl Entry { - fn new(key: K, value: K::Value) -> Self { - Self { - pair: Arc::new((key, value)), - scoped: false, - } - } - - fn is(&self) -> bool { - self.pair.style_id() == TypeId::of::

() - } - - fn is_of(&self) -> bool { - self.pair.node_id() == TypeId::of::() - } - - fn is_of_id(&self, node: TypeId) -> bool { - self.pair.node_id() == node - } - - fn downcast(&self) -> Option<&K::Value> { - self.pair.as_any().downcast_ref() - } - - fn interruption(&self) -> Option { - if self.is_of::() { - Some(Interruption::Page) - } else if self.is_of::() { - Some(Interruption::Par) - } else { - None - } - } -} - -impl Debug for Entry { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("#[")?; - self.pair.dyn_fmt(f)?; - if self.scoped { - f.write_str(" (scoped)")?; - } - f.write_str("]") - } -} - -impl PartialEq for Entry { - fn eq(&self, other: &Self) -> bool { - self.pair.dyn_eq(other) && self.scoped == other.scoped - } -} - -impl Hash for Entry { - fn hash(&self, state: &mut H) { - state.write_u64(self.pair.hash64()); - state.write_u8(self.scoped as u8); - } -} - -/// This trait is implemented for pairs of zero-sized property keys and their -/// value types below. Although it is zero-sized, the property `P` must be part -/// of the implementing type so that we can use it in the methods (it must be a -/// constrained type parameter). -trait Bounds: Sync + Send + 'static { - fn as_any(&self) -> &dyn Any; - fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result; - fn dyn_eq(&self, other: &Entry) -> bool; - fn hash64(&self) -> u64; - fn node_id(&self) -> TypeId; - fn style_id(&self) -> TypeId; -} - -impl Bounds for (K, K::Value) { - fn as_any(&self) -> &dyn Any { - &self.1 - } - - fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{} = {:?}", K::NAME, self.1) - } - - fn dyn_eq(&self, other: &Entry) -> bool { - self.style_id() == other.pair.style_id() - && if let Some(other) = other.downcast::() { - &self.1 == other - } else { - false - } - } - - fn hash64(&self) -> u64 { - let mut state = fxhash::FxHasher64::default(); - self.style_id().hash(&mut state); - self.1.hash(&mut state); - state.finish() - } - - fn node_id(&self) -> TypeId { - K::node_id() - } - - fn style_id(&self) -> TypeId { - TypeId::of::() - } -} - -/// A show rule recipe. -#[derive(Debug, Clone, PartialEq, Hash)] -struct Recipe { - node: TypeId, - func: Func, - span: Span, -} - /// A chain of style maps, similar to a linked list. /// -/// A style chain allows to conceptually merge (and fold) properties from -/// multiple style maps in a node hierarchy in a non-allocating way. Rather than -/// eagerly merging the maps, each access walks the hierarchy from the innermost -/// to the outermost map, trying to find a match and then folding it with -/// matches further up the chain. +/// A style chain allows to combine properties from multiple style maps in a +/// node hierarchy in a non-allocating way. Rather than eagerly merging the +/// maps, each access walks the hierarchy from the innermost to the outermost +/// map, trying to find a match and then folding it with matches further up the +/// chain. #[derive(Default, Clone, Copy, Hash)] pub struct StyleChain<'a> { /// The first link of this chain. - link: Option>, + head: &'a [Entry], /// The remaining links in the chain. - outer: Option<&'a Self>, -} - -/// The two kinds of links in the chain. -#[derive(Clone, Copy, Hash)] -enum Link<'a> { - /// Just a map with styles. - Map(&'a StyleMap), - /// A barrier that, in combination with one more such barrier, stops scoped - /// styles for the node with this type id. - Barrier(TypeId), + tail: Option<&'a Self>, } impl<'a> StyleChain<'a> { - /// Start a new style chain with a root map. - pub fn new(map: &'a StyleMap) -> Self { - Self { link: Some(Link::Map(map)), outer: None } + /// Create a new, empty style chain. + pub fn new() -> Self { + Self::default() } - /// The number of links in the chain. - pub fn len(self) -> usize { - self.links().count() + /// Start a new style chain with a root map. + pub fn with_root(root: &'a StyleMap) -> Self { + Self { head: &root.0, tail: None } + } + + /// Get the output value of a style property. + /// + /// Returns the property's default value if no map in the chain contains an + /// entry for it. Also takes care of folding and returns references where + /// applicable. + pub fn get>(self, key: K) -> K::Output { + K::get(self.values(key)) + } + + /// Execute and return the result of a user recipe for a node if there is + /// any. + pub fn show(self, ctx: &mut Context, values: I) -> TypResult> + where + T: Node, + I: IntoIterator, + { + if let Some(recipe) = self + .entries() + .filter_map(Entry::recipe) + .find(|recipe| recipe.node == TypeId::of::()) + { + let args = Args::from_values(recipe.span, values); + Ok(Some(recipe.func.call(ctx, args)?.cast().at(recipe.span)?)) + } else { + Ok(None) + } + } +} + +impl<'a> StyleChain<'a> { + /// Return the chain, but without the trailing scoped property for the given + /// `node`. This is a 90% hack fix for show node constructor scoping. + pub(super) fn unscoped(mut self, node: TypeId) -> Self { + while self + .head + .last() + .and_then(Entry::property) + .map_or(false, |p| p.scoped && p.node == node) + { + let len = self.head.len(); + self.head = &self.head[.. len - 1] + } + self + } + + /// Remove the last link from the chain. + fn pop(&mut self) { + *self = self.tail.copied().unwrap_or_default(); } /// Build a style map from the suffix (all links beyond the `len`) of the /// chain. - /// - /// Panics if the suffix contains barrier links. - pub fn suffix(self, len: usize) -> StyleMap { + fn suffix(self, len: usize) -> StyleMap { let mut suffix = StyleMap::new(); - let remove = self.len().saturating_sub(len); - for link in self.links().take(remove) { - match link { - Link::Map(map) => suffix.apply(map), - Link::Barrier(_) => panic!("suffix contains barrier"), - } + let take = self.links().count().saturating_sub(len); + for link in self.links().take(take) { + suffix.0.splice(0 .. 0, link.iter().cloned()); } suffix } - /// Remove the last link from the chain. - pub fn pop(&mut self) { - *self = self.outer.copied().unwrap_or_default(); - } - - /// Return the chain, but without the last link if that one contains only - /// scoped styles. This is a hack. - pub(crate) fn unscoped(mut self, node: TypeId) -> Self { - if let Some(Link::Map(map)) = self.link { - if map.props.iter().all(|e| e.scoped && e.is_of_id(node)) - && map.recipes.is_empty() - { - self.pop(); - } - } - self - } -} - -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: K) -> K::Value - where - K::Value: Copy, - { - self.get_cloned(key) - } - - /// Get a reference to a style property's value. - /// - /// This is naturally only possible for properties that don't need folding. - /// Prefer `get` if possible or resort to `get_cloned` for non-`Copy` - /// properties that need folding. - /// - /// 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: K) -> &'a K::Value - where - K: Referencable, - { - self.values(key).next().unwrap_or_else(|| K::default_ref()) - } - - /// Get the (folded) value of any style property. - /// - /// 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. - /// - /// Returns the property's default value if no map in the chain contains an - /// entry for it. - pub fn get_cloned(self, key: K) -> K::Value { - if K::FOLDING { - self.values(key) - .cloned() - .chain(std::iter::once(K::default())) - .reduce(K::fold) - .unwrap() - } else { - self.values(key).next().cloned().unwrap_or_else(K::default) - } - } - - /// Execute a user recipe for a node. - pub fn show( - self, - node: &dyn Any, - ctx: &mut Context, - values: impl IntoIterator, - ) -> TypResult> { - Ok(if let Some(recipe) = self.recipes(node.type_id()).next() { - let args = Args::from_values(recipe.span, values); - Some(recipe.func.call(ctx, args)?.cast().at(recipe.span)?) - } else { - None - }) - } - - /// Insert a barrier into the style chain. - /// - /// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style - /// can still be read through a single barrier (the one of the node it - /// _should_ apply to), but a second barrier will make it invisible. - pub fn barred<'b>(&'b self, node: TypeId) -> StyleChain<'b> { - if self - .maps() - .any(|map| map.props.iter().any(|entry| entry.scoped && entry.is_of_id(node))) - { - StyleChain { - link: Some(Link::Barrier(node)), - outer: Some(self), - } - } else { - *self - } - } -} - -impl<'a> StyleChain<'a> { /// Iterate over all values for the given property in the chain. - fn values(self, _: K) -> impl Iterator { - let mut depth = 0; - self.links().flat_map(move |link| { - let mut entries: &[Entry] = &[]; - match link { - Link::Map(map) => entries = &map.props, - Link::Barrier(id) => depth += (id == K::node_id()) as usize, - } - entries - .iter() - .rev() - .filter(move |entry| entry.is::() && (!entry.scoped || depth <= 1)) - .filter_map(|entry| entry.downcast::()) - }) + fn values>(self, _: K) -> Values<'a, K> { + Values { + entries: self.entries(), + depth: 0, + key: PhantomData, + } } - /// Iterate over the recipes for the given node. - fn recipes(self, node: TypeId) -> impl Iterator { - self.maps() - .flat_map(|map| map.recipes.iter().rev()) - .filter(move |recipe| recipe.node == node) - } - - /// Iterate over the map links of the chain. - fn maps(self) -> impl Iterator { - self.links().filter_map(|link| match link { - Link::Map(map) => Some(map), - Link::Barrier(_) => None, - }) + /// Iterate over the entries of the chain. + fn entries(self) -> Entries<'a> { + Entries { + inner: [].as_slice().iter(), + links: self.links(), + } } /// Iterate over the links of the chain. - fn links(self) -> impl Iterator> { - let mut cursor = Some(self); - std::iter::from_fn(move || { - let Self { link, outer } = cursor?; - cursor = outer.copied(); - link - }) + fn links(self) -> Links<'a> { + Links(Some(self)) } } impl Debug for StyleChain<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - for link in self.links() { - link.fmt(f)?; + for entry in self.entries() { + writeln!(f, "{:?}", entry)?; } Ok(()) } } -impl Debug for Link<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Map(map) => map.fmt(f), - Self::Barrier(id) => writeln!(f, "Barrier({:?})", id), - } - } -} - impl PartialEq for StyleChain<'_> { fn eq(&self, other: &Self) -> bool { let as_ptr = |s| s as *const _; - self.link == other.link && self.outer.map(as_ptr) == other.outer.map(as_ptr) + self.head.as_ptr() == other.head.as_ptr() + && self.head.len() == other.head.len() + && self.tail.map(as_ptr) == other.tail.map(as_ptr) } } -impl PartialEq for Link<'_> { - fn eq(&self, other: &Self) -> bool { - match (*self, *other) { - (Self::Map(a), Self::Map(b)) => std::ptr::eq(a, b), - (Self::Barrier(a), Self::Barrier(b)) => a == b, - _ => false, +/// An iterator over the values in a style chain. +struct Values<'a, K> { + entries: Entries<'a>, + depth: usize, + key: PhantomData, +} + +impl<'a, K: Key<'a>> Iterator for Values<'a, K> { + type Item = &'a K::Value; + + fn next(&mut self) -> Option { + while let Some(entry) = self.entries.next() { + match entry { + Entry::Property(property) => { + if let Some(value) = property.downcast::() { + if !property.scoped || self.depth <= 1 { + return Some(value); + } + } + } + Entry::Barrier(id, _) => { + self.depth += (*id == K::node()) as usize; + } + Entry::Recipe(_) => {} + } } + + None + } +} + +/// An iterator over the entries in a style chain. +struct Entries<'a> { + inner: std::slice::Iter<'a, Entry>, + links: Links<'a>, +} + +impl<'a> Iterator for Entries<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + loop { + if let Some(entry) = self.inner.next_back() { + return Some(entry); + } + + match self.links.next() { + Some(next) => self.inner = next.iter(), + None => return None, + } + } + } +} + +/// An iterator over the links of a style chain. +struct Links<'a>(Option>); + +impl<'a> Iterator for Links<'a> { + type Item = &'a [Entry]; + + fn next(&mut self) -> Option { + let StyleChain { head, tail } = self.0?; + self.0 = tail.copied(); + Some(head) } } @@ -648,9 +687,9 @@ impl<'a, T> StyleVecBuilder<'a, T> { None => return Default::default(), }; - let mut shared = trunk.len(); + let mut shared = trunk.links().count(); for &(mut chain, _) in iter { - let len = chain.len(); + let len = chain.links().count(); if len < shared { for _ in 0 .. shared - len { trunk.pop(); diff --git a/src/eval/value.rs b/src/eval/value.rs index 300444ded..c61c95616 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -359,12 +359,12 @@ impl Dynamic { /// Whether the wrapped type is `T`. pub fn is(&self) -> bool { - self.0.as_any().is::() + (*self.0).as_any().is::() } /// Try to downcast to a reference to a specific type. pub fn downcast(&self) -> Option<&T> { - self.0.as_any().downcast_ref() + (*self.0).as_any().downcast_ref() } /// The name of the stored value's type. diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index d11de9d1c..23ad52abb 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -82,7 +82,7 @@ impl Layout for ImageNode { } // Apply link if it exists. - if let Some(url) = styles.get_ref(TextNode::LINK) { + if let Some(url) = styles.get(TextNode::LINK) { frame.link(url); } diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 9f9ff889e..177f466e3 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -132,7 +132,7 @@ impl Layout for ShapeNode { } // Apply link if it exists. - if let Some(url) = styles.get_ref(TextNode::LINK) { + if let Some(url) = styles.get(TextNode::LINK) { frame.link(url); } diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index 3602bea68..9f398277d 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -37,12 +37,12 @@ impl Layout for FlowNode { let styles = map.chain(&styles); match child { FlowChild::Leading => { - let em = styles.get(TextNode::SIZE).abs; + let em = styles.get(TextNode::SIZE); let amount = styles.get(ParNode::LEADING).resolve(em); layouter.layout_spacing(amount.into()); } FlowChild::Parbreak => { - let em = styles.get(TextNode::SIZE).abs; + let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING); let spacing = styles.get(ParNode::SPACING); let amount = (leading + spacing).resolve(em); diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index b1008febc..c8af48432 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -28,8 +28,10 @@ impl PageNode { /// How many columns the page has. pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); /// The page's header. + #[property(referenced)] pub const HEADER: Marginal = Marginal::None; /// The page's footer. + #[property(referenced)] pub const FOOTER: Marginal = Marginal::None; fn construct(_: &mut Context, args: &mut Args) -> TypResult { @@ -116,8 +118,8 @@ impl PageNode { let regions = Regions::repeat(size, size, size.map(Length::is_finite)); let mut frames = child.layout(ctx, ®ions, styles)?; - let header = styles.get_ref(Self::HEADER); - let footer = styles.get_ref(Self::FOOTER); + let header = styles.get(Self::HEADER); + let footer = styles.get(Self::FOOTER); // Realize header and footer. for frame in &mut frames { diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs index ddd80435b..e6548438f 100644 --- a/src/library/math/mod.rs +++ b/src/library/math/mod.rs @@ -15,6 +15,7 @@ pub struct MathNode { #[node(showable)] impl MathNode { /// The raw text's font family. Just the normal text family if `auto`. + #[property(referenced)] pub const FAMILY: Smart = Smart::Custom(FontFamily::new("Latin Modern Math")); @@ -28,16 +29,14 @@ impl MathNode { impl Show for MathNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { + let args = [Value::Str(self.formula.clone()), Value::Bool(self.display)]; let mut content = styles - .show(self, ctx, [ - Value::Str(self.formula.clone()), - Value::Bool(self.display), - ])? + .show::(ctx, args)? .unwrap_or_else(|| Content::Text(self.formula.trim().into())); let mut map = StyleMap::new(); - if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) { - map.set_family(family, styles); + if let Smart::Custom(family) = styles.get(Self::FAMILY) { + map.set_family(family.clone(), styles); } content = content.styled_with_map(map); diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 39be5994b..a2e296fa7 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -9,8 +9,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ - Arg, Args, Array, Cast, Content, Dict, Func, Key, Layout, LayoutNode, Merge, Node, - Regions, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Value, + Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, + Node, Regions, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Value, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 7d3273f5d..7b00c6435 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -1,5 +1,5 @@ use crate::library::prelude::*; -use crate::library::text::{FontFamily, TextNode}; +use crate::library::text::{FontFamily, FontSize, TextNode, Toggle}; /// A section heading. #[derive(Debug, Hash)] @@ -14,25 +14,34 @@ pub struct HeadingNode { #[node(showable)] impl HeadingNode { /// The heading's font family. Just the normal text family if `auto`. + #[property(referenced)] pub const FAMILY: Leveled> = Leveled::Value(Smart::Auto); /// The color of text in the heading. Just the normal text color if `auto`. + #[property(referenced)] pub const FILL: Leveled> = Leveled::Value(Smart::Auto); /// The size of text in the heading. - pub const SIZE: Leveled = Leveled::Mapping(|level| { + #[property(referenced)] + pub const SIZE: Leveled = Leveled::Mapping(|level| { let upscale = (1.6 - 0.1 * level as f64).max(0.75); - Relative::new(upscale).into() + FontSize(Relative::new(upscale).into()) }); /// Whether text in the heading is strengthend. + #[property(referenced)] pub const STRONG: Leveled = Leveled::Value(true); /// Whether text in the heading is emphasized. + #[property(referenced)] pub const EMPH: Leveled = Leveled::Value(false); /// Whether the heading is underlined. + #[property(referenced)] pub const UNDERLINE: Leveled = Leveled::Value(false); /// The extra padding above the heading. + #[property(referenced)] pub const ABOVE: Leveled = Leveled::Value(Length::zero()); /// The extra padding below the heading. + #[property(referenced)] pub const BELOW: Leveled = Leveled::Value(Length::zero()); /// Whether the heading is block-level. + #[property(referenced)] pub const BLOCK: Leveled = Leveled::Value(true); fn construct(_: &mut Context, args: &mut Args) -> TypResult { @@ -47,16 +56,17 @@ impl Show for HeadingNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { macro_rules! resolve { ($key:expr) => { - styles.get_cloned($key).resolve(ctx, self.level)? + styles.get($key).resolve(ctx, self.level)? }; } - // Resolve the user recipe. + let args = [ + Value::Int(self.level as i64), + Value::Content(self.body.clone()), + ]; + let mut body = styles - .show(self, ctx, [ - Value::Int(self.level as i64), - Value::Content(self.body.clone()), - ])? + .show::(ctx, args)? .unwrap_or_else(|| self.body.clone()); let mut map = StyleMap::new(); @@ -71,11 +81,11 @@ impl Show for HeadingNode { } if resolve!(Self::STRONG) { - map.set(TextNode::STRONG, true); + map.set(TextNode::STRONG, Toggle); } if resolve!(Self::EMPH) { - map.set(TextNode::EMPH, true); + map.set(TextNode::EMPH, Toggle); } let mut seq = vec![]; @@ -116,15 +126,15 @@ pub enum Leveled { Func(Func, Span), } -impl Leveled { +impl Leveled { /// Resolve the value based on the level. - pub fn resolve(self, ctx: &mut Context, level: usize) -> TypResult { + pub fn resolve(&self, ctx: &mut Context, level: usize) -> TypResult { Ok(match self { - Self::Value(value) => value, + Self::Value(value) => value.clone(), Self::Mapping(mapping) => mapping(level), Self::Func(func, span) => { - let args = Args::from_values(span, [Value::Int(level as i64)]); - func.call(ctx, args)?.cast().at(span)? + let args = Args::from_values(*span, [Value::Int(level as i64)]); + func.call(ctx, args)?.cast().at(*span)? } }) } diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index 414f601e9..1b22e1665 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -31,6 +31,7 @@ pub type EnumNode = ListNode; #[node(showable)] impl ListNode { /// How the list is labelled. + #[property(referenced)] pub const LABEL: Label = Label::Default; /// The spacing between the list items of a non-wide list. pub const SPACING: Linear = Linear::zero(); @@ -58,17 +59,14 @@ impl ListNode { impl Show for ListNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { - let content = if let Some(content) = styles.show( - self, - ctx, - self.items.iter().map(|item| Value::Content((*item.body).clone())), - )? { + let args = self.items.iter().map(|item| Value::Content((*item.body).clone())); + let content = if let Some(content) = styles.show::(ctx, args)? { content } else { let mut children = vec![]; let mut number = self.start; - let label = styles.get_ref(Self::LABEL); + let label = styles.get(Self::LABEL); for item in &self.items { number = item.number.unwrap_or(number); @@ -79,7 +77,7 @@ impl Show for ListNode { number += 1; } - let em = styles.get(TextNode::SIZE).abs; + let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING); let spacing = if self.wide { styles.get(ParNode::SPACING) diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index 0e455eadc..64785006f 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -55,11 +55,8 @@ impl TableNode { impl Show for TableNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { - if let Some(content) = styles.show( - self, - ctx, - self.children.iter().map(|child| Value::Content(child.clone())), - )? { + let args = self.children.iter().map(|child| Value::Content(child.clone())); + if let Some(content) = styles.show::(ctx, args)? { return Ok(content); } diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index b98eb0b28..a6375e4ee 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -21,11 +21,11 @@ pub type OverlineNode = DecoNode; #[node(showable)] impl DecoNode { /// Stroke color of the line, defaults to the text color if `None`. - #[shorthand] + #[property(shorthand)] pub const STROKE: Option = None; /// Thickness of the line's strokes (dependent on scaled font size), read /// from the font tables if `None`. - #[shorthand] + #[property(shorthand)] pub const THICKNESS: Option = None; /// Position of the line relative to the baseline (dependent on scaled font /// size), read from the font tables if `None`. @@ -45,16 +45,16 @@ impl DecoNode { impl Show for DecoNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { Ok(styles - .show(self, ctx, [Value::Content(self.0.clone())])? + .show::(ctx, [Value::Content(self.0.clone())])? .unwrap_or_else(|| { - self.0.clone().styled(TextNode::LINES, vec![Decoration { + self.0.clone().styled(TextNode::DECO, Decoration { line: L, stroke: styles.get(Self::STROKE), thickness: styles.get(Self::THICKNESS), offset: styles.get(Self::OFFSET), extent: styles.get(Self::EXTENT), evade: styles.get(Self::EVADE), - }]) + }) })) } } diff --git a/src/library/text/link.rs b/src/library/text/link.rs index 4c2b5e7b2..3ef7011d1 100644 --- a/src/library/text/link.rs +++ b/src/library/text/link.rs @@ -29,14 +29,13 @@ impl LinkNode { impl Show for LinkNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { + let args = [Value::Str(self.url.clone()), match &self.body { + Some(body) => Value::Content(body.clone()), + None => Value::None, + }]; + let mut body = styles - .show(self, ctx, [ - Value::Str(self.url.clone()), - match &self.body { - Some(body) => Value::Content(body.clone()), - None => Value::None, - }, - ])? + .show::(ctx, args)? .or_else(|| self.body.clone()) .unwrap_or_else(|| { let url = &self.url; diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 2c163a59a..64994136e 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -13,7 +13,6 @@ pub use raw::*; pub use shaping::*; use std::borrow::Cow; -use std::ops::BitXor; use ttf_parser::Tag; @@ -28,7 +27,7 @@ pub struct TextNode; #[node] impl TextNode { /// A prioritized sequence of font families. - #[variadic] + #[property(referenced, variadic)] pub const FAMILY: Vec = vec![FontFamily::new("IBM Plex Sans")]; /// Whether to allow font fallback when the primary font list contains no /// match. @@ -40,14 +39,13 @@ impl TextNode { pub const WEIGHT: FontWeight = FontWeight::REGULAR; /// The width of the glyphs. pub const STRETCH: FontStretch = FontStretch::NORMAL; + /// The size of the glyphs. + #[property(shorthand, fold)] + pub const SIZE: FontSize = Length::pt(11.0); /// The glyph fill color. - #[shorthand] + #[property(shorthand)] pub const FILL: Paint = Color::BLACK.into(); - /// The size of the glyphs. - #[shorthand] - #[fold(Linear::compose)] - pub const SIZE: Linear = Length::pt(11.0).into(); /// The amount of space that should be added between characters. pub const TRACKING: Em = Em::zero(); /// The ratio by which spaces should be stretched. @@ -84,26 +82,24 @@ impl TextNode { /// Whether to convert fractions. ("frac") pub const FRACTIONS: bool = false; /// Raw OpenType features to apply. + #[property(fold)] pub const FEATURES: Vec<(Tag, u32)> = vec![]; /// Whether the font weight should be increased by 300. - #[skip] - #[fold(bool::bitxor)] - pub const STRONG: bool = false; + #[property(hidden, fold)] + pub const STRONG: Toggle = false; /// Whether the the font style should be inverted. - #[skip] - #[fold(bool::bitxor)] - pub const EMPH: bool = false; - /// The case transformation that should be applied to the next. - #[skip] + #[property(hidden, fold)] + pub const EMPH: Toggle = false; + /// A case transformation that should be applied to the text. + #[property(hidden)] pub const CASE: Option = None; - /// Decorative lines. - #[skip] - #[fold(|a, b| a.into_iter().chain(b).collect())] - pub const LINES: Vec = vec![]; /// An URL the text should link to. - #[skip] + #[property(hidden, referenced)] pub const LINK: Option = None; + /// Decorative lines. + #[property(hidden, fold)] + pub const DECO: Decoration = vec![]; fn construct(_: &mut Context, args: &mut Args) -> TypResult { // The text constructor is special: It doesn't create a text node. @@ -113,44 +109,6 @@ impl TextNode { } } -/// Strong text, rendered in boldface. -#[derive(Debug, Hash)] -pub struct StrongNode(pub Content); - -#[node(showable)] -impl StrongNode { - fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::show(Self(args.expect("body")?))) - } -} - -impl Show for StrongNode { - fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { - Ok(styles - .show(self, ctx, [Value::Content(self.0.clone())])? - .unwrap_or_else(|| self.0.clone().styled(TextNode::STRONG, true))) - } -} - -/// Emphasized text, rendered with an italic face. -#[derive(Debug, Hash)] -pub struct EmphNode(pub Content); - -#[node(showable)] -impl EmphNode { - fn construct(_: &mut Context, args: &mut Args) -> TypResult { - Ok(Content::show(Self(args.expect("body")?))) - } -} - -impl Show for EmphNode { - fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { - Ok(styles - .show(self, ctx, [Value::Content(self.0.clone())])? - .unwrap_or_else(|| self.0.clone().styled(TextNode::EMPH, true))) - } -} - /// A font family like "Arial". #[derive(Clone, Eq, PartialEq, Hash)] pub struct FontFamily(EcoString); @@ -228,6 +186,26 @@ castable! { Value::Relative(v) => Self::from_ratio(v.get() as f32), } +/// The size of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct FontSize(pub Linear); + +impl Fold for FontSize { + type Output = Length; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.0.rel.resolve(outer) + self.0.abs + } +} + +castable! { + FontSize, + Expected: "linear", + Value::Length(v) => Self(v.into()), + Value::Relative(v) => Self(v.into()), + Value::Linear(v) => Self(v), +} + castable! { Em, Expected: "float", @@ -353,6 +331,15 @@ castable! { .collect(), } +impl Fold for Vec<(Tag, u32)> { + type Output = Self; + + fn fold(mut self, outer: Self::Output) -> Self::Output { + self.extend(outer); + self + } +} + /// A case transformation on text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Case { @@ -371,3 +358,62 @@ impl Case { } } } + +/// A toggle that turns on and off alternatingly if folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Toggle; + +impl Fold for Toggle { + type Output = bool; + + fn fold(self, outer: Self::Output) -> Self::Output { + !outer + } +} + +impl Fold for Decoration { + type Output = Vec; + + fn fold(self, mut outer: Self::Output) -> Self::Output { + outer.insert(0, self); + outer + } +} + +/// Strong text, rendered in boldface. +#[derive(Debug, Hash)] +pub struct StrongNode(pub Content); + +#[node(showable)] +impl StrongNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + Ok(Content::show(Self(args.expect("body")?))) + } +} + +impl Show for StrongNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { + Ok(styles + .show::(ctx, [Value::Content(self.0.clone())])? + .unwrap_or_else(|| self.0.clone().styled(TextNode::STRONG, Toggle))) + } +} + +/// Emphasized text, rendered with an italic face. +#[derive(Debug, Hash)] +pub struct EmphNode(pub Content); + +#[node(showable)] +impl EmphNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + Ok(Content::show(Self(args.expect("body")?))) + } +} + +impl Show for EmphNode { + fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { + Ok(styles + .show::(ctx, [Value::Content(self.0.clone())])? + .unwrap_or_else(|| self.0.clone().styled(TextNode::EMPH, Toggle))) + } +} diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 97e5a3f57..673571350 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -28,6 +28,7 @@ pub enum ParChild { #[node] impl ParNode { /// An ISO 639-1 language code. + #[property(referenced)] pub const LANG: Option = None; /// The direction for text and inline objects. pub const DIR: Dir = Dir::LTR; @@ -611,7 +612,7 @@ fn breakpoints<'a>( let mut lang = None; if styles.get(ParNode::HYPHENATE).unwrap_or(styles.get(ParNode::JUSTIFY)) { lang = styles - .get_ref(ParNode::LANG) + .get(ParNode::LANG) .as_ref() .and_then(|iso| iso.as_bytes().try_into().ok()) .and_then(hypher::Lang::from_iso); @@ -768,7 +769,7 @@ fn stack( regions: &Regions, styles: StyleChain, ) -> Vec> { - let em = styles.get(TextNode::SIZE).abs; + let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING).resolve(em); let align = styles.get(ParNode::ALIGN); let justify = styles.get(ParNode::JUSTIFY); @@ -832,7 +833,7 @@ fn commit( if let Some(glyph) = text.glyphs.first() { if text.styles.get(TextNode::OVERHANG) { let start = text.dir.is_positive(); - let em = text.styles.get(TextNode::SIZE).abs; + let em = text.styles.get(TextNode::SIZE); let amount = overhang(glyph.c, start) * glyph.x_advance.resolve(em); offset -= amount; remaining += amount; @@ -847,7 +848,7 @@ fn commit( && (reordered.len() > 1 || text.glyphs.len() > 1) { let start = !text.dir.is_positive(); - let em = text.styles.get(TextNode::SIZE).abs; + let em = text.styles.get(TextNode::SIZE); let amount = overhang(glyph.c, start) * glyph.x_advance.resolve(em); remaining += amount; } diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs index 5c2133c24..f09bc1d07 100644 --- a/src/library/text/raw.rs +++ b/src/library/text/raw.rs @@ -3,7 +3,7 @@ use syntect::easy::HighlightLines; use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet}; use syntect::parsing::SyntaxSet; -use super::{FontFamily, TextNode}; +use super::{FontFamily, TextNode, Toggle}; use crate::library::prelude::*; use crate::source::SourceId; use crate::syntax::{self, RedNode}; @@ -27,8 +27,10 @@ pub struct RawNode { #[node(showable)] impl RawNode { /// The raw text's font family. Just the normal text family if `none`. + #[property(referenced)] pub const FAMILY: Smart = Smart::Custom(FontFamily::new("IBM Plex Mono")); /// The language to syntax-highlight in. + #[property(referenced)] pub const LANG: Option = None; fn construct(_: &mut Context, args: &mut Args) -> TypResult { @@ -41,7 +43,7 @@ impl RawNode { impl Show for RawNode { fn show(&self, ctx: &mut Context, styles: StyleChain) -> TypResult { - let lang = styles.get_ref(Self::LANG).as_ref(); + let lang = styles.get(Self::LANG).as_ref(); let foreground = THEME .settings .foreground @@ -49,14 +51,16 @@ impl Show for RawNode { .unwrap_or(Color::BLACK) .into(); - let mut content = if let Some(content) = styles.show(self, ctx, [ + let args = [ Value::Str(self.text.clone()), match lang { Some(lang) => Value::Str(lang.clone()), None => Value::None, }, Value::Bool(self.block), - ])? { + ]; + + let mut content = if let Some(content) = styles.show::(ctx, args)? { content } else if matches!( lang.map(|s| s.to_lowercase()).as_deref(), @@ -93,8 +97,8 @@ impl Show for RawNode { }; let mut map = StyleMap::new(); - if let Smart::Custom(family) = styles.get_cloned(Self::FAMILY) { - map.set_family(family, styles); + if let Smart::Custom(family) = styles.get(Self::FAMILY) { + map.set_family(family.clone(), styles); } content = content.styled_with_map(map); @@ -118,11 +122,11 @@ fn styled(piece: &str, foreground: Paint, style: Style) -> Content { } if style.font_style.contains(FontStyle::BOLD) { - styles.set(TextNode::STRONG, true); + styles.set(TextNode::STRONG, Toggle); } if style.font_style.contains(FontStyle::ITALIC) { - styles.set(TextNode::EMPH, true); + styles.set(TextNode::EMPH, Toggle); } if style.font_style.contains(FontStyle::UNDERLINE) { diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index 6087032f2..66936792a 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -77,7 +77,7 @@ impl<'a> ShapedText<'a> { for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { let pos = Point::new(offset, self.baseline); - let size = self.styles.get(TextNode::SIZE).abs; + let size = self.styles.get(TextNode::SIZE); let fill = self.styles.get(TextNode::FILL); let glyphs = group .iter() @@ -99,7 +99,7 @@ impl<'a> ShapedText<'a> { let width = text.width(); // Apply line decorations. - for deco in self.styles.get_cloned(TextNode::LINES) { + for deco in self.styles.get(TextNode::DECO) { decorate(&mut frame, &deco, fonts, &text, pos, width); } @@ -108,7 +108,7 @@ impl<'a> ShapedText<'a> { } // Apply link if it exists. - if let Some(url) = self.styles.get_ref(TextNode::LINK) { + if let Some(url) = self.styles.get(TextNode::LINK) { frame.link(url); } @@ -127,7 +127,7 @@ impl<'a> ShapedText<'a> { .filter(|g| g.is_space()) .map(|g| g.x_advance) .sum::() - .resolve(self.styles.get(TextNode::SIZE).abs) + .resolve(self.styles.get(TextNode::SIZE)) } /// Reshape a range of the shaped text, reusing information from this @@ -154,7 +154,7 @@ impl<'a> ShapedText<'a> { /// Push a hyphen to end of the text. pub fn push_hyphen(&mut self, fonts: &mut FontStore) { - let size = self.styles.get(TextNode::SIZE).abs; + let size = self.styles.get(TextNode::SIZE); let variant = variant(self.styles); families(self.styles).find_map(|family| { let face_id = fonts.select(family, variant)?; @@ -467,7 +467,7 @@ fn measure( let mut top = Length::zero(); let mut bottom = Length::zero(); - let size = styles.get(TextNode::SIZE).abs; + let size = styles.get(TextNode::SIZE); let top_edge = styles.get(TextNode::TOP_EDGE); let bottom_edge = styles.get(TextNode::BOTTOM_EDGE); @@ -537,7 +537,7 @@ fn families(styles: StyleChain) -> impl Iterator + Clone { let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] }; styles - .get_ref(TextNode::FAMILY) + .get(TextNode::FAMILY) .iter() .map(|family| family.as_str()) .chain(tail.iter().copied()) @@ -609,7 +609,7 @@ fn tags(styles: StyleChain) -> Vec { feat(b"frac", 1); } - for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { + for (tag, value) in styles.get(TextNode::FEATURES) { tags.push(Feature::new(tag, value, ..)) } diff --git a/tests/typeset.rs b/tests/typeset.rs index 5e9b79fea..f0e7564ff 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -13,7 +13,7 @@ use typst::eval::{Smart, StyleMap, Value}; use typst::frame::{Element, Frame}; use typst::geom::{Length, RgbaColor}; use typst::library::layout::PageNode; -use typst::library::text::TextNode; +use typst::library::text::{FontSize, TextNode}; use typst::loading::FsLoader; use typst::parse::Scanner; use typst::source::SourceFile; @@ -67,7 +67,7 @@ fn main() { 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::BOTTOM, Smart::Custom(Length::pt(10.0).into())); - styles.set(TextNode::SIZE, Length::pt(10.0).into()); + styles.set(TextNode::SIZE, FontSize(Length::pt(10.0).into())); // Hook up an assert function into the global scope. let mut std = typst::library::new();