use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use super::*;
use crate::engine::Engine;
use crate::foundations::NativeElement;
use crate::introspection::{SplitLocator, Tag};
use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use crate::model::{ParLine, ParLineMarker};
use crate::text::{Lang, TextElem};
use crate::utils::Numeric;
const SHY: char = '\u{ad}';
const HYPHEN: char = '-';
const EN_DASH: char = '–';
const EM_DASH: char = '—';
const LINE_SEPARATOR: char = '\u{2028}';
pub struct Line<'a> {
pub items: Items<'a>,
pub width: Abs,
pub justify: bool,
pub dash: Option<Dash>,
}
impl<'a> Line<'a> {
pub fn empty() -> Self {
Self {
items: Items::new(),
width: Abs::zero(),
justify: false,
dash: None,
}
}
pub fn justifiables(&self) -> usize {
let mut count = 0;
for shaped in self.items.iter().filter_map(Item::text) {
count += shaped.justifiables();
}
if self
.items
.last()
.and_then(Item::text)
.map(|s| s.cjk_justifiable_at_last())
.unwrap_or(false)
{
count -= 1;
}
count
}
pub fn stretchability(&self) -> Abs {
self.items
.iter()
.filter_map(Item::text)
.map(|s| s.stretchability())
.sum()
}
pub fn shrinkability(&self) -> Abs {
self.items
.iter()
.filter_map(Item::text)
.map(|s| s.shrinkability())
.sum()
}
pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(),
Item::Frame(frame, _) => frame.width() < Abs::zero(),
_ => false,
})
}
pub fn fr(&self) -> Fr {
self.items
.iter()
.filter_map(|item| match item {
Item::Fractional(fr, _) => Some(*fr),
_ => None,
})
.sum()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Dash {
Soft,
Hard,
Other,
}
pub fn line<'a>(
engine: &Engine,
p: &'a Preparation,
range: Range,
breakpoint: Breakpoint,
pred: Option<&Line>,
) -> Line<'a> {
let full = &p.text[range.clone()];
let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory);
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
Some(Dash::Soft)
} else if full.ends_with(HYPHEN) {
Some(Dash::Hard)
} else if full.ends_with([EN_DASH, EM_DASH]) {
Some(Dash::Other)
} else {
None
};
let trim = range.start + breakpoint.trim(full).len();
let mut items = collect_items(engine, p, range, trim);
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.fallback);
}
}
if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback);
}
}
adjust_cj_at_line_boundaries(p, full, &mut items);
let width = items.iter().map(Item::natural_width).sum();
Line { items, width, justify, dash }
}
fn collect_items<'a>(
engine: &Engine,
p: &'a Preparation,
range: Range,
trim: usize,
) -> Items<'a> {
let mut items = Items::new();
let mut fallback = None;
reorder(p, range.clone(), |subrange, rtl| {
let from = items.len();
collect_range(engine, p, subrange, trim, &mut items, &mut fallback);
if rtl {
items.reorder(from);
}
});
let prefix = items
.iter()
.take_while(|item| matches!(item, Item::Absolute(_, true)))
.count();
if prefix > 0 {
items.drain(..prefix);
}
while matches!(items.last(), Some(Item::Absolute(_, true))) {
items.pop();
}
if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback {
items.push(fallback);
}
}
items
}
fn reorder<F>(p: &Preparation, range: Range, mut f: F)
where
F: FnMut(Range, bool),
{
let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL);
return;
};
if range.is_empty() {
f(range, p.dir == Dir::RTL);
return;
}
let para = bidi
.paragraphs
.iter()
.find(|para| para.range.contains(&range.start))
.unwrap();
let (levels, runs) = bidi.visual_runs(para, range.clone());
for run in runs {
let rtl = levels[run.start].is_rtl();
f(run, rtl)
}
}
fn collect_range<'a>(
engine: &Engine,
p: &'a Preparation,
range: Range,
trim: usize,
items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>,
) {
for (subrange, item) in p.slice(range.clone()) {
let Item::Text(shaped) = item else {
items.push(item);
continue;
};
let sliced =
range.start.max(subrange.start)..range.end.min(subrange.end).min(trim);
let split = subrange.start < sliced.start || sliced.end < subrange.end;
if sliced.is_empty() {
*fallback = Some(ItemEntry::from(Item::Text(shaped.empty())));
} else if split {
let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped));
} else {
items.push(item);
}
}
}
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{
adjust_cj_at_line_start(p, items);
}
if text.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{
adjust_cj_at_line_end(p, items);
}
}
fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let Some(shaped) = items.first_text_mut() else { return };
let Some(glyph) = shaped.glyphs.first() else { return };
if glyph.is_cjk_right_aligned_punctuation() {
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() {
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let shrink = glyph.x_offset;
glyph.x_advance -= shrink;
glyph.x_offset = Em::zero();
glyph.adjustability.shrinkability.0 = Em::zero();
shaped.width -= shrink.at(shaped.size);
}
}
fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let Some(shaped) = items.last_text_mut() else { return };
let Some(glyph) = shaped.glyphs.last() else { return };
let style = cjk_punct_style(shaped.lang, shaped.region);
if glyph.is_cjk_left_aligned_punctuation(style) {
let shrink = glyph.shrinkability().1;
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing
&& glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one()
{
let shrink = glyph.x_advance - glyph.x_offset - Em::one();
let glyph = shaped.glyphs.to_mut().last_mut().unwrap();
glyph.x_advance -= shrink;
glyph.adjustability.shrinkability.1 = Em::zero();
shaped.width -= shrink.at(shaped.size);
}
}
fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
if pred_line.dash != Some(Dash::Hard) {
return false;
}
let Some(Item::Text(shaped)) = pred_line.items.last() else { return false };
match shaped.lang {
Lang::LOWER_SORBIAN
| Lang::CZECH
| Lang::CROATIAN
| Lang::POLISH
| Lang::PORTUGUESE
| Lang::SLOVAK => true,
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()),
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
pub fn commit(
engine: &mut Engine,
p: &Preparation,
line: &Line,
width: Abs,
full: Abs,
shrink: bool,
locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero();
if p.dir == Dir::LTR {
offset += p.hang;
}
if let Some(Item::Text(text)) = line.items.first() {
if let Some(glyph) = text.glyphs.first() {
if !text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& (line.items.len() > 1 || text.glyphs.len() > 1)
{
let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
offset -= amount;
remaining += amount;
}
}
}
if let Some(Item::Text(text)) = line.items.last() {
if let Some(glyph) = text.glyphs.last() {
if text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& (line.items.len() > 1 || text.glyphs.len() > 1)
{
let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
remaining += amount;
}
}
}
let fr = line.fr();
let mut justification_ratio = 0.0;
let mut extra_justification = Abs::zero();
let shrinkability = line.shrinkability();
let stretchability = line.stretchability();
if remaining < Abs::zero() && shrinkability > Abs::zero() && shrink {
justification_ratio = (remaining / shrinkability).max(-1.0);
remaining = (remaining + shrinkability).min(Abs::zero());
} else if line.justify && fr.is_zero() {
if stretchability > Abs::zero() {
justification_ratio = (remaining / stretchability).min(1.0);
remaining = (remaining - stretchability).max(Abs::zero());
}
let justifiables = line.justifiables();
if justifiables > 0 && remaining > Abs::zero() {
extra_justification = remaining / justifiables as f64;
remaining = Abs::zero();
}
}
let mut top = Abs::zero();
let mut bottom = Abs::zero();
let mut frames = vec![];
for item in line.items.iter() {
let mut push = |offset: &mut Abs, frame: Frame| {
let width = frame.width();
top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame));
*offset += width;
};
match item {
Item::Absolute(v, _) => {
offset += *v;
}
Item::Fractional(v, elem) => {
let amount = v.share(fr, remaining);
if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full);
let mut frame =
elem.layout(engine, loc.relayout(), *styles, region)?;
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
} else {
offset += amount;
}
}
Item::Text(shaped) => {
let frame = shaped.build(
engine,
&p.spans,
justification_ratio,
extra_justification,
);
push(&mut offset, frame.post_processed(shaped.styles));
}
Item::Frame(frame, styles) => {
let mut frame = frame.clone();
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame));
}
Item::Skip(_) => {}
}
}
if !fr.is_zero() {
remaining = Abs::zero();
}
let size = Size::new(width, top + bottom);
let mut output = Frame::soft(size);
output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top);
for (offset, frame) in frames {
let x = offset + p.align.position(remaining);
let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame);
}
Ok(output)
}
fn add_par_line_marker(
output: &mut Frame,
styles: StyleChain,
engine: &mut Engine,
locator: &mut SplitLocator,
top: Abs,
) {
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
let clearance = ParLine::number_clearance_in(styles);
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
let key = crate::utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc);
let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker)));
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
}
fn overhang(c: char) -> f64 {
match c {
'–' | '—' => 0.2,
'-' => 0.55,
'.' | ',' => 0.8,
':' | ';' => 0.3,
'\u{60C}' | '\u{6D4}' => 0.4,
_ => 0.0,
}
}
pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> {
pub fn new() -> Self {
Self(vec![])
}
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) {
self.0.push(entry.into());
}
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|item| &**item)
}
pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item)
}
pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|item| &**item)
}
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.text_mut()
}
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.text_mut()
}
pub fn reorder(&mut self, from: usize) {
self.0[from..].reverse()
}
}
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl<'a> Deref for Items<'a> {
type Target = Vec<ItemEntry<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for Items<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Debug for Items<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_list().entries(&self.0).finish()
}
}
pub enum ItemEntry<'a> {
Ref(&'a Item<'a>),
Box(Box<Item<'a>>),
}
impl<'a> ItemEntry<'a> {
fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
match self {
Self::Ref(item) => {
let text = item.text()?;
*self = Self::Box(Box::new(Item::Text(text.clone())));
match self {
Self::Box(item) => item.text_mut(),
_ => unreachable!(),
}
}
Self::Box(item) => item.text_mut(),
}
}
}
impl<'a> Deref for ItemEntry<'a> {
type Target = Item<'a>;
fn deref(&self) -> &Self::Target {
match self {
Self::Ref(item) => item,
Self::Box(item) => item,
}
}
}
impl Debug for ItemEntry<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<'a> From<&'a Item<'a>> for ItemEntry<'a> {
fn from(item: &'a Item<'a>) -> Self {
Self::Ref(item)
}
}
impl<'a> From<Item<'a>> for ItemEntry<'a> {
fn from(item: Item<'a>) -> Self {
Self::Box(Box::new(item))
}
}