use std::sync::Arc;
#[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, AvailableSpace,
BidiDirection, GlyphKind, JustifyContent, LayoutError, LoadedFonts, LogicalItem, OverflowInfo,
ParsedFontTrait, Point, PositionedItem, Rect, ShapedCluster, ShapedGlyph, 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_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) => {
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::Box(
item.clone(),
get_item_measure(item, is_vertical),
));
}
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 is_min_content = matches!(constraints.available_width, AvailableSpace::MinContent);
let line_width = match constraints.available_width {
AvailableSpace::Definite(w) => w,
AvailableSpace::MaxContent => f32::MAX / 2.0,
AvailableSpace::MinContent => f32::MAX / 2.0,
};
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 ratio = if current_width < line_width {
if stretch > 0.0 {
(line_width - current_width) / stretch
} else {
INFINITY_BADNESS }
} else if current_width > line_width {
if shrink > 0.0 {
(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 should_justify = constraints.text_justify != JustifyContent::None
&& (!is_last_line || constraints.text_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 (constraints.text_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 = match physical_align {
TextAlign::Center => remaining_space / 2.0,
TextAlign::Right => remaining_space,
_ => 0.0,
};
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.line_height;
start_node = end_node;
}
UnifiedLayout {
items: positioned_items,
overflow: OverflowInfo::default(),
}
}
fn split_cluster_for_hyphenation(
cluster: &ShapedCluster,
glyph_break_index: usize,
) -> Option<(ShapedCluster, ShapedCluster)> {
if glyph_break_index >= cluster.glyphs.len() - 1 {
return None;
}
let first_part_glyphs = cluster.glyphs[..=glyph_break_index].to_vec();
let second_part_glyphs = cluster.glyphs[glyph_break_index + 1..].to_vec();
if first_part_glyphs.is_empty() || second_part_glyphs.is_empty() {
return None;
}
let first_part_advance: f32 = first_part_glyphs
.iter()
.map(|g| g.advance + g.kerning)
.sum();
let second_part_advance: f32 = second_part_glyphs
.iter()
.map(|g| g.advance + g.kerning)
.sum();
let first_part = ShapedCluster {
glyphs: first_part_glyphs,
advance: first_part_advance,
..cluster.clone()
};
let second_part = ShapedCluster {
glyphs: second_part_glyphs,
advance: second_part_advance,
..cluster.clone()
};
Some((first_part, second_part))
}