From db820ae9aa095cf47d4ae3a582467b01613c3711 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 17 Apr 2022 12:11:00 +0200 Subject: [PATCH] Repeat function --- src/geom/length.rs | 8 ++++ src/geom/scalar.rs | 14 +++++++ src/library/mod.rs | 1 + src/library/text/mod.rs | 2 + src/library/text/par.rs | 83 +++++++++++++++++++++++++------------ src/library/text/repeat.rs | 24 +++++++++++ tests/ref/text/repeat.png | Bin 0 -> 6637 bytes tests/typ/text/repeat.typ | 15 +++++++ 8 files changed, 120 insertions(+), 27 deletions(-) create mode 100644 src/library/text/repeat.rs create mode 100644 tests/ref/text/repeat.png create mode 100644 tests/typ/text/repeat.typ 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 0000000000000000000000000000000000000000..898de96f1687e10bcb7d88b9d202a45b6922e896 GIT binary patch literal 6637 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 literal 0 HcmV?d00001 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) \ +]