use std::num::NonZeroUsize;
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart};
use crate::introspection::{
Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator,
SplitLocator, Tag,
};
use crate::layout::{
layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame,
FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
};
use crate::model::{
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
};
use crate::syntax::Span;
use crate::utils::NonZeroExt;
pub fn compose(
engine: &mut Engine,
work: &mut Work,
config: &Config,
locator: Locator,
regions: Regions,
) -> SourceResult<Frame> {
Composer {
engine,
config,
page_base: regions.base(),
column: 0,
page_insertions: Insertions::default(),
column_insertions: Insertions::default(),
work,
footnote_spill: None,
footnote_queue: vec![],
}
.page(locator, regions)
}
pub struct Composer<'a, 'b, 'x, 'y> {
pub engine: &'x mut Engine<'y>,
pub work: &'x mut Work<'a, 'b>,
pub config: &'x Config<'x>,
column: usize,
page_base: Size,
page_insertions: Insertions<'a, 'b>,
column_insertions: Insertions<'a, 'b>,
footnote_spill: Option<std::vec::IntoIter<Frame>>,
footnote_queue: Vec<Packed<FootnoteElem>>,
}
impl<'a, 'b> Composer<'a, 'b, '_, '_> {
fn page(mut self, locator: Locator, regions: Regions) -> SourceResult<Frame> {
let checkpoint = self.work.clone();
let output = loop {
let mut pod = regions;
pod.size.y -= self.page_insertions.height();
match self.page_contents(locator.relayout(), pod) {
Ok(frame) => break frame,
Err(Stop::Finish(_)) => unreachable!(),
Err(Stop::Relayout(PlacementScope::Column)) => unreachable!(),
Err(Stop::Relayout(PlacementScope::Parent)) => {
*self.work = checkpoint.clone();
continue;
}
Err(Stop::Error(err)) => return Err(err),
};
};
drop(checkpoint);
Ok(self.page_insertions.finalize(self.work, self.config, output))
}
fn page_contents(&mut self, locator: Locator, regions: Regions) -> FlowResult<Frame> {
if self.config.columns.count == 1 {
return self.column(locator, regions);
}
let column_height = regions.size.y;
let backlog: Vec<_> = std::iter::once(&column_height)
.chain(regions.backlog)
.flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count))
.skip(1)
.collect();
let mut inner = Regions {
size: Size::new(self.config.columns.width, column_height),
backlog: &backlog,
expand: Axes::new(true, regions.expand.y),
..regions
};
let size = Size::new(
regions.size.x,
if regions.expand.y { regions.size.y } else { Abs::zero() },
);
let mut output = Frame::hard(size);
let mut offset = Abs::zero();
let mut locator = locator.split();
for i in 0..self.config.columns.count {
self.column = i;
let frame = self.column(locator.next(&()), inner)?;
if !regions.expand.y {
output.size_mut().y.set_max(frame.height());
}
let width = frame.width();
let x = if self.config.columns.dir == Dir::LTR {
offset
} else {
regions.size.x - offset - width
};
offset += width + self.config.columns.gutter;
output.push_frame(Point::with_x(x), frame);
inner.next();
}
Ok(output)
}
fn column(&mut self, locator: Locator, regions: Regions) -> FlowResult<Frame> {
self.column_insertions = Insertions::default();
if let Some(spill) = self.work.footnote_spill.take() {
self.footnote_spill(spill, regions.base())?;
}
let checkpoint = self.work.clone();
let inner = loop {
let mut pod = regions;
pod.size.y -= self.column_insertions.height();
match self.column_contents(pod) {
Ok(frame) => break frame,
Err(Stop::Finish(_)) => unreachable!(),
Err(Stop::Relayout(PlacementScope::Column)) => {
*self.work = checkpoint.clone();
continue;
}
err => return err,
}
};
drop(checkpoint);
self.work.footnotes.extend(self.footnote_queue.drain(..));
if let Some(spill) = self.footnote_spill.take() {
self.work.footnote_spill = Some(spill);
}
let insertions = std::mem::take(&mut self.column_insertions);
let mut output = insertions.finalize(self.work, self.config, inner);
if let Some(line_config) = &self.config.line_numbers {
layout_line_numbers(
self.engine,
self.config,
line_config,
locator,
self.column,
&mut output,
)?;
}
Ok(output)
}
fn column_contents(&mut self, regions: Regions) -> FlowResult<Frame> {
for note in std::mem::take(&mut self.work.footnotes) {
self.footnote(note, &mut regions.clone(), Abs::zero(), false)?;
}
for placed in std::mem::take(&mut self.work.floats) {
self.float(placed, ®ions, false)?;
}
distribute(self, regions)
}
pub fn float(
&mut self,
placed: &'b PlacedChild<'a>,
regions: &Regions,
clearance: bool,
) -> FlowResult<()> {
let loc = placed.location();
if self.skipped(loc) {
return Ok(());
}
if !self.work.floats.is_empty() {
self.work.floats.push(placed);
return Ok(());
}
let base = match placed.scope {
PlacementScope::Column => regions.base(),
PlacementScope::Parent => self.page_base,
};
let frame = placed.layout(self.engine, base)?;
let remaining = match placed.scope {
PlacementScope::Column => regions.size.y,
PlacementScope::Parent => {
let remaining: Abs = regions
.iter()
.map(|size| size.y)
.take(self.config.columns.count - self.column)
.sum();
remaining / self.config.columns.count as f64
}
};
let clearance = if clearance { placed.clearance } else { Abs::zero() };
let need = frame.height() + clearance;
if !remaining.fits(need) && regions.may_progress() {
self.work.floats.push(placed);
return Ok(());
}
self.footnotes(regions, &frame, need, false)?;
let align_y = placed.align_y.map(Option::unwrap).unwrap_or_else(|| {
let used = base.y - remaining;
let half = need / 2.0;
let ratio = (used + half) / base.y;
if ratio <= 0.5 {
FixedAlignment::Start
} else {
FixedAlignment::End
}
});
let area = match placed.scope {
PlacementScope::Column => &mut self.column_insertions,
PlacementScope::Parent => &mut self.page_insertions,
};
area.push_float(placed, frame, align_y);
area.skips.push(loc);
Err(Stop::Relayout(placed.scope))
}
pub fn footnotes(
&mut self,
regions: &Regions,
frame: &Frame,
flow_need: Abs,
breakable: bool,
) -> FlowResult<()> {
if !self.config.root {
return Ok(());
}
let mut notes = vec![];
for tag in &self.work.tags {
let Tag::Start(elem) = tag else { continue };
let Some(note) = elem.to_packed::<FootnoteElem>() else { continue };
notes.push((Abs::zero(), note.clone()));
}
find_in_frame_impl::<FootnoteElem>(&mut notes, frame, Abs::zero());
if notes.is_empty() {
return Ok(());
}
let mut relayout = false;
let mut regions = *regions;
let mut migratable = !breakable && regions.may_progress();
for (y, elem) in notes {
let flow_need = if breakable { y } else { flow_need };
match self.footnote(elem, &mut regions, flow_need, migratable) {
Ok(()) => {}
Err(Stop::Relayout(_)) => relayout = true,
err => return err,
}
migratable = false;
}
if relayout {
return Err(Stop::Relayout(PlacementScope::Column));
}
Ok(())
}
fn footnote(
&mut self,
elem: Packed<FootnoteElem>,
regions: &mut Regions,
flow_need: Abs,
migratable: bool,
) -> FlowResult<()> {
let loc = elem.location().unwrap();
if elem.is_ref() || self.skipped(loc) {
return Ok(());
}
let area = &mut self.column_insertions;
if self.footnote_spill.is_some() || !self.footnote_queue.is_empty() {
self.footnote_queue.push(elem);
return Ok(());
}
let mut separator = None;
let mut separator_need = Abs::zero();
if area.footnotes.is_empty() {
let frame =
layout_footnote_separator(self.engine, self.config, regions.base())?;
separator_need += self.config.footnote.clearance + frame.height();
separator = Some(frame);
}
let mut pod = *regions;
pod.expand.y = false;
pod.size.y -= flow_need + separator_need + self.config.footnote.gap;
let frames = layout_footnote(self.engine, self.config, &elem, pod)?.into_frames();
let nested = find_in_frames::<FootnoteElem>(&frames);
let mut iter = frames.into_iter();
let first = iter.next().unwrap();
let note_need = self.config.footnote.gap + first.height();
if first.is_empty() {
if migratable {
return Err(Stop::Finish(false));
} else {
self.footnote_queue.push(elem);
return Ok(());
}
}
if let Some(frame) = separator {
area.push_footnote_separator(self.config, frame);
regions.size.y -= separator_need;
}
area.push_footnote(self.config, first);
area.skips.push(loc);
regions.size.y -= note_need;
if !iter.as_slice().is_empty() {
self.footnote_spill = Some(iter);
}
for (_, note) in nested {
self.footnote(note, regions, flow_need, migratable)?;
}
Err(Stop::Relayout(PlacementScope::Column))
}
fn footnote_spill(
&mut self,
mut iter: std::vec::IntoIter<Frame>,
base: Size,
) -> SourceResult<()> {
let area = &mut self.column_insertions;
let separator = layout_footnote_separator(self.engine, self.config, base)?;
area.push_footnote_separator(self.config, separator);
let frame = iter.next().unwrap();
area.push_footnote(self.config, frame);
if !iter.as_slice().is_empty() {
self.footnote_spill = Some(iter);
}
Ok(())
}
fn skipped(&self, loc: Location) -> bool {
self.work.skips.contains(&loc)
|| self.page_insertions.skips.contains(&loc)
|| self.column_insertions.skips.contains(&loc)
}
pub fn insertion_width(&self) -> Abs {
self.column_insertions.width.max(self.page_insertions.width)
}
}
fn layout_footnote_separator(
engine: &mut Engine,
config: &Config,
base: Size,
) -> SourceResult<Frame> {
layout_frame(
engine,
&config.footnote.separator,
Locator::root(),
config.shared,
Region::new(base, Axes::new(config.footnote.expand, false)),
)
}
fn layout_footnote(
engine: &mut Engine,
config: &Config,
elem: &Packed<FootnoteElem>,
pod: Regions,
) -> SourceResult<Fragment> {
let loc = elem.location().unwrap();
layout_fragment(
engine,
&FootnoteEntry::new(elem.clone()).pack(),
Locator::synthesize(loc),
config.shared,
pod,
)
.map(|mut fragment| {
for frame in &mut fragment {
frame.set_parent(loc);
}
fragment
})
}
#[derive(Default)]
struct Insertions<'a, 'b> {
top_floats: Vec<(&'b PlacedChild<'a>, Frame)>,
bottom_floats: Vec<(&'b PlacedChild<'a>, Frame)>,
footnotes: Vec<Frame>,
footnote_separator: Option<Frame>,
top_size: Abs,
bottom_size: Abs,
width: Abs,
skips: Vec<Location>,
}
impl<'a, 'b> Insertions<'a, 'b> {
fn push_float(
&mut self,
placed: &'b PlacedChild<'a>,
frame: Frame,
align_y: FixedAlignment,
) {
self.width.set_max(frame.width());
let amount = frame.height() + placed.clearance;
let pair = (placed, frame);
if align_y == FixedAlignment::Start {
self.top_size += amount;
self.top_floats.push(pair);
} else {
self.bottom_size += amount;
self.bottom_floats.push(pair);
}
}
fn push_footnote(&mut self, config: &Config, frame: Frame) {
self.width.set_max(frame.width());
self.bottom_size += config.footnote.gap + frame.height();
self.footnotes.push(frame);
}
fn push_footnote_separator(&mut self, config: &Config, frame: Frame) {
self.width.set_max(frame.width());
self.bottom_size += config.footnote.clearance + frame.height();
self.footnote_separator = Some(frame);
}
fn height(&self) -> Abs {
self.top_size + self.bottom_size
}
fn finalize(self, work: &mut Work, config: &Config, inner: Frame) -> Frame {
work.extend_skips(&self.skips);
if self.top_floats.is_empty()
&& self.bottom_floats.is_empty()
&& self.footnote_separator.is_none()
&& self.footnotes.is_empty()
{
return inner;
}
let size = inner.size() + Size::with_y(self.height());
let mut output = Frame::soft(size);
let mut offset_top = Abs::zero();
let mut offset_bottom = size.y - self.bottom_size;
for (placed, frame) in self.top_floats {
let x = placed.align_x.position(size.x - frame.width());
let y = offset_top;
let delta = placed.delta.zip_map(size, Rel::relative_to).to_point();
offset_top += frame.height() + placed.clearance;
output.push_frame(Point::new(x, y) + delta, frame);
}
output.push_frame(Point::with_y(self.top_size), inner);
for (placed, frame) in self.bottom_floats {
offset_bottom += placed.clearance;
let x = placed.align_x.position(size.x - frame.width());
let y = offset_bottom;
let delta = placed.delta.zip_map(size, Rel::relative_to).to_point();
offset_bottom += frame.height();
output.push_frame(Point::new(x, y) + delta, frame);
}
if let Some(frame) = self.footnote_separator {
offset_bottom += config.footnote.clearance;
let y = offset_bottom;
offset_bottom += frame.height();
output.push_frame(Point::with_y(y), frame);
}
for frame in self.footnotes {
offset_bottom += config.footnote.gap;
let y = offset_bottom;
offset_bottom += frame.height();
output.push_frame(Point::with_y(y), frame);
}
output
}
}
fn layout_line_numbers(
engine: &mut Engine,
config: &Config,
line_config: &LineNumberConfig,
locator: Locator,
column: usize,
output: &mut Frame,
) -> SourceResult<()> {
let mut locator = locator.split();
if column == 0 && line_config.scope == LineNumberingScope::Page {
let reset = layout_line_number_reset(engine, config, &mut locator)?;
output.push_frame(Point::zero(), reset);
}
let mut lines = find_in_frame::<ParLineMarker>(output);
if lines.is_empty() {
return Ok(());
}
lines.sort_by_key(|&(y, _)| y);
let mut max_number_width = Abs::zero();
let mut prev_bottom = None;
let mut line_numbers = vec![];
for &(y, ref marker) in &lines {
if prev_bottom.is_some_and(|bottom| y < bottom) {
continue;
}
let frame = layout_line_number(engine, config, &mut locator, &marker.numbering)?;
prev_bottom = Some(y + frame.height().max(Abs::pt(1.0)));
max_number_width.set_max(frame.width());
line_numbers.push((y, marker, frame));
}
for (y, marker, frame) in line_numbers {
let margin = {
let opposite =
config.columns.count >= 2 && column + 1 == config.columns.count;
if opposite { OuterHAlignment::End } else { marker.number_margin }
.resolve(config.shared)
};
let clearance = match marker.number_clearance {
Smart::Auto => line_config.default_clearance,
Smart::Custom(rel) => rel.resolve(config.shared),
};
let x = match margin {
FixedAlignment::Start => -max_number_width - clearance,
FixedAlignment::End => output.width() + clearance,
FixedAlignment::Center => unreachable!(),
};
let shift = {
let align = marker
.number_align
.map(|align| align.resolve(config.shared))
.unwrap_or_else(|| margin.inv());
align.position(max_number_width - frame.width())
};
let pos = Point::new(x + shift, y);
output.push_frame(pos, frame);
}
Ok(())
}
fn layout_line_number_reset(
engine: &mut Engine,
config: &Config,
locator: &mut SplitLocator,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let update = CounterUpdate::Set(CounterState::init(false));
let content = counter.update(Span::detached(), update);
layout_frame(
engine,
&content,
locator.next(&()),
config.shared,
Region::new(Axes::splat(Abs::zero()), Axes::splat(false)),
)
}
fn layout_line_number(
engine: &mut Engine,
config: &Config,
locator: &mut SplitLocator,
numbering: &Numbering,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let update = CounterUpdate::Step(NonZeroUsize::ONE);
let numbering = Smart::Custom(numbering.clone());
let content = Content::sequence(vec![
counter.clone().update(Span::detached(), update),
CounterDisplayElem::new(counter, numbering, false).pack(),
]);
let mut frame = layout_frame(
engine,
&content,
locator.next(&()),
config.shared,
Region::new(Axes::splat(Abs::inf()), Axes::splat(false)),
)?;
frame.translate(Point::with_y(-frame.baseline()));
Ok(frame)
}
fn find_in_frame<T: NativeElement>(frame: &Frame) -> Vec<(Abs, Packed<T>)> {
let mut output = vec![];
find_in_frame_impl(&mut output, frame, Abs::zero());
output
}
fn find_in_frames<T: NativeElement>(frames: &[Frame]) -> Vec<(Abs, Packed<T>)> {
let mut output = vec![];
for frame in frames {
find_in_frame_impl(&mut output, frame, Abs::zero());
}
output
}
fn find_in_frame_impl<T: NativeElement>(
output: &mut Vec<(Abs, Packed<T>)>,
frame: &Frame,
y_offset: Abs,
) {
for (pos, item) in frame.items() {
let y = y_offset + pos.y;
match item {
FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y),
FrameItem::Tag(Tag::Start(elem)) => {
if let Some(elem) = elem.to_packed::<T>() {
output.push((y, elem.clone()));
}
}
_ => {}
}
}
}