#[cfg(feature = "text_layout_hyphenation")]
use hyphenation::{Hyphenator, Standard};
#[cfg(not(feature = "text_layout_hyphenation"))]
use crate::text3::cache::Standard;
use crate::text3::cache::{
get_base_direction_from_logical, get_item_measure, is_word_separator, is_zero_width_space,
AvailableSpace, BidiDirection, JustifyContent, LayoutError, LoadedFonts,
LogicalItem, OverflowInfo, ParsedFontTrait, Point, PositionedItem,
ShapedItem, TextAlign, UnifiedConstraints, UnifiedLayout,
};
const INFINITY_BADNESS: f32 = 10000.0;
#[derive(Debug, Clone)]
enum LayoutNode {
Box(ShapedItem, f32), Glue {
item: ShapedItem,
width: f32,
stretch: f32,
shrink: f32,
},
Penalty {
item: Option<ShapedItem>,
width: f32,
penalty: f32,
},
}
#[derive(Debug, Clone, Copy)]
struct Breakpoint {
demerit: f32,
previous: usize,
line: usize,
}
pub(crate) fn kp_layout<T: ParsedFontTrait>(
items: &[ShapedItem],
logical_items: &[LogicalItem],
constraints: &UnifiedConstraints,
hyphenator: Option<&Standard>,
fonts: &LoadedFonts<T>,
) -> Result<UnifiedLayout, LayoutError> {
if items.is_empty() {
return Ok(UnifiedLayout {
items: Vec::new(),
overflow: OverflowInfo::default(),
});
}
let nodes = convert_items_to_nodes(items, hyphenator, fonts);
let breaks = find_optimal_breakpoints(&nodes, constraints);
let final_layout: UnifiedLayout =
position_lines_from_breaks(&nodes, &breaks, logical_items, constraints);
Ok(final_layout)
}
fn convert_items_to_nodes<T: ParsedFontTrait>(
items: &[ShapedItem],
hyphenator: Option<&Standard>,
fonts: &LoadedFonts<T>,
) -> Vec<LayoutNode> {
let mut nodes = Vec::new();
let is_vertical = false; let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() {
match item {
item if is_zero_width_space(item) => {
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: 0.0,
});
}
item if is_word_separator(item) => {
let width = get_item_measure(item, is_vertical);
nodes.push(LayoutNode::Glue {
item: item.clone(),
width,
stretch: width * 0.5,
shrink: width * 0.33,
});
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: 0.0,
});
}
ShapedItem::Cluster(cluster)
if cluster.text.ends_with('\u{002D}')
|| cluster.text.ends_with('\u{2010}') =>
{
let width = get_item_measure(item, is_vertical);
nodes.push(LayoutNode::Box(item.clone(), width));
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: 0.0,
});
}
ShapedItem::Cluster(cluster) => {
let mut current_word_clusters = vec![cluster.clone()];
while let Some(peeked_item) = item_iter.peek() {
if let ShapedItem::Cluster(next_cluster) = peeked_item {
current_word_clusters.push(next_cluster.clone());
item_iter.next(); } else {
break;
}
}
let hyphenation_breaks = hyphenator.and_then(|h| {
crate::text3::cache::find_all_hyphenation_breaks(
¤t_word_clusters,
h,
is_vertical,
fonts,
)
});
if hyphenation_breaks.is_none() {
for c in current_word_clusters {
nodes.push(LayoutNode::Box(ShapedItem::Cluster(c.clone()), c.advance));
}
} else {
let breaks = hyphenation_breaks.unwrap();
let mut current_item_cursor = 0;
for b in breaks.iter() {
let num_items_in_syllable = b.line_part.len() - current_item_cursor;
for item in b.line_part.iter().skip(current_item_cursor) {
nodes.push(LayoutNode::Box(
item.clone(),
get_item_measure(item, is_vertical),
));
}
current_item_cursor += num_items_in_syllable;
let hyphen_measure = get_item_measure(&b.hyphen_item, is_vertical);
nodes.push(LayoutNode::Penalty {
item: Some(b.hyphen_item.clone()),
width: hyphen_measure,
penalty: 50.0, });
}
if let Some(last_break) = breaks.last() {
for remainder_item in &last_break.remainder_part {
nodes.push(LayoutNode::Box(
remainder_item.clone(),
get_item_measure(remainder_item, is_vertical),
));
}
} else {
for c in current_word_clusters {
nodes.push(LayoutNode::Box(ShapedItem::Cluster(c.clone()), c.advance));
}
}
}
}
ShapedItem::Object { .. } | ShapedItem::CombinedBlock { .. } => {
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: 0.0,
});
nodes.push(LayoutNode::Box(
item.clone(),
get_item_measure(item, is_vertical),
));
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: 0.0,
});
}
ShapedItem::Tab { bounds, .. } => {
nodes.push(LayoutNode::Glue {
item: item.clone(),
width: bounds.width,
stretch: bounds.width * 0.5, shrink: bounds.width * 0.33,
});
}
ShapedItem::Break { .. } => {
nodes.push(LayoutNode::Penalty {
item: None,
width: 0.0,
penalty: -INFINITY_BADNESS,
});
}
}
}
nodes
}
fn find_optimal_breakpoints(nodes: &[LayoutNode], constraints: &UnifiedConstraints) -> Vec<usize> {
let line_width = match constraints.available_width {
AvailableSpace::Definite(w) => w,
AvailableSpace::MaxContent => f32::MAX / 2.0,
AvailableSpace::MinContent => f32::MAX / 2.0,
};
let text_indent = constraints.text_indent;
let first_line_width = if !constraints.text_indent_hanging {
line_width - text_indent
} else {
line_width
};
let non_first_line_width = if constraints.text_indent_hanging {
line_width - text_indent
} else {
line_width
};
let mut breakpoints = vec![
Breakpoint {
demerit: INFINITY_BADNESS,
previous: 0,
line: 0
};
nodes.len() + 1
];
breakpoints[0] = Breakpoint {
demerit: 0.0,
previous: 0,
line: 0,
};
for i in 0..nodes.len() {
if !matches!(nodes.get(i), Some(LayoutNode::Penalty { .. })) {
continue;
}
for j in (0..=i).rev() {
let (mut current_width, mut stretch, mut shrink) = (0.0, 0.0, 0.0);
for k in j..=i {
match &nodes[k] {
LayoutNode::Box(_, w) => current_width += w,
LayoutNode::Glue {
width,
stretch: s,
shrink: k,
..
} => {
current_width += width;
stretch += s;
shrink += k;
}
LayoutNode::Penalty { width, .. } => current_width += width,
}
}
let effective_line_width = if breakpoints[j].line == 0 {
first_line_width
} else if constraints.text_indent_hanging {
non_first_line_width
} else {
line_width
};
let ratio = if current_width < effective_line_width {
if stretch > 0.0 {
(effective_line_width - current_width) / stretch
} else {
INFINITY_BADNESS }
} else if current_width > effective_line_width {
if shrink > 0.0 {
(effective_line_width - current_width) / shrink
} else {
INFINITY_BADNESS }
} else {
0.0 };
if ratio < -1.0 {
continue;
}
let mut badness = 100.0 * ratio.abs().powi(3);
if let Some(LayoutNode::Penalty { penalty, .. }) = nodes.get(i) {
if *penalty >= 0.0 {
badness += penalty;
} else if *penalty <= -INFINITY_BADNESS {
badness = -INFINITY_BADNESS; }
}
let demerit = badness + breakpoints[j].demerit;
if demerit < breakpoints[i + 1].demerit {
breakpoints[i + 1] = Breakpoint {
demerit,
previous: j,
line: breakpoints[j].line + 1,
};
}
}
}
let mut breaks = Vec::new();
let mut current = nodes.len();
while current > 0 {
breaks.push(current);
let prev_idx = breakpoints[current].previous;
current = prev_idx;
}
breaks.reverse();
breaks
}
fn position_lines_from_breaks(
nodes: &[LayoutNode],
breaks: &[usize],
logical_items: &[LogicalItem],
constraints: &UnifiedConstraints,
) -> UnifiedLayout {
let mut positioned_items = Vec::new();
let mut start_node = 0;
let mut cross_axis_pen = 0.0;
let base_direction = get_base_direction_from_logical(logical_items);
for (line_index, &end_node) in breaks.iter().enumerate() {
let line_nodes = &nodes[start_node..end_node];
let is_last_line = line_index == breaks.len() - 1;
let line_items: Vec<ShapedItem> = line_nodes
.iter()
.filter_map(|node| match node {
LayoutNode::Box(item, _) => Some(item.clone()),
LayoutNode::Glue { item, .. } => Some(item.clone()),
LayoutNode::Penalty { item, .. } => item.clone(),
})
.collect();
let mut extra_per_space = 0.0;
let line_width: f32 = line_items.iter().map(|i| get_item_measure(i, false)).sum();
let ends_with_forced_break = line_nodes.iter().any(|n| matches!(
n, LayoutNode::Penalty { penalty, .. } if *penalty <= -INFINITY_BADNESS
));
let effective_align = super::cache::resolve_effective_alignment(
constraints.text_align,
constraints.text_align_last,
is_last_line || ends_with_forced_break,
);
let should_justify = constraints.text_justify != JustifyContent::None
&& (!is_last_line || constraints.text_align == TextAlign::JustifyAll
|| effective_align == TextAlign::Justify || effective_align == TextAlign::JustifyAll);
let available_width_f32 = match constraints.available_width {
AvailableSpace::Definite(w) => w,
AvailableSpace::MaxContent => line_width,
AvailableSpace::MinContent => line_width,
};
if should_justify {
let space_to_add = available_width_f32 - line_width;
if space_to_add > 0.0 {
let space_count = line_items
.iter()
.filter(|item| is_word_separator(item))
.count();
if space_count > 0 {
extra_per_space = space_to_add / space_count as f32;
}
}
}
let total_width: f32 = line_items
.iter()
.map(|item| get_item_measure(item, false))
.sum();
let is_indefinite = matches!(
constraints.available_width,
AvailableSpace::MaxContent | AvailableSpace::MinContent
);
let remaining_space = if is_indefinite {
0.0
} else {
available_width_f32
- (total_width
+ extra_per_space
* line_items
.iter()
.filter(|item| is_word_separator(item))
.count() as f32)
};
let physical_align = match (effective_align, base_direction) {
(TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
(TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
(TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
(TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
(other, _) => other,
};
let mut main_axis_pen = if remaining_space < 0.0 {
0.0
} else {
match physical_align {
TextAlign::Center => remaining_space / 2.0,
TextAlign::Right => remaining_space,
_ => 0.0,
}
};
if constraints.text_indent != 0.0 {
let is_indent_target = if constraints.text_indent_each_line {
line_index == 0 } else {
line_index == 0
};
let should_indent = if constraints.text_indent_hanging {
!is_indent_target
} else {
is_indent_target
};
if should_indent {
main_axis_pen += constraints.text_indent;
}
}
for item in line_items {
let item_advance = get_item_measure(&item, false);
let draw_pos = match &item {
ShapedItem::Cluster(c) if !c.glyphs.is_empty() => {
let glyph = &c.glyphs[0];
Point {
x: main_axis_pen + glyph.offset.x,
y: cross_axis_pen - glyph.offset.y, }
}
_ => Point {
x: main_axis_pen,
y: cross_axis_pen,
},
};
positioned_items.push(PositionedItem {
item: item.clone(),
position: draw_pos,
line_index,
});
main_axis_pen += item_advance;
if is_word_separator(&item) {
main_axis_pen += extra_per_space;
}
}
cross_axis_pen += constraints.resolved_line_height();
start_node = end_node;
}
UnifiedLayout {
items: positioned_items,
overflow: OverflowInfo::default(),
}
}