use crate::context::Context;
use crate::hbox::{HorzBox, PureHorzBox, FORCED_BREAK_PENALTY};
use crate::length::Length;
use crate::vbox::VertBox;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BreakKind {
Allowed,
Mandatory,
}
pub fn break_opportunities(text: &str) -> Vec<(usize, BreakKind)> {
unicode_linebreak::linebreaks(text)
.map(|(i, opp)| {
let kind = match opp {
unicode_linebreak::BreakOpportunity::Mandatory => BreakKind::Mandatory,
unicode_linebreak::BreakOpportunity::Allowed => BreakKind::Allowed,
};
(i, kind)
})
.collect()
}
const LINE_PENALTY: f64 = 10.0;
const RATIO_STRETCH_LIMIT: f64 = 2.0;
const RATIO_SHRINK_LIMIT: f64 = -1.0;
const BADNESS_TOO_LONG: f64 = 100_000.0;
const BADNESS_DROPPED: f64 = 1.0e12;
struct LineMetrics {
natural: Length,
stretch: Length,
shrink: Length,
has_fil: bool,
cjk_natural: Length,
has_glue: bool,
}
impl LineMetrics {
fn empty() -> LineMetrics {
LineMetrics {
natural: Length::ZERO,
stretch: Length::ZERO,
shrink: Length::ZERO,
has_fil: false,
has_glue: false,
cjk_natural: Length::ZERO,
}
}
fn push(&mut self, bx: &PureHorzBox) {
match bx {
PureHorzBox::InnerString { width, text, .. } => {
self.natural += *width;
if text.chars().any(is_cjk) {
self.cjk_natural += *width;
}
}
PureHorzBox::OuterEmpty {
natural: n,
shrinkable,
stretchable,
} => {
self.natural += *n;
self.stretch += *stretchable;
self.shrink += *shrinkable;
self.has_glue = true;
}
PureHorzBox::OuterFil => self.has_fil = true,
PureHorzBox::FixedEmpty { width } => self.natural += *width,
PureHorzBox::Image { width, .. } => self.natural += *width,
PureHorzBox::Discretionary { no_break, .. } => {
for b in no_break {
self.natural += b.natural_width();
}
}
PureHorzBox::Graphics { width, .. } => self.natural += *width,
PureHorzBox::GraphicsOuter { .. } => self.has_fil = true,
PureHorzBox::Math { width, .. } => self.natural += *width,
PureHorzBox::HookPageBreak { .. } => {}
PureHorzBox::Tabular(tab) => self.natural += tab.width,
PureHorzBox::EmbeddedBlock { width, .. } => self.natural += *width,
PureHorzBox::Frame { width, .. } => self.natural += *width,
PureHorzBox::FrameMarker { .. } => {}
PureHorzBox::InlineFrameMarker { .. } => {}
PureHorzBox::Footnote { .. } => {}
PureHorzBox::InlineMark(_) => {}
}
}
}
fn measure(line: &[PureHorzBox]) -> LineMetrics {
let mut m = LineMetrics::empty();
for bx in line {
m.push(bx);
}
m
}
fn measure_range(pure: &[PureHorzBox], start: usize, raw_end: usize) -> LineMetrics {
let mut m = LineMetrics::empty();
if start > 0 {
if let PureHorzBox::Discretionary { post_break, .. } = &pure[start - 1] {
for b in post_break {
m.push(b);
}
}
}
for bx in trim_trailing_glue(trim_leading_glue(&pure[start..raw_end])) {
if let PureHorzBox::Discretionary { no_break, .. } = bx {
for b in no_break {
m.push(b);
}
} else {
m.push(bx);
}
}
if raw_end < pure.len() {
if let PureHorzBox::Discretionary { pre_break, .. } = &pure[raw_end] {
for b in pre_break {
m.push(b);
}
}
}
m
}
fn is_cjk(c: char) -> bool {
matches!(c,
'\u{3000}'..='\u{303F}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{FF00}'..='\u{FFEF}' )
}
pub fn natural_metrics(boxes: &[HorzBox]) -> (Length, Length, Length) {
fn go<'a>(
pure: impl IntoIterator<Item = &'a PureHorzBox>,
width: &mut Length,
height: &mut Length,
depth: &mut Length,
) {
for bx in pure {
match bx {
PureHorzBox::InnerString {
width: w,
height: h,
depth: d,
..
} => {
*width += *w;
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::OuterEmpty { natural, .. } => *width += *natural,
PureHorzBox::OuterFil => {}
PureHorzBox::FixedEmpty { width: w } => *width += *w,
PureHorzBox::Image { width: w, height: h, .. } => {
*width += *w;
*height = (*height).max(*h);
}
PureHorzBox::Discretionary { no_break, .. } => go(no_break, width, height, depth),
PureHorzBox::Graphics {
width: w,
height: h,
depth: d,
..
} => {
*width += *w;
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::Math {
width: w,
height: h,
depth: d,
..
} => {
*width += *w;
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::GraphicsOuter { height: h, depth: d, .. } => {
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::HookPageBreak { .. } => {}
PureHorzBox::Tabular(tab) => {
*width += tab.width;
*height = (*height).max(tab.height);
*depth = (*depth).max(tab.depth);
}
PureHorzBox::EmbeddedBlock {
width: w,
height: h,
depth: d,
..
} => {
*width += *w;
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::Frame {
width: w,
height: h,
depth: d,
..
} => {
*width += *w;
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::FrameMarker { .. } => {}
PureHorzBox::InlineFrameMarker { height: h, depth: d, .. } => {
*height = (*height).max(*h);
*depth = (*depth).max(*d);
}
PureHorzBox::Footnote { .. } => {}
PureHorzBox::InlineMark(_) => {}
}
}
}
let mut width = Length::ZERO;
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
go(
boxes.iter().map(|HorzBox::Pure(p)| p),
&mut width,
&mut height,
&mut depth,
);
(width, height, depth)
}
pub fn measure_block(block: &[VertBox]) -> (Length, Length) {
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
for vb in block {
match vb {
VertBox::Line { height: h, depth: d, .. } => {
height += *h;
depth += *d;
}
VertBox::Skip(s) | VertBox::ParagTop(s) | VertBox::FramePad(s) => height += *s,
VertBox::ClearPage
| VertBox::HookPageBreak(_)
| VertBox::FrameStart(_)
| VertBox::FrameEnd(_)
| VertBox::ListMark(_) => {}
}
}
(height, depth)
}
fn badness(width: Length, metrics: &LineMetrics) -> f64 {
let slack = width - metrics.natural;
if slack.0.abs() < 1e-9 {
return 0.0;
}
if slack.is_positive() {
if metrics.has_fil {
return 0.0;
}
if metrics.stretch.is_positive() {
let ratio = slack / metrics.stretch;
if ratio <= RATIO_STRETCH_LIMIT {
return (10000.0 * ratio.abs().powi(3)).min(BADNESS_TOO_LONG);
}
return BADNESS_DROPPED;
}
BADNESS_DROPPED
} else {
if metrics.shrink.is_positive() {
let ratio = slack / metrics.shrink;
if ratio < RATIO_SHRINK_LIMIT {
too_long_badness(slack, width)
} else {
(10000.0 * ratio.abs().powi(3)).min(BADNESS_TOO_LONG)
}
} else if metrics.has_glue {
too_long_badness(slack, width)
} else {
too_long_badness(slack, width)
}
}
}
fn carries_ink(b: &PureHorzBox) -> bool {
!matches!(
b,
PureHorzBox::OuterEmpty { .. }
| PureHorzBox::OuterFil
| PureHorzBox::FixedEmpty { .. }
| PureHorzBox::FrameMarker { .. }
| PureHorzBox::InlineFrameMarker { .. }
| PureHorzBox::HookPageBreak { .. }
| PureHorzBox::Discretionary { .. }
)
}
fn is_too_long(width: Length, m: &LineMetrics) -> bool {
let slack = width - m.natural;
if slack.0 >= 0.0 {
return false;
}
if m.shrink.is_positive() {
(slack / m.shrink) < RATIO_SHRINK_LIMIT
} else {
true
}
}
fn too_long_badness(slack: Length, width: Length) -> f64 {
let overflow = -slack.0;
BADNESS_TOO_LONG * (1.0 + (overflow / width.0).max(0.0))
}
fn demerits(b: f64, penalty: i32) -> f64 {
let base = LINE_PENALTY + b;
if penalty <= FORCED_BREAK_PENALTY {
base
} else {
(base + penalty as f64).max(0.0)
}
}
pub fn break_into_lines(ctx: &Context, boxes: Vec<HorzBox>) -> Vec<VertBox> {
let pure: Vec<PureHorzBox> = boxes.into_iter().map(|HorzBox::Pure(p)| p).collect();
let width = ctx.paragraph_width;
let n = pure.len();
if n == 0 {
return Vec::new();
}
{
let whole = line_content(&pure, 0, n);
let wm = measure(&whole);
let has_forced_break = pure.iter().any(PureHorzBox::is_forced_break);
if !has_forced_break && wm.natural <= width && badness(width, &wm) >= BADNESS_DROPPED {
return vec![layout_line(ctx, whole, width, true)];
}
}
let mut starts: Vec<usize> = vec![0];
let mut ends: Vec<usize> = Vec::new();
let last_ink = pure.iter().rposition(carries_ink).unwrap_or(0);
for g in 1..n {
let is_disc =
matches!(pure[g], PureHorzBox::Discretionary { .. }) && pure[g].is_break_point();
if g > last_ink {
continue;
}
if (pure[g].is_break_point() && !pure[g - 1].is_break_point()) || is_disc {
ends.push(g);
starts.push(g + 1);
}
}
ends.push(n);
let m = ends.len();
const EPS: f64 = 1e-6;
let mut dp: Vec<(f64, usize)> = vec![(f64::INFINITY, usize::MAX); m + 1];
let mut back: Vec<usize> = vec![usize::MAX; m + 1];
dp[0] = (0.0, 0);
let mut floor: usize = 0;
let mut spent_overfull: Vec<bool> = vec![false; m + 1];
for j in 1..=m {
let raw_end = ends[j - 1];
let penalty = if raw_end < n {
pure[raw_end].break_penalty()
} else {
0
};
for i in (floor..j).rev() {
if dp[i].0.is_infinite() {
continue;
}
let start = starts[i];
if start > raw_end {
continue;
}
let mut metrics = measure_range(&pure, start, raw_end);
if raw_end < n {
if let PureHorzBox::Discretionary { pre_break, .. } = &pure[raw_end] {
for b in pre_break {
metrics.natural += b.natural_width();
}
}
}
if is_too_long(width, &metrics) {
if spent_overfull[i] {
continue; }
spent_overfull[i] = true;
}
let b = badness(width, &metrics);
let d = demerits(b, penalty);
let cand_cost = dp[i].0 + d;
let cand_lines = dp[i].1 + 1;
if cand_cost < dp[j].0 - EPS
|| ((cand_cost - dp[j].0).abs() <= EPS && cand_lines < dp[j].1)
{
dp[j] = (cand_cost, cand_lines);
back[j] = i;
}
if i + 1 != j && metrics.natural.0 > width.0 * 4.0 + 1.0 {
break;
}
}
if raw_end < n && pure[raw_end].is_forced_break() {
floor = j;
}
}
let mut line_ranges: Vec<(usize, usize)> = Vec::new();
let mut j = m;
while j > 0 {
let i = back[j];
debug_assert_ne!(i, usize::MAX, "no path found to breakpoint {j}");
line_ranges.push((starts[i], ends[j - 1]));
j = i;
}
line_ranges.reverse();
let line_count = line_ranges.len();
line_ranges
.into_iter()
.enumerate()
.flat_map(|(idx, (start, raw_end))| {
let content = line_content(&pure, start, raw_end);
if let Some(block) = sole_breakable_block(&content) {
return block;
}
vec![layout_line(ctx, content, width, idx + 1 == line_count)]
})
.collect()
}
fn trim_trailing_glue(line: &[PureHorzBox]) -> &[PureHorzBox] {
let mut end = line.len();
while end > 0 {
match &line[end - 1] {
PureHorzBox::OuterEmpty { .. } => end -= 1,
b @ PureHorzBox::Discretionary { .. } if b.is_break_point() => end -= 1,
_ => break,
}
}
&line[..end]
}
fn trim_leading_glue(line: &[PureHorzBox]) -> &[PureHorzBox] {
let mut start = 0;
while start < line.len()
&& match &line[start] {
PureHorzBox::OuterEmpty { .. } => true,
b @ PureHorzBox::Discretionary { .. } => b.is_break_point(),
_ => false,
}
{
start += 1;
}
&line[start..]
}
fn line_content(pure: &[PureHorzBox], start: usize, raw_end: usize) -> Vec<PureHorzBox> {
let mut out = Vec::new();
if start > 0 {
if let PureHorzBox::Discretionary { post_break, .. } = &pure[start - 1] {
out.extend(post_break.iter().cloned());
}
}
for bx in trim_trailing_glue(trim_leading_glue(&pure[start..raw_end])) {
if let PureHorzBox::Discretionary { no_break, .. } = bx {
out.extend(no_break.iter().cloned());
} else {
out.push(bx.clone());
}
}
if raw_end < pure.len() {
if let PureHorzBox::Discretionary { pre_break, .. } = &pure[raw_end] {
out.extend(pre_break.iter().cloned());
}
}
out
}
fn sole_breakable_block(content: &[PureHorzBox]) -> Option<Vec<VertBox>> {
let mut found: Option<&Vec<VertBox>> = None;
for bx in content {
match bx {
PureHorzBox::EmbeddedBlock {
block,
breakable: true,
..
} => {
if found.is_some() {
return None; }
found = Some(block);
}
PureHorzBox::FrameMarker { .. }
| PureHorzBox::InlineFrameMarker { .. }
| PureHorzBox::HookPageBreak { .. }
| PureHorzBox::OuterEmpty { .. }
| PureHorzBox::OuterFil
| PureHorzBox::FixedEmpty { .. } => {}
_ => return None,
}
}
found.cloned()
}
fn layout_line(ctx: &Context, line: Vec<PureHorzBox>, width: Length, is_last: bool) -> VertBox {
let (contents, height, depth) = justify_line(line, width, is_last);
VertBox::Line {
height,
depth,
leading: ctx.leading,
contents,
}
}
pub fn fit_cell(content: Vec<HorzBox>, width: Length) -> (Vec<(Length, PureHorzBox)>, Length, Length) {
let (_, height, depth) = natural_metrics(&content);
let pure: Vec<PureHorzBox> = content.into_iter().map(|HorzBox::Pure(p)| p).collect();
let (contents, _, _) = justify_line(pure, width, false);
(contents, height, depth)
}
fn justify_line(
line: Vec<PureHorzBox>,
width: Length,
is_last: bool,
) -> (Vec<(Length, PureHorzBox)>, Length, Length) {
let natural: Length = line
.iter()
.map(|b| b.natural_width())
.fold(Length::ZERO, |acc, w| acc + w);
let slack = width - natural;
let fil_count = line
.iter()
.filter(|b| matches!(b, PureHorzBox::OuterFil | PureHorzBox::GraphicsOuter { .. }))
.count();
let stretch_total: Length = line
.iter()
.map(|b| match b {
PureHorzBox::OuterEmpty { stretchable, .. } => *stretchable,
_ => Length::ZERO,
})
.fold(Length::ZERO, |acc, w| acc + w);
let shrink_total: Length = line
.iter()
.map(|b| match b {
PureHorzBox::OuterEmpty { shrinkable, .. } => *shrinkable,
_ => Length::ZERO,
})
.fold(Length::ZERO, |acc, w| acc + w);
let shrink_ratio = if slack.is_positive() || !shrink_total.is_positive() {
0.0
} else {
(slack / shrink_total).max(-1.0)
};
let mut x = Length::ZERO;
let mut contents = Vec::with_capacity(line.len());
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
for mut bx in line {
let advance = match &mut bx {
PureHorzBox::InnerString {
width,
height: h,
depth: d,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
*width
}
PureHorzBox::OuterEmpty {
natural,
shrinkable,
stretchable,
} => {
let mut adv = *natural;
if slack.is_positive() {
if fil_count == 0 && stretch_total.is_positive() && !is_last {
adv += slack * (*stretchable / stretch_total);
}
} else if shrink_ratio != 0.0 {
adv += *shrinkable * shrink_ratio;
}
adv
}
PureHorzBox::OuterFil => {
if fil_count > 0 && slack.is_positive() {
slack * (1.0 / fil_count as f64)
} else {
Length::ZERO
}
}
PureHorzBox::FixedEmpty { width } => *width,
PureHorzBox::Image { width, height: h, .. } => {
height = height.max(*h);
depth = depth.max(Length::ZERO);
*width
}
PureHorzBox::Discretionary { .. } => Length::ZERO,
PureHorzBox::Graphics {
width,
height: h,
depth: d,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
*width
}
PureHorzBox::GraphicsOuter {
height: h,
depth: d,
width: w,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
let adv = if fil_count > 0 && slack.is_positive() {
slack * (1.0 / fil_count as f64)
} else {
Length::ZERO
};
*w = adv;
adv
}
PureHorzBox::Math {
width,
height: h,
depth: d,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
*width
}
PureHorzBox::HookPageBreak { .. } => Length::ZERO,
PureHorzBox::Tabular(tab) => {
height = height.max(tab.height);
depth = depth.max(tab.depth);
tab.width
}
PureHorzBox::EmbeddedBlock {
width,
height: h,
depth: d,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
*width
}
PureHorzBox::Frame {
width,
height: h,
depth: d,
..
} => {
height = height.max(*h);
depth = depth.max(*d);
*width
}
PureHorzBox::FrameMarker { .. } => Length::ZERO,
PureHorzBox::InlineFrameMarker { height: h, depth: d, .. } => {
height = height.max(*h);
depth = depth.max(*d);
Length::ZERO
}
PureHorzBox::Footnote { .. } => Length::ZERO,
PureHorzBox::InlineMark(_) => Length::ZERO,
};
contents.push((x, bx));
x += advance;
}
(contents, height, depth)
}