diff --git a/src/geom/length.rs b/src/geom/length.rs index 838d33c00..968887648 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -193,6 +193,14 @@ assign_impl!(Length -= Length); assign_impl!(Length *= f64); assign_impl!(Length /= f64); +impl Rem for Length { + type Output = Self; + + fn rem(self, other: Self) -> Self::Output { + Self(self.0 % other.0) + } +} + impl Sum for Length { fn sum>(iter: I) -> Self { Self(iter.map(|s| s.0).sum()) diff --git a/src/geom/scalar.rs b/src/geom/scalar.rs index 91225a2b4..b45ae60af 100644 --- a/src/geom/scalar.rs +++ b/src/geom/scalar.rs @@ -148,6 +148,20 @@ impl> DivAssign for Scalar { } } +impl> Rem for Scalar { + type Output = Self; + + fn rem(self, rhs: T) -> Self::Output { + Self(self.0 % rhs.into().0) + } +} + +impl> RemAssign for Scalar { + fn rem_assign(&mut self, rhs: T) { + self.0 %= rhs.into().0; + } +} + impl Sum for Scalar { fn sum>(iter: I) -> Self { Self(iter.map(|s| s.0).sum()) diff --git a/src/library/mod.rs b/src/library/mod.rs index 0034b5815..d3ed98da5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -29,6 +29,7 @@ pub fn new() -> Scope { std.def_node::("strike"); std.def_node::("overline"); std.def_node::("link"); + std.def_node::("repeat"); // Structure. std.def_node::("heading"); diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 0eb57339f..bde553e23 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -6,6 +6,7 @@ mod link; mod par; mod quotes; mod raw; +mod repeat; mod shaping; pub use deco::*; @@ -14,6 +15,7 @@ pub use link::*; pub use par::*; pub use quotes::*; pub use raw::*; +pub use repeat::*; pub use shaping::*; use std::borrow::Cow; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index fc978357e..4717c3af1 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -4,7 +4,7 @@ use unicode_bidi::{BidiInfo, Level}; use unicode_script::{Script, UnicodeScript}; use xi_unicode::LineBreakIterator; -use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; +use super::{shape, Lang, Quoter, Quotes, RepeatNode, ShapedText, TextNode}; use crate::font::FontStore; use crate::library::layout::Spacing; use crate::library::prelude::*; @@ -76,7 +76,7 @@ impl Layout for ParNode { let lines = linebreak(&p, &mut ctx.fonts, regions.first.x); // Stack the lines into one frame per region. - Ok(stack(&lines, &mut ctx.fonts, regions, styles)) + stack(ctx, &lines, regions, styles) } } @@ -262,6 +262,8 @@ enum Item<'a> { Fractional(Fraction), /// A layouted child node. Frame(Frame), + /// A repeating node. + Repeat(&'a RepeatNode), } impl<'a> Item<'a> { @@ -278,7 +280,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.text.len(), Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), - Self::Frame(_) => NODE_REPLACE.len_utf8(), + Self::Frame(_) | Self::Repeat(_) => NODE_REPLACE.len_utf8(), } } @@ -287,7 +289,7 @@ impl<'a> Item<'a> { match self { Item::Text(shaped) => shaped.width, Item::Absolute(v) => *v, - Item::Fractional(_) => Length::zero(), + Item::Fractional(_) | Self::Repeat(_) => Length::zero(), Item::Frame(frame) => frame.size.x, } } @@ -374,6 +376,7 @@ impl<'a> Line<'a> { self.items() .filter_map(|item| match item { Item::Fractional(fr) => Some(*fr), + Item::Repeat(_) => Some(Fraction::one()), _ => None, }) .sum() @@ -518,10 +521,14 @@ fn prepare<'a>( } }, Segment::Node(node) => { - let size = Size::new(regions.first.x, regions.base.y); - let pod = Regions::one(size, regions.base, Spec::splat(false)); - let frame = node.layout(ctx, &pod, styles)?.remove(0); - items.push(Item::Frame(Arc::take(frame))); + if let Some(repeat) = node.downcast() { + items.push(Item::Repeat(repeat)); + } else { + let size = Size::new(regions.first.x, regions.base.y); + let pod = Regions::one(size, regions.base, Spec::splat(false)); + let frame = node.layout(ctx, &pod, styles)?.remove(0); + items.push(Item::Frame(Arc::take(frame))); + } } } @@ -954,11 +961,11 @@ fn line<'a>( /// Combine layouted lines into one frame per region. fn stack( + ctx: &mut Context, lines: &[Line], - fonts: &mut FontStore, regions: &Regions, styles: StyleChain, -) -> Vec> { +) -> TypResult>> { let leading = styles.get(ParNode::LEADING); let align = styles.get(ParNode::ALIGN); let justify = styles.get(ParNode::JUSTIFY); @@ -978,7 +985,7 @@ fn stack( // Stack the lines into one frame per region. for line in lines { - let frame = commit(line, fonts, width, align, justify); + let frame = commit(ctx, line, ®ions, width, styles, align, justify)?; let height = frame.size.y; while !regions.first.y.fits(height) && !regions.in_last() { @@ -1001,17 +1008,19 @@ fn stack( } finished.push(Arc::new(output)); - finished + Ok(finished) } /// Commit to a line and build its frame. fn commit( + ctx: &mut Context, line: &Line, - fonts: &mut FontStore, + regions: &Regions, width: Length, + styles: StyleChain, align: Align, justify: bool, -) -> Frame { +) -> TypResult { let mut remaining = width - line.width; let mut offset = Length::zero(); @@ -1067,24 +1076,44 @@ fn commit( // Build the frames and determine the height and baseline. let mut frames = vec![]; for item in reordered { - let frame = match item { + let mut push = |offset: &mut Length, frame: Frame| { + let width = frame.size.x; + top.set_max(frame.baseline()); + bottom.set_max(frame.size.y - frame.baseline()); + frames.push((*offset, frame)); + *offset += width; + }; + + match item { Item::Absolute(v) => { offset += *v; - continue; } Item::Fractional(v) => { offset += v.share(fr, remaining); - continue; } - Item::Text(shaped) => shaped.build(fonts, justification), - Item::Frame(frame) => frame.clone(), - }; - - let width = frame.size.x; - top.set_max(frame.baseline()); - bottom.set_max(frame.size.y - frame.baseline()); - frames.push((offset, frame)); - offset += width; + Item::Text(shaped) => { + push(&mut offset, shaped.build(&mut ctx.fonts, justification)); + } + Item::Frame(frame) => { + push(&mut offset, frame.clone()); + } + Item::Repeat(node) => { + let before = offset; + let width = Fraction::one().share(fr, remaining); + let size = Size::new(width, regions.base.y); + let pod = Regions::one(size, regions.base, Spec::new(false, false)); + let frame = node.layout(ctx, &pod, styles)?.remove(0); + let count = (width / frame.size.x).floor(); + let apart = (width % frame.size.x) / (count - 1.0); + if frame.size.x > Length::zero() { + for _ in 0 .. (count as usize).min(1000) { + push(&mut offset, frame.as_ref().clone()); + offset += apart; + } + } + offset = before + width; + } + } } let size = Size::new(width, top + bottom); @@ -1098,7 +1127,7 @@ fn commit( output.merge_frame(Point::new(x, y), frame); } - output + Ok(output) } /// Return a line's items in visual order. diff --git a/src/library/text/repeat.rs b/src/library/text/repeat.rs new file mode 100644 index 000000000..68036be7e --- /dev/null +++ b/src/library/text/repeat.rs @@ -0,0 +1,24 @@ +use crate::library::prelude::*; + +/// Fill space by repeating something horizontally. +#[derive(Debug, Hash)] +pub struct RepeatNode(pub LayoutNode); + +#[node] +impl RepeatNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + Ok(Content::inline(Self(args.expect("body")?))) + } +} + +impl Layout for RepeatNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult>> { + // The actual repeating happens directly in the paragraph. + self.0.layout(ctx, regions, styles) + } +} diff --git a/tests/ref/text/repeat.png b/tests/ref/text/repeat.png new file mode 100644 index 000000000..898de96f1 Binary files /dev/null and b/tests/ref/text/repeat.png differ diff --git a/tests/typ/text/repeat.typ b/tests/typ/text/repeat.typ new file mode 100644 index 000000000..0036999a0 --- /dev/null +++ b/tests/typ/text/repeat.typ @@ -0,0 +1,15 @@ +// Test the `repeat` function. + +--- +#let sections = ( + ("Introduction", 1), + ("Approach", 1), + ("Evaluation", 3), + ("Discussion", 15), + ("Related Work", 16), + ("Conclusion", 253), +) + +#for section in sections [ + #section(0) #repeat[.] #section(1) \ +]