diff --git a/src/frame.rs b/src/frame.rs index 2a16b6f21..1bd1f4548 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -37,6 +37,34 @@ impl Frame { self.baseline.unwrap_or(self.size.y) } + /// The layer the next item will be added on. This corresponds to the number + /// of elements in the frame. + pub fn layer(&self) -> usize { + self.elements.len() + } + + /// Whether the frame has comparatively few elements. + pub fn is_light(&self) -> bool { + self.elements.len() <= 5 + } + + /// Add an element at a position in the foreground. + pub fn push(&mut self, pos: Point, element: Element) { + self.elements.push((pos, element)); + } + + /// Add a frame. + /// + /// Automatically decides whether to inline the frame or to include it as a + /// group based on the number of elements in the frame. + pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) { + if self.elements.is_empty() || frame.as_ref().is_light() { + frame.inline(self, self.layer(), pos); + } else { + self.elements.push((pos, Element::Group(Group::new(frame.share())))); + } + } + /// Add an element at a position in the background. pub fn prepend(&mut self, pos: Point, element: Element) { self.elements.insert(0, (pos, element)); @@ -50,15 +78,14 @@ impl Frame { self.elements.splice(0 .. 0, insert); } - /// Add an element at a position in the foreground. - pub fn push(&mut self, pos: Point, element: Element) { - self.elements.push((pos, element)); - } - - /// The layer the next item will be added on. This corresponds to the number - /// of elements in the frame. - pub fn layer(&self) -> usize { - self.elements.len() + /// Add a frame at a position in the background. + pub fn prepend_frame(&mut self, pos: Point, frame: impl FrameRepr) { + if self.elements.is_empty() || frame.as_ref().is_light() { + frame.inline(self, 0, pos); + } else { + self.elements + .insert(0, (pos, Element::Group(Group::new(frame.share())))); + } } /// Insert an element at the given layer in the frame. @@ -68,18 +95,6 @@ impl Frame { self.elements.insert(layer, (pos, element)); } - /// Add a frame. - /// - /// Automatically decides whether to inline the frame or to include it as a - /// group based on the number of elements in the frame. - pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) { - if self.elements.is_empty() || frame.as_ref().elements.len() <= 5 { - frame.inline(self, pos); - } else { - self.elements.push((pos, Element::Group(Group::new(frame.share())))); - } - } - /// Resize the frame to a new size, distributing new space according to the /// given alignments. pub fn resize(&mut self, target: Size, aligns: Spec) { @@ -153,7 +168,7 @@ pub trait FrameRepr: AsRef { fn share(self) -> Arc; /// Inline `self` into the sink frame. - fn inline(self, sink: &mut Frame, offset: Point); + fn inline(self, sink: &mut Frame, layer: usize, offset: Point); } impl FrameRepr for Frame { @@ -161,16 +176,18 @@ impl FrameRepr for Frame { Arc::new(self) } - fn inline(self, sink: &mut Frame, offset: Point) { + fn inline(self, sink: &mut Frame, layer: usize, offset: Point) { if offset.is_zero() { if sink.elements.is_empty() { sink.elements = self.elements; } else { - sink.elements.extend(self.elements); + sink.elements.splice(layer .. layer, self.elements); } } else { - sink.elements - .extend(self.elements.into_iter().map(|(p, e)| (p + offset, e))); + sink.elements.splice( + layer .. layer, + self.elements.into_iter().map(|(p, e)| (p + offset, e)), + ); } } } @@ -180,12 +197,15 @@ impl FrameRepr for Arc { self } - fn inline(self, sink: &mut Frame, offset: Point) { + fn inline(self, sink: &mut Frame, layer: usize, offset: Point) { match Arc::try_unwrap(self) { - Ok(frame) => frame.inline(sink, offset), - Err(rc) => sink - .elements - .extend(rc.elements.iter().cloned().map(|(p, e)| (p + offset, e))), + Ok(frame) => frame.inline(sink, layer, offset), + Err(rc) => { + sink.elements.splice( + layer .. layer, + rc.elements.iter().cloned().map(|(p, e)| (p + offset, e)), + ); + } } } } @@ -198,10 +218,10 @@ impl FrameRepr for MaybeShared { } } - fn inline(self, sink: &mut Frame, offset: Point) { + fn inline(self, sink: &mut Frame, layer: usize, offset: Point) { match self { - Self::Owned(owned) => owned.inline(sink, offset), - Self::Shared(shared) => shared.inline(sink, offset), + Self::Owned(owned) => owned.inline(sink, layer, offset), + Self::Shared(shared) => shared.inline(sink, layer, offset), } } } diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 7d91aa6d0..8435e5103 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -34,6 +34,12 @@ impl PageNode { /// The page's footer. #[property(referenced)] pub const FOOTER: Marginal = Marginal::None; + /// Content in the page's background. + #[property(referenced)] + pub const BACKGROUND: Marginal = Marginal::None; + /// Content in the page's foreground. + #[property(referenced)] + pub const FOREGROUND: Marginal = Marginal::None; fn construct(_: &mut Machine, args: &mut Args) -> TypResult { Ok(Content::Page(Self(args.expect("body")?))) @@ -95,22 +101,29 @@ impl PageNode { let header = styles.get(Self::HEADER); let footer = styles.get(Self::FOOTER); + let foreground = styles.get(Self::FOREGROUND); + let background = styles.get(Self::BACKGROUND); - // Realize header and footer. + // Realize overlays. for frame in &mut frames { let size = frame.size; - let padding = padding.resolve(styles).relative_to(size); - for (y, h, marginal) in [ - (Length::zero(), padding.top, header), - (size.y - padding.bottom, padding.bottom, footer), + let pad = padding.resolve(styles).relative_to(size); + let pw = size.x - pad.left - pad.right; + let py = size.y - pad.bottom; + for (marginal, pos, area) in [ + (header, Point::with_x(pad.left), Size::new(pw, pad.top)), + (footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)), + (foreground, Point::zero(), size), + (background, Point::zero(), size), ] { if let Some(content) = marginal.resolve(ctx, page)? { - let pos = Point::new(padding.left, y); - let w = size.x - padding.left - padding.right; - let area = Size::new(w, h); - let pod = Regions::one(area, area, area.map(Length::is_finite)); + let pod = Regions::one(area, area, Spec::splat(true)); let sub = content.layout(ctx, &pod, styles)?.remove(0); - Arc::make_mut(frame).push_frame(pos, sub); + if std::ptr::eq(marginal, background) { + Arc::make_mut(frame).prepend_frame(pos, sub); + } else { + Arc::make_mut(frame).push_frame(pos, sub); + } } } @@ -140,7 +153,7 @@ impl PagebreakNode { } } -/// A header or footer definition. +/// A header, footer, foreground or background definition. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Marginal { /// Nothing, diff --git a/tests/ref/layout/locate-group.png b/tests/ref/layout/locate-group.png index e38a4a32a..c9fe40657 100644 Binary files a/tests/ref/layout/locate-group.png and b/tests/ref/layout/locate-group.png differ diff --git a/tests/ref/layout/page-marginals.png b/tests/ref/layout/page-marginals.png index 734973b68..78dd4591f 100644 Binary files a/tests/ref/layout/page-marginals.png and b/tests/ref/layout/page-marginals.png differ diff --git a/tests/typ/layout/locate-group.typ b/tests/typ/layout/locate-group.typ index 5c022f5ac..c3a5ddab2 100644 --- a/tests/typ/layout/locate-group.typ +++ b/tests/typ/layout/locate-group.typ @@ -32,16 +32,43 @@ ..{ let seen = () for item in all { - if item.value not in seen { - seen.push(item.value) - (num(all, item.value), item.value) - } + if item.value in seen { continue } + (num(all, item.value), item.value) + seen.push(item.value) } } ))} As shown in #cite("abc") and #cite("def") and #cite("abc") ... +--- +// Test lovely sidebar. +#let lovely = group("lovely") +#let words = ("Juliet", "soft", "fair", "maid") +#let regex = regex(words.map(p => "(" + p + ")").join("|")) +#show word: regex as underline(word) + lovely.entry(_ => {}) +#set page( + paper: "a8", + margins: (left: 25pt, rest: 15pt), + foreground: lovely.all(entries => { + let seen = () + for y in entries.map(it => it.y) { + if y in seen { continue } + let line = entries.filter(it => it.y == y) + for i, it in line { + let x = 10pt - 4pt * (line.len() - i - 1) + place(dx: x, dy: it.y - 8pt, [💗]) + } + seen.push(y) + } + }), +) + +But, soft! what light through yonder window breaks? It is the east, and Juliet +is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and +pale with grief, That thou her maid art far more fair than she: Be not her maid, +since she is envious. + --- // Test that `all` contains `me`. // Ref: false diff --git a/tests/typ/layout/page-marginals.typ b/tests/typ/layout/page-marginals.typ index 6e8e3d853..9b3236925 100644 --- a/tests/typ/layout/page-marginals.typ +++ b/tests/typ/layout/page-marginals.typ @@ -7,6 +7,9 @@ text(0.8em)[_Chapter 1_] }), footer: page => v(5pt) + align(center)[\~ #page \~], + background: n => if n <= 2 { + place(center + horizon, circle(radius: 1cm, fill: luma(90%))) + } ) But, soft! what light through yonder window breaks? It is the east, and Juliet