use super::shared::{line_height_pt, push_warning, size_with_defaults, EPS};
use super::{LayoutCtx, LayoutResult, Layoutable};
use crate::geometry::{Constraints, Rect, Size};
use crate::render_node::RenderNode;
use crate::text::{text_width_pt, wrap_text};
use crate::warnings::{LayoutWarning, LayoutWarningKind};
use lightweight_pdf_core::{Element, Line, Overflow, Rect as RectElement, Spacer, Text, TextStyle};
const WIDOW_ORPHAN_N: usize = 2;
fn max_lines_fitting(area_height: f32, lh: f32, available: usize) -> usize {
(((area_height + EPS) / lh).floor().max(0.0) as usize).min(available)
}
fn text_lines_node(area: Rect, style: TextStyle, lines: Vec<String>, lh: f32) -> RenderNode {
let height = lines.len() as f32 * lh;
RenderNode::clipped(
area,
RenderNode::TextLines {
area: Rect { height, ..area },
style,
lines,
line_height_pt: lh,
},
)
}
impl Layoutable for Text {
fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
let width = self.common.width.unwrap_or(constraints.max_width);
let lines = wrap_text(ctx.resolver, &self.style, &self.content, width);
let lh = line_height_pt(&self.style);
let actual_width = lines
.iter()
.map(|l| text_width_pt(ctx.resolver, self.style.font, self.style.size, l))
.fold(0.0f32, f32::max);
Size {
width: self.common.width.unwrap_or(actual_width.min(width)),
height: self.common.height.unwrap_or(lines.len() as f32 * lh),
}
}
fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
let lines = wrap_text(ctx.resolver, &self.style, &self.content, area.width);
let lh = line_height_pt(&self.style);
let total_height = lines.len() as f32 * lh;
if total_height <= area.height + EPS || lines.len() <= 1 {
if total_height > area.height + EPS {
push_warning(warnings, LayoutWarningKind::TextClipped, page, text_clipped_hint(&self.content));
}
return LayoutResult::Fit(text_lines_node(area, self.style, lines, lh));
}
if self.common.height.is_some() {
return LayoutResult::Fit(layout_text_fixed_overflow(self, ctx, area, lines, lh, warnings, page));
}
let max_lines_by_height = max_lines_fitting(area.height, lh, lines.len());
let mut split_at = max_lines_by_height;
if lines.len() < 2 * WIDOW_ORPHAN_N {
split_at = 0;
} else if split_at < WIDOW_ORPHAN_N {
split_at = 0;
} else if lines.len() - split_at < WIDOW_ORPHAN_N {
let adjusted = lines.len().saturating_sub(WIDOW_ORPHAN_N);
split_at = if adjusted >= WIDOW_ORPHAN_N { adjusted } else { 0 };
}
if split_at == 0 {
return LayoutResult::Split {
current: RenderNode::Empty,
remainder: Element::Text(self.clone()),
};
}
let (current_lines, remainder_lines) = lines.split_at(split_at);
let current = text_lines_node(
Rect {
height: current_lines.len() as f32 * lh,
..area
},
self.style,
current_lines.to_vec(),
lh,
);
let remainder_text = remainder_lines.join(" ");
let mut remainder = self.clone();
remainder.content = remainder_text;
LayoutResult::Split {
current,
remainder: Element::Text(remainder),
}
}
}
fn layout_text_fixed_overflow(
text: &Text,
ctx: &LayoutCtx,
area: Rect,
lines: Vec<String>,
lh: f32,
warnings: &mut Vec<LayoutWarning>,
page: usize,
) -> RenderNode {
let max_lines = max_lines_fitting(area.height, lh, lines.len());
if max_lines >= lines.len() {
return text_lines_node(area, text.style, lines, lh);
}
push_warning(warnings, LayoutWarningKind::TextClipped, page, text_clipped_hint(&text.content));
let take = if text.common.overflow == Overflow::Ellipsis {
max_lines.max(1).min(lines.len())
} else {
max_lines
};
let mut kept: Vec<String> = lines.into_iter().take(take).collect();
if text.common.overflow == Overflow::Ellipsis {
if let Some(last) = kept.last_mut() {
*last = fit_with_ellipsis(ctx, &text.style, last, area.width);
}
}
text_lines_node(area, text.style, kept, lh)
}
fn fit_with_ellipsis(ctx: &LayoutCtx, style: &TextStyle, line: &str, max_width: f32) -> String {
let mut chars: Vec<char> = line.chars().collect();
loop {
let candidate: String = chars.iter().collect::<String>() + "…";
if text_width_pt(ctx.resolver, style.font, style.size, &candidate) <= max_width || chars.is_empty() {
return candidate;
}
chars.pop();
}
}
fn text_clipped_hint(content: &str) -> String {
format!("Text \"{}\"", truncate_hint(content))
}
fn truncate_hint(s: &str) -> String {
if s.len() > 24 {
format!("{}…", &s[..24])
} else {
s.to_string()
}
}
impl Layoutable for Spacer {
fn measure(&self, _ctx: &LayoutCtx, _constraints: Constraints) -> Size {
Size {
width: self.size,
height: self.size,
}
}
fn layout(&self, _ctx: &LayoutCtx, _area: Rect, _warnings: &mut Vec<LayoutWarning>, _page: usize) -> LayoutResult {
LayoutResult::Fit(RenderNode::Empty)
}
}
impl Layoutable for Line {
fn measure(&self, _ctx: &LayoutCtx, constraints: Constraints) -> Size {
size_with_defaults(&self.common, constraints, self.thickness)
}
fn layout(&self, _ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
if self.thickness > area.height + EPS {
push_warning(warnings, LayoutWarningKind::ContentOverflow, page, "Line");
}
let y_mid = area.y + (self.thickness / 2.0).min(area.height);
let node = RenderNode::Line {
x1: area.x,
y1: y_mid,
x2: area.x + area.width,
y2: y_mid,
thickness: self.thickness,
color: self.color,
};
LayoutResult::Fit(RenderNode::clipped(area, node))
}
}
impl Layoutable for RectElement {
fn measure(&self, _ctx: &LayoutCtx, constraints: Constraints) -> Size {
size_with_defaults(&self.common, constraints, 0.0)
}
fn layout(&self, _ctx: &LayoutCtx, area: Rect, _warnings: &mut Vec<LayoutWarning>, _page: usize) -> LayoutResult {
let node = RenderNode::Rect {
area,
background: self.common.background,
border: self.common.border,
};
LayoutResult::Fit(RenderNode::clipped(area, node))
}
}