summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/pages')
-rw-r--r--crates/typst-layout/src/pages/collect.rs165
-rw-r--r--crates/typst-layout/src/pages/finalize.rs73
-rw-r--r--crates/typst-layout/src/pages/mod.rs159
-rw-r--r--crates/typst-layout/src/pages/run.rs267
4 files changed, 664 insertions, 0 deletions
diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs
new file mode 100644
index 00000000..1903d6ac
--- /dev/null
+++ b/crates/typst-layout/src/pages/collect.rs
@@ -0,0 +1,165 @@
+use std::collections::HashSet;
+
+use typst_library::foundations::StyleChain;
+use typst_library::introspection::{Locator, SplitLocator, Tag, TagElem};
+use typst_library::layout::{PagebreakElem, Parity};
+use typst_library::routines::Pair;
+
+/// An item in page layout.
+pub enum Item<'a> {
+ /// A page run containing content. All runs will be layouted in parallel.
+ Run(&'a [Pair<'a>], StyleChain<'a>, Locator<'a>),
+ /// Tags in between pages. These will be prepended to the first start of
+ /// the next page, or appended at the very end of the final page if there is
+ /// no next page.
+ Tags(&'a [Pair<'a>]),
+ /// An instruction to possibly add a page to bring the page number parity to
+ /// the desired state. Can only be done at the end, sequentially, because it
+ /// requires knowledge of the concrete page number.
+ Parity(Parity, StyleChain<'a>, Locator<'a>),
+}
+
+/// Slices up the children into logical parts, processing styles and handling
+/// things like tags and weak pagebreaks.
+pub fn collect<'a>(
+ mut children: &'a mut [Pair<'a>],
+ mut locator: SplitLocator<'a>,
+ mut initial: StyleChain<'a>,
+) -> Vec<Item<'a>> {
+ // The collected page-level items.
+ let mut items: Vec<Item<'a>> = vec![];
+ // When this is true, an empty page should be added to `pages` at the end.
+ let mut staged_empty_page = true;
+
+ // The `children` are a flat list of flow-level items and pagebreaks. This
+ // loops splits it up into pagebreaks and consecutive slices of
+ // non-pagebreaks. From these pieces, we build page items that we can then
+ // layout in parallel.
+ while let Some(&(elem, styles)) = children.first() {
+ if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() {
+ // Add a blank page if we encounter a strong pagebreak and there was
+ // a staged empty page.
+ let strong = !pagebreak.weak(styles);
+ if strong && staged_empty_page {
+ let locator = locator.next(&elem.span());
+ items.push(Item::Run(&[], initial, locator));
+ }
+
+ // Add an instruction to adjust the page parity if requested.
+ if let Some(parity) = pagebreak.to(styles) {
+ let locator = locator.next(&elem.span());
+ items.push(Item::Parity(parity, styles, locator));
+ }
+
+ // The initial styles for the next page are ours unless this is a
+ // "boundary" pagebreak. Such a pagebreak is generated at the end of
+ // the scope of a page set rule to ensure a page boundary. It's
+ // styles correspond to the styles _before_ the page set rule, so we
+ // don't want to apply it to a potential empty page.
+ if !pagebreak.boundary(styles) {
+ initial = styles;
+ }
+
+ // Stage an empty page after a strong pagebreak.
+ staged_empty_page |= strong;
+
+ // Advance to the next child.
+ children = &mut children[1..];
+ } else {
+ // Find the end of the consecutive non-pagebreak run.
+ let end =
+ children.iter().take_while(|(c, _)| !c.is::<PagebreakElem>()).count();
+
+ // Migrate start tags without accompanying end tags from before a
+ // pagebreak to after it.
+ let end = migrate_unterminated_tags(children, end);
+ if end == 0 {
+ continue;
+ }
+
+ // Advance to the rest of the children.
+ let (group, rest) = children.split_at_mut(end);
+ children = rest;
+
+ // If all that is left now are tags, then we don't want to add a
+ // page just for them (since no group would have been detected in a
+ // tagless layout and tags should never affect the layout). For this
+ // reason, we remember them in a `PageItem::Tags` and later insert
+ // them at the _very start_ of the next page, even before the
+ // header.
+ //
+ // We don't do this if all that's left is end boundary pagebreaks
+ // and if an empty page is still staged, since then we can just
+ // conceptually replace that final page with us.
+ if group.iter().all(|(c, _)| c.is::<TagElem>())
+ && !(staged_empty_page
+ && children.iter().all(|&(c, s)| {
+ c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary(s))
+ }))
+ {
+ items.push(Item::Tags(group));
+ continue;
+ }
+
+ // Record a page run and then disregard a staged empty page because
+ // we have real content now.
+ let locator = locator.next(&elem.span());
+ items.push(Item::Run(group, initial, locator));
+ staged_empty_page = false;
+ }
+ }
+
+ // Flush a staged empty page.
+ if staged_empty_page {
+ items.push(Item::Run(&[], initial, locator.next(&())));
+ }
+
+ items
+}
+
+/// Migrates trailing start tags without accompanying end tags from before
+/// a pagebreak to after it. Returns the position right after the last
+/// non-migrated tag.
+///
+/// This is important because we want the positions of introspectable elements
+/// that technically started before a pagebreak, but have no visible content
+/// yet, to be after the pagebreak. A typical case where this happens is `show
+/// heading: it => pagebreak() + it`.
+fn migrate_unterminated_tags(children: &mut [Pair], mid: usize) -> usize {
+ // Compute the range from before the first trailing tag to after the last
+ // following pagebreak.
+ let (before, after) = children.split_at(mid);
+ let start = mid - before.iter().rev().take_while(|&(c, _)| c.is::<TagElem>()).count();
+ let end = mid + after.iter().take_while(|&(c, _)| c.is::<PagebreakElem>()).count();
+
+ // Determine the set of tag locations which we won't migrate (because they
+ // are terminated).
+ let excluded: HashSet<_> = children[start..mid]
+ .iter()
+ .filter_map(|(c, _)| match c.to_packed::<TagElem>()?.tag {
+ Tag::Start(_) => None,
+ Tag::End(loc, _) => Some(loc),
+ })
+ .collect();
+
+ // A key function that partitions the area of interest into three groups:
+ // Excluded tags (-1) | Pagebreaks (0) | Migrated tags (1).
+ let key = |(c, _): &Pair| match c.to_packed::<TagElem>() {
+ Some(elem) => {
+ if excluded.contains(&elem.tag.location()) {
+ -1
+ } else {
+ 1
+ }
+ }
+ None => 0,
+ };
+
+ // Partition the children using a *stable* sort. While it would be possible
+ // to write a more efficient direct algorithm for this, the sort version is
+ // less likely to have bugs and this is absolutely not on a hot path.
+ children[start..end].sort_by_key(key);
+
+ // Compute the new end index, right before the pagebreaks.
+ start + children[start..end].iter().take_while(|pair| key(pair) == -1).count()
+}
diff --git a/crates/typst-layout/src/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs
new file mode 100644
index 00000000..8df6cb16
--- /dev/null
+++ b/crates/typst-layout/src/pages/finalize.rs
@@ -0,0 +1,73 @@
+use typst_library::diag::SourceResult;
+use typst_library::engine::Engine;
+use typst_library::introspection::{ManualPageCounter, Tag};
+use typst_library::layout::{Frame, FrameItem, Page, Point};
+
+use super::LayoutedPage;
+
+/// Piece together the inner page frame and the marginals. We can only do this
+/// at the very end because inside/outside margins require knowledge of the
+/// physical page number, which is unknown during parallel layout.
+pub fn finalize(
+ engine: &mut Engine,
+ counter: &mut ManualPageCounter,
+ tags: &mut Vec<Tag>,
+ LayoutedPage {
+ inner,
+ mut margin,
+ binding,
+ two_sided,
+ header,
+ footer,
+ background,
+ foreground,
+ fill,
+ numbering,
+ }: LayoutedPage,
+) -> SourceResult<Page> {
+ // If two sided, left becomes inside and right becomes outside.
+ // Thus, for left-bound pages, we want to swap on even pages and
+ // for right-bound pages, we want to swap on odd pages.
+ if two_sided && binding.swap(counter.physical()) {
+ std::mem::swap(&mut margin.left, &mut margin.right);
+ }
+
+ // Create a frame for the full page.
+ let mut frame = Frame::hard(inner.size() + margin.sum_by_axis());
+
+ // Add tags.
+ for tag in tags.drain(..) {
+ frame.push(Point::zero(), FrameItem::Tag(tag));
+ }
+
+ // Add the "before" marginals. The order in which we push things here is
+ // important as it affects the relative ordering of introspectable elements
+ // and thus how counters resolve.
+ if let Some(background) = background {
+ frame.push_frame(Point::zero(), background);
+ }
+ if let Some(header) = header {
+ frame.push_frame(Point::with_x(margin.left), header);
+ }
+
+ // Add the inner contents.
+ frame.push_frame(Point::new(margin.left, margin.top), inner);
+
+ // Add the "after" marginals.
+ if let Some(footer) = footer {
+ let y = frame.height() - footer.height();
+ frame.push_frame(Point::new(margin.left, y), footer);
+ }
+ if let Some(foreground) = foreground {
+ frame.push_frame(Point::zero(), foreground);
+ }
+
+ // Apply counter updates from within the page to the manual page counter.
+ counter.visit(engine, &frame)?;
+
+ // Get this page's number and then bump the counter for the next page.
+ let number = counter.logical();
+ counter.step();
+
+ Ok(Page { frame, fill, numbering, number })
+}
diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs
new file mode 100644
index 00000000..b969749a
--- /dev/null
+++ b/crates/typst-layout/src/pages/mod.rs
@@ -0,0 +1,159 @@
+//! Layout of content into a [`Document`].
+
+mod collect;
+mod finalize;
+mod run;
+
+use comemo::{Tracked, TrackedMut};
+use typst_library::diag::SourceResult;
+use typst_library::engine::{Engine, Route, Sink, Traced};
+use typst_library::foundations::{Content, StyleChain};
+use typst_library::introspection::{
+ Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
+};
+use typst_library::layout::{FrameItem, Page, Point};
+use typst_library::model::{Document, DocumentInfo};
+use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
+use typst_library::World;
+
+use self::collect::{collect, Item};
+use self::finalize::finalize;
+use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
+
+/// Layout content into a document.
+///
+/// This first performs root-level realization and then lays out the resulting
+/// elements. In contrast to [`layout_fragment`](crate::layout_fragment),
+/// this does not take regions since the regions are defined by the page
+/// configuration in the content and style chain.
+#[typst_macros::time(name = "document")]
+pub fn layout_document(
+ engine: &mut Engine,
+ content: &Content,
+ styles: StyleChain,
+) -> SourceResult<Document> {
+ layout_document_impl(
+ engine.routines,
+ engine.world,
+ engine.introspector,
+ engine.traced,
+ TrackedMut::reborrow_mut(&mut engine.sink),
+ engine.route.track(),
+ content,
+ styles,
+ )
+}
+
+/// The internal implementation of `layout_document`.
+#[comemo::memoize]
+#[allow(clippy::too_many_arguments)]
+fn layout_document_impl(
+ routines: &Routines,
+ world: Tracked<dyn World + '_>,
+ introspector: Tracked<Introspector>,
+ traced: Tracked<Traced>,
+ sink: TrackedMut<Sink>,
+ route: Tracked<Route>,
+ content: &Content,
+ styles: StyleChain,
+) -> SourceResult<Document> {
+ let mut locator = Locator::root().split();
+ let mut engine = Engine {
+ routines,
+ world,
+ introspector,
+ traced,
+ sink,
+ route: Route::extend(route).unnested(),
+ };
+
+ // Mark the external styles as "outside" so that they are valid at the page
+ // level.
+ let styles = styles.to_map().outside();
+ let styles = StyleChain::new(&styles);
+
+ let arenas = Arenas::default();
+ let mut info = DocumentInfo::default();
+ let mut children = (engine.routines.realize)(
+ RealizationKind::Root(&mut info),
+ &mut engine,
+ &mut locator,
+ &arenas,
+ content,
+ styles,
+ )?;
+
+ let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
+ let introspector = Introspector::new(&pages);
+
+ Ok(Document { pages, info, introspector })
+}
+
+/// Layouts the document's pages.
+fn layout_pages<'a>(
+ engine: &mut Engine,
+ children: &'a mut [Pair<'a>],
+ locator: SplitLocator<'a>,
+ styles: StyleChain<'a>,
+) -> SourceResult<Vec<Page>> {
+ // Slice up the children into logical parts.
+ let items = collect(children, locator, styles);
+
+ // Layout the page runs in parallel.
+ let mut runs = engine.parallelize(
+ items.iter().filter_map(|item| match item {
+ Item::Run(children, initial, locator) => {
+ Some((children, initial, locator.relayout()))
+ }
+ _ => None,
+ }),
+ |engine, (children, initial, locator)| {
+ layout_page_run(engine, children, locator, *initial)
+ },
+ );
+
+ let mut pages = vec![];
+ let mut tags = vec![];
+ let mut counter = ManualPageCounter::new();
+
+ // Collect and finalize the runs, handling things like page parity and tags
+ // between pages.
+ for item in &items {
+ match item {
+ Item::Run(..) => {
+ let layouted = runs.next().unwrap()?;
+ for layouted in layouted {
+ let page = finalize(engine, &mut counter, &mut tags, layouted)?;
+ pages.push(page);
+ }
+ }
+ Item::Parity(parity, initial, locator) => {
+ if !parity.matches(pages.len()) {
+ continue;
+ }
+
+ let layouted = layout_blank_page(engine, locator.relayout(), *initial)?;
+ let page = finalize(engine, &mut counter, &mut tags, layouted)?;
+ pages.push(page);
+ }
+ Item::Tags(items) => {
+ tags.extend(
+ items
+ .iter()
+ .filter_map(|(c, _)| c.to_packed::<TagElem>())
+ .map(|elem| elem.tag.clone()),
+ );
+ }
+ }
+ }
+
+ // Add the remaining tags to the very end of the last page.
+ if !tags.is_empty() {
+ let last = pages.last_mut().unwrap();
+ let pos = Point::with_y(last.frame.height());
+ last.frame
+ .push_multiple(tags.into_iter().map(|tag| (pos, FrameItem::Tag(tag))));
+ }
+
+ Ok(pages)
+}
diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs
new file mode 100644
index 00000000..15735faa
--- /dev/null
+++ b/crates/typst-layout/src/pages/run.rs
@@ -0,0 +1,267 @@
+use comemo::{Track, Tracked, TrackedMut};
+use typst_library::diag::SourceResult;
+use typst_library::engine::{Engine, Route, Sink, Traced};
+use typst_library::foundations::{
+ Content, NativeElement, Resolve, Smart, StyleChain, Styles,
+};
+use typst_library::introspection::{
+ Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, TagElem,
+};
+use typst_library::layout::{
+ Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, Dir, Frame, HAlignment,
+ Length, OuterVAlignment, PageElem, Paper, Region, Regions, Rel, Sides, Size,
+ VAlignment,
+};
+use typst_library::model::Numbering;
+use typst_library::routines::{Pair, Routines};
+use typst_library::text::TextElem;
+use typst_library::visualize::Paint;
+use typst_library::World;
+use typst_utils::Numeric;
+
+use crate::flow::layout_flow;
+
+/// A mostly finished layout for one page. Needs only knowledge of its exact
+/// page number to be finalized into a `Page`. (Because the margins can depend
+/// on the page number.)
+#[derive(Clone)]
+pub struct LayoutedPage {
+ pub inner: Frame,
+ pub margin: Sides<Abs>,
+ pub binding: Binding,
+ pub two_sided: bool,
+ pub header: Option<Frame>,
+ pub footer: Option<Frame>,
+ pub background: Option<Frame>,
+ pub foreground: Option<Frame>,
+ pub fill: Smart<Option<Paint>>,
+ pub numbering: Option<Numbering>,
+}
+
+/// Layout a single page suitable for parity adjustment.
+pub fn layout_blank_page(
+ engine: &mut Engine,
+ locator: Locator,
+ initial: StyleChain,
+) -> SourceResult<LayoutedPage> {
+ let layouted = layout_page_run(engine, &[], locator, initial)?;
+ Ok(layouted.into_iter().next().unwrap())
+}
+
+/// Layout a page run with uniform properties.
+#[typst_macros::time(name = "page run")]
+pub fn layout_page_run(
+ engine: &mut Engine,
+ children: &[Pair],
+ locator: Locator,
+ initial: StyleChain,
+) -> SourceResult<Vec<LayoutedPage>> {
+ layout_page_run_impl(
+ engine.routines,
+ engine.world,
+ engine.introspector,
+ engine.traced,
+ TrackedMut::reborrow_mut(&mut engine.sink),
+ engine.route.track(),
+ children,
+ locator.track(),
+ initial,
+ )
+}
+
+/// The internal implementation of `layout_page_run`.
+#[comemo::memoize]
+#[allow(clippy::too_many_arguments)]
+fn layout_page_run_impl(
+ routines: &Routines,
+ world: Tracked<dyn World + '_>,
+ introspector: Tracked<Introspector>,
+ traced: Tracked<Traced>,
+ sink: TrackedMut<Sink>,
+ route: Tracked<Route>,
+ children: &[Pair],
+ locator: Tracked<Locator>,
+ initial: StyleChain,
+) -> SourceResult<Vec<LayoutedPage>> {
+ let link = LocatorLink::new(locator);
+ let mut locator = Locator::link(&link).split();
+ let mut engine = Engine {
+ routines,
+ world,
+ introspector,
+ traced,
+ sink,
+ route: Route::extend(route),
+ };
+
+ // Determine the page-wide styles.
+ let styles = determine_page_styles(children, initial);
+ let styles = StyleChain::new(&styles);
+
+ // When one of the lengths is infinite the page fits its content along
+ // that axis.
+ let width = PageElem::width_in(styles).unwrap_or(Abs::inf());
+ let height = PageElem::height_in(styles).unwrap_or(Abs::inf());
+ let mut size = Size::new(width, height);
+ if PageElem::flipped_in(styles) {
+ std::mem::swap(&mut size.x, &mut size.y);
+ }
+
+ let mut min = width.min(height);
+ if !min.is_finite() {
+ min = Paper::A4.width();
+ }
+
+ // Determine the margins.
+ let default = Rel::<Length>::from((2.5 / 21.0) * min);
+ let margin = PageElem::margin_in(styles);
+ let two_sided = margin.two_sided.unwrap_or(false);
+ let margin = margin
+ .sides
+ .map(|side| side.and_then(Smart::custom).unwrap_or(default))
+ .resolve(styles)
+ .relative_to(size);
+
+ let fill = PageElem::fill_in(styles);
+ let foreground = PageElem::foreground_in(styles);
+ let background = PageElem::background_in(styles);
+ let header_ascent = PageElem::header_ascent_in(styles).relative_to(margin.top);
+ let footer_descent = PageElem::footer_descent_in(styles).relative_to(margin.bottom);
+ let numbering = PageElem::numbering_in(styles);
+ let number_align = PageElem::number_align_in(styles);
+ let binding =
+ PageElem::binding_in(styles).unwrap_or_else(|| match TextElem::dir_in(styles) {
+ Dir::LTR => Binding::Left,
+ _ => Binding::Right,
+ });
+
+ // Construct the numbering (for header or footer).
+ let numbering_marginal = numbering.as_ref().map(|numbering| {
+ let both = match numbering {
+ Numbering::Pattern(pattern) => pattern.pieces() >= 2,
+ Numbering::Func(_) => true,
+ };
+
+ let mut counter = CounterDisplayElem::new(
+ Counter::new(CounterKey::Page),
+ Smart::Custom(numbering.clone()),
+ both,
+ )
+ .pack();
+
+ // We interpret the Y alignment as selecting header or footer
+ // and then ignore it for aligning the actual number.
+ if let Some(x) = number_align.x() {
+ counter = counter.aligned(x.into());
+ }
+
+ counter
+ });
+
+ let header = PageElem::header_in(styles);
+ let footer = PageElem::footer_in(styles);
+ let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) {
+ (header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None))
+ } else {
+ (header.as_ref().unwrap_or(&None), footer.as_ref().unwrap_or(&numbering_marginal))
+ };
+
+ // Layout the children.
+ let area = size - margin.sum_by_axis();
+ let fragment = layout_flow(
+ &mut engine,
+ children,
+ &mut locator,
+ styles,
+ Regions::repeat(area, area.map(Abs::is_finite)),
+ PageElem::columns_in(styles),
+ ColumnsElem::gutter_in(styles),
+ true,
+ )?;
+
+ // Layouts a single marginal.
+ let mut layout_marginal = |content: &Option<Content>, area, align| {
+ let Some(content) = content else { return Ok(None) };
+ let aligned = content.clone().styled(AlignElem::set_alignment(align));
+ crate::layout_frame(
+ &mut engine,
+ &aligned,
+ locator.next(&content.span()),
+ styles,
+ Region::new(area, Axes::splat(true)),
+ )
+ .map(Some)
+ };
+
+ // Layout marginals.
+ let mut layouted = Vec::with_capacity(fragment.len());
+ for inner in fragment {
+ let header_size = Size::new(inner.width(), margin.top - header_ascent);
+ let footer_size = Size::new(inner.width(), margin.bottom - footer_descent);
+ let full_size = inner.size() + margin.sum_by_axis();
+ let mid = HAlignment::Center + VAlignment::Horizon;
+ layouted.push(LayoutedPage {
+ inner,
+ fill: fill.clone(),
+ numbering: numbering.clone(),
+ header: layout_marginal(header, header_size, Alignment::BOTTOM)?,
+ footer: layout_marginal(footer, footer_size, Alignment::TOP)?,
+ background: layout_marginal(background, full_size, mid)?,
+ foreground: layout_marginal(foreground, full_size, mid)?,
+ margin,
+ binding,
+ two_sided,
+ });
+ }
+
+ Ok(layouted)
+}
+
+/// Determines the styles used for a page run itself and page-level content like
+/// marginals and footnotes.
+///
+/// As a base, we collect the styles that are shared by all elements on the page
+/// run. As a fallback if there are no elements, we use the styles active at the
+/// pagebreak that introduced the page (at the very start, we use the default
+/// styles). Then, to produce our page styles, we filter this list of styles
+/// according to a few rules:
+///
+/// - Other styles are only kept if they are `outside && (initial || liftable)`.
+/// - "Outside" means they were not produced within a show rule or that the
+/// show rule "broke free" to the page level by emitting page styles.
+/// - "Initial" means they were active at the pagebreak that introduced the
+/// page. Since these are intuitively already active, they should be kept even
+/// if not liftable. (E.g. `text(red, page(..)`) makes the footer red.)
+/// - "Liftable" means they can be lifted to the page-level even though they
+/// weren't yet active at the very beginning. Set rule styles are liftable as
+/// opposed to direct constructor calls:
+/// - For `set page(..); set text(red)` the red text is kept even though it
+/// comes after the weak pagebreak from set page.
+/// - For `set page(..); text(red)[..]` the red isn't kept because the
+/// constructor styles are not liftable.
+fn determine_page_styles(children: &[Pair], initial: StyleChain) -> Styles {
+ // Determine the shared styles (excluding tags).
+ let tagless = children.iter().filter(|(c, _)| !c.is::<TagElem>()).map(|&(_, s)| s);
+ let base = StyleChain::trunk(tagless).unwrap_or(initial).to_map();
+
+ // Determine the initial styles that are also shared by everything. We can't
+ // use `StyleChain::trunk` because it currently doesn't deal with partially
+ // shared links (where a subslice matches).
+ let trunk_len = initial
+ .to_map()
+ .as_slice()
+ .iter()
+ .zip(base.as_slice())
+ .take_while(|&(a, b)| a == b)
+ .count();
+
+ // Filter the base styles according to our rules.
+ base.into_iter()
+ .enumerate()
+ .filter(|(i, style)| {
+ let initial = *i < trunk_len;
+ style.outside() && (initial || style.liftable())
+ })
+ .map(|(_, style)| style)
+ .collect()
+}