use comemo::Track;
use smallvec::smallvec;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, Resolve, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, Dir, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Size,
};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
use typst_library::pdf::PdfMarkerTag;
use typst_library::text::TextElem;
use typst_syntax::Span;
use crate::stack::{StackLayoutChild, layout_stack_internal};
#[typst_macros::time(span = elem.span())]
pub fn layout_list(
elem: &Packed<ListElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let indent = elem.indent.get(styles);
let body_indent = elem.body_indent.get(styles);
let tight = elem.tight.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
});
let is_rtl = styles.get(TextElem::dir).resolve(styles) == Dir::RTL;
let Depth(depth) = styles.get(ListElem::depth);
let marker_align = elem.marker_align.get(styles);
let baseline_align = marker_align.y().is_none();
let marker = elem
.marker
.get_ref(styles)
.resolve(engine, styles, depth)?
.aligned(marker_align);
let mut items = vec![];
for item in &elem.children {
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
let body = body.set(ListElem::depth, Depth(1));
let item = ItemContent {
marker: PdfMarkerTag::ListItemLabel(marker.clone()),
body: PdfMarkerTag::ListItemBody(body),
};
items.push(item);
}
let layouter = ListLayouter::new(
gutter,
elem.span(),
indent,
body_indent,
baseline_align,
is_rtl,
styles,
);
layout_items(layouter, items, engine, locator, styles, regions)
}
#[typst_macros::time(span = elem.span())]
pub fn layout_enum(
elem: &Packed<EnumElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let numbering = elem.numbering.get_ref(styles);
let reversed = elem.reversed.get(styles);
let indent = elem.indent.get(styles);
let body_indent = elem.body_indent.get(styles);
let tight = elem.tight.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
});
let is_rtl = styles.get(TextElem::dir).resolve(styles) == Dir::RTL;
let mut items = vec![];
let mut number = elem
.start
.get(styles)
.unwrap_or_else(|| if reversed { elem.children.len() as u64 } else { 1 });
let mut parents = styles.get_cloned(EnumElem::parents);
let full = elem.full.get(styles);
let number_align = elem.number_align.get(styles);
let baseline_align = number_align.y().is_none();
for item in &elem.children {
number = item.number.get(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full {
parents.push(number);
let content = numbering
.apply(engine, context.track(), item.span(), &parents)?
.display();
parents.pop();
content
} else {
match numbering {
Numbering::Pattern(pattern) => TextElem::packed(pattern.apply_kth(
engine,
item.span(),
parents.len(),
number,
)),
other => other
.apply(engine, context.track(), item.span(), &[number])?
.display(),
}
};
let resolved = resolved.aligned(number_align).set(TextElem::overhang, false);
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
let body = body.set(EnumElem::parents, smallvec![number]);
let item = ItemContent {
marker: PdfMarkerTag::ListItemLabel(resolved),
body: PdfMarkerTag::ListItemBody(body),
};
items.push(item);
number =
if reversed { number.saturating_sub(1) } else { number.saturating_add(1) };
}
let layouter = ListLayouter::new(
gutter,
elem.span(),
indent,
body_indent,
baseline_align,
is_rtl,
styles,
);
layout_items(layouter, items, engine, locator, styles, regions)
}
struct ItemContent {
marker: Content,
body: Content,
}
struct ListLayouter {
gutter: Length,
span: Span,
indent: Abs,
body_indent: Abs,
baseline_align: bool,
is_rtl: bool,
marker_width: Abs,
body_width: Option<Abs>,
}
impl ListLayouter {
fn new(
gutter: Length,
span: Span,
indent: Length,
body_indent: Length,
baseline_align: bool,
is_rtl: bool,
styles: StyleChain,
) -> Self {
let indent = indent.resolve(styles);
let body_indent = body_indent.resolve(styles);
Self {
gutter,
span,
indent,
body_indent,
baseline_align,
is_rtl,
marker_width: Abs::zero(),
body_width: None,
}
}
}
#[typst_macros::time(span = layouter.span)]
fn layout_items(
mut layouter: ListLayouter,
items: Vec<ItemContent>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut locator = locator.split();
let locators: Vec<_> = items
.iter()
.map(|item| {
let marker_locator = locator.next(&item.marker.span());
let body_locator = locator.next(&item.body.span());
(marker_locator, body_locator)
})
.collect();
layouter.marker_width =
measure_markers(&layouter, &items, &locators, engine, styles, regions)?;
if regions.size.x.to_raw().is_infinite() || !regions.expand.x {
layouter.body_width =
Some(measure_bodies(&layouter, &items, &locators, engine, styles, regions)?);
}
let cells =
items
.iter()
.zip(&locators)
.map(|(item, (marker_locator, body_locator))| {
StackLayoutChild::CustomLayouter(|engine, styles, regions| {
layout_item(
item,
&layouter,
engine,
marker_locator,
body_locator,
styles,
regions,
)
})
});
layout_stack_internal(
cells,
layouter.span,
Some(layouter.gutter.into()),
Dir::TTB,
engine,
locator.next(&()),
styles,
regions,
)
}
fn measure_markers<'a>(
list: &ListLayouter,
items: &[ItemContent],
locators: &[(Locator<'a>, Locator<'a>)],
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Abs> {
let available_width = regions.size.x - list.indent - list.body_indent;
let mut marker_width = Abs::zero();
for (item, (marker_locator, _)) in items.iter().zip(locators) {
let marker = crate::layout_frame(
engine,
&item.marker,
marker_locator.relayout(),
styles,
Region::new(Axes::new(available_width, Abs::inf()), Axes::splat(false)),
)?;
marker_width.set_max(marker.width());
}
Ok(marker_width.min(available_width))
}
fn measure_bodies(
list: &ListLayouter,
items: &[ItemContent],
locators: &[(Locator, Locator)],
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Abs> {
let available_width = regions.size.x - list.indent - list.body_indent;
let mut measured_body_width = Abs::zero();
for (item, (_, body_locator)) in items.iter().zip(locators) {
let body = crate::layout_frame(
engine,
&item.body,
body_locator.relayout(),
styles,
Region::new(Axes::new(available_width, Abs::inf()), Axes::splat(false)),
)?;
measured_body_width.set_max(body.width());
}
Ok(measured_body_width.min(available_width - list.marker_width))
}
#[typst_macros::time(span = item.body.span())]
fn layout_item(
item: &ItemContent,
list: &ListLayouter,
engine: &mut Engine,
marker_locator: &Locator,
body_locator: &Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut layouter =
ItemLayouter::new(item, list, marker_locator, body_locator, styles, regions);
let mut marker = layouter.layout_marker(
Region::new(
Axes::new(list.marker_width, regions.base().y),
Axes::new(true, false),
),
engine,
)?;
let mut body = layouter.layout_body(layouter.body_regions, engine)?;
if layouter.list.baseline_align {
layouter.baseline_align(&marker, &mut body, engine)?;
} else {
layouter.vertical_align(&mut marker, &body, engine)?;
};
layouter.finish(marker, body)
}
struct ItemLayouter<'a> {
item: &'a ItemContent,
list: &'a ListLayouter,
marker_locator: &'a Locator<'a>,
body_locator: &'a Locator<'a>,
styles: StyleChain<'a>,
first_frame: usize,
body_regions: Regions<'a>,
body_offset: Point,
marker_offset: Point,
}
impl<'a> ItemLayouter<'a> {
fn new(
item: &'a ItemContent,
list: &'a ListLayouter,
marker_locator: &'a Locator<'a>,
body_locator: &'a Locator<'a>,
styles: StyleChain<'a>,
regions: Regions<'a>,
) -> Self {
let total_body_indent = list.indent + list.marker_width + list.body_indent;
let mut body_regions = regions;
if let Some(body_width) = list.body_width {
body_regions.size.x = body_width;
body_regions.expand.x = true;
} else {
body_regions.size.x -= total_body_indent;
}
Self {
item,
list,
marker_locator,
body_locator,
styles,
first_frame: 0,
body_regions,
body_offset: Point::with_x(total_body_indent),
marker_offset: Point::with_x(list.indent),
}
}
fn layout_marker(&self, region: Region, engine: &mut Engine) -> SourceResult<Frame> {
crate::layout_frame(
engine,
&self.item.marker,
self.marker_locator.relayout(),
self.styles,
region,
)
}
fn layout_body(
&mut self,
regions: Regions,
engine: &mut Engine,
) -> SourceResult<Fragment> {
let fragment = crate::layout_fragment(
engine,
&self.item.body,
self.body_locator.relayout(),
self.styles,
regions,
)?;
if should_skip_first_frame(&fragment) {
self.first_frame = 1;
}
Ok(fragment)
}
fn baseline_align(
&mut self,
marker: &Frame,
body_fragment: &mut Fragment,
engine: &mut Engine,
) -> SourceResult<()> {
let diff = if marker.has_baseline()
&& let Some(first) = body_fragment.as_slice().get(self.first_frame)
&& first.has_baseline()
{
first.baseline() - marker.baseline()
} else {
Abs::zero()
};
if diff >= Abs::zero() {
self.marker_offset.y = diff;
} else {
let mut regions = self.body_regions;
regions.size.y += diff;
*body_fragment = self.layout_body(regions, engine)?;
self.body_offset.y = -diff;
};
Ok(())
}
fn vertical_align(
&mut self,
marker: &mut Frame,
body: &Fragment,
engine: &mut Engine,
) -> SourceResult<()> {
let height = if let Some(body_first) = body.as_slice().get(self.first_frame) {
body_first.height().max(marker.height())
} else {
marker.height()
};
let region =
Region::new(Axes::new(self.list.marker_width, height), Axes::splat(true));
*marker = self.layout_marker(region, engine)?;
Ok(())
}
fn finish(&self, marker: Frame, body_fragment: Fragment) -> SourceResult<Fragment> {
let mut frames = vec![];
for (i, body_frame) in body_fragment.into_iter().enumerate() {
let width = self.body_offset.x + body_frame.width();
let height = (marker.height() + self.marker_offset.y)
.max(body_frame.height() + self.body_offset.y);
let mut frame = Frame::soft(Size::new(width, height));
let mut body_pos = self.body_offset;
if self.list.is_rtl {
body_pos.x = Abs::zero();
}
if i == self.first_frame {
let mut marker_pos = self.marker_offset;
if self.list.is_rtl {
marker_pos.x = width - (self.list.marker_width + marker_pos.x);
}
frame.push_frame(marker_pos, marker.clone());
}
frame.push_frame(body_pos, body_frame);
frames.push(frame);
}
Ok(Fragment::frames(frames))
}
}
fn should_skip_first_frame(fragment: &Fragment) -> bool {
fragment.len() > 1
&& is_empty_frame(&fragment.as_slice()[0])
&& fragment.iter().skip(1).any(|f| !is_empty_frame(f))
}
fn is_empty_frame(frame: &Frame) -> bool {
frame.items().all(|(_, item)| matches!(item, FrameItem::Tag(_)))
}