use rdom_core::NodeId;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::layout::WhiteSpace;
use super::{InlineFragment, LineBox};
pub(super) struct PendingGrapheme {
owner: NodeId,
text_node: NodeId,
source_offset: usize,
text: String,
width: u16,
}
pub(super) struct LinePacker {
content_width: u16,
ws: WhiteSpace,
lines: Vec<LineBox>,
cur_fragments: Vec<InlineFragment>,
cur_line_width: u16,
word_buffer: Vec<PendingGrapheme>,
word_width: u16,
pending_space: bool,
pending_space_source: Option<(NodeId, NodeId, usize)>,
emitted_any: bool,
}
impl LinePacker {
pub(super) fn new(content_width: u16, ws: WhiteSpace) -> Self {
Self {
content_width,
ws,
lines: Vec::new(),
cur_fragments: Vec::new(),
cur_line_width: 0,
word_buffer: Vec::new(),
word_width: 0,
pending_space: false,
pending_space_source: None,
emitted_any: false,
}
}
pub(super) fn take_lines(&mut self) -> Vec<LineBox> {
std::mem::take(&mut self.lines)
}
pub(super) fn push_text(&mut self, owner: NodeId, text_node: NodeId, text: &str) {
let mut source_offset = 0usize;
for g in text.graphemes(true) {
self.push_grapheme(owner, text_node, source_offset, g);
source_offset += g.len();
}
}
pub(super) fn push_hard_break(&mut self, _owner: NodeId) {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.pending_space = false;
self.pending_space_source = None;
self.break_line();
}
fn push_grapheme(&mut self, owner: NodeId, text_node: NodeId, source_offset: usize, g: &str) {
let first = g.chars().next().unwrap_or(' ');
if first.is_control() {
match self.ws {
WhiteSpace::Pre | WhiteSpace::PreWrap => match g {
"\n" | "\r\n" => {
self.push_hard_break(owner);
return;
}
"\r" => return,
"\t" => {
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: " ".to_string(),
width: 1,
});
self.word_width = self.word_width.saturating_add(1);
return;
}
_ => return,
},
_ => {
if !self.word_buffer.is_empty() {
self.commit_word();
}
if self.emitted_any {
self.pending_space = true;
self.pending_space_source = Some((owner, text_node, source_offset));
}
return;
}
}
}
let w = UnicodeWidthStr::width(g) as u16;
if w == 0 {
return;
}
match self.ws {
WhiteSpace::Pre => {
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: g.to_string(),
width: w,
});
self.word_width = self.word_width.saturating_add(w);
}
WhiteSpace::PreWrap => {
if g == " " {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: " ".to_string(),
width: 1,
});
self.word_width = self.word_width.saturating_add(1);
self.commit_word();
} else if w == 2 {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: g.to_string(),
width: w,
});
self.word_width = self.word_width.saturating_add(w);
self.commit_word();
} else {
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: g.to_string(),
width: w,
});
self.word_width = self.word_width.saturating_add(w);
if g == "-" {
self.commit_word();
}
}
}
WhiteSpace::Normal | WhiteSpace::NoWrap => {
if is_collapsible_whitespace(g) {
if !self.word_buffer.is_empty() {
self.commit_word();
}
if self.emitted_any {
self.pending_space = true;
self.pending_space_source = Some((owner, text_node, source_offset));
}
return;
}
if w == 2 {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: g.to_string(),
width: w,
});
self.word_width = self.word_width.saturating_add(w);
self.commit_word();
return;
}
self.word_buffer.push(PendingGrapheme {
owner,
text_node,
source_offset,
text: g.to_string(),
width: w,
});
self.word_width = self.word_width.saturating_add(w);
if g == "-" {
self.commit_word();
}
}
}
}
fn commit_word(&mut self) {
if self.word_buffer.is_empty() {
return;
}
let separator: u16 = if self.pending_space && self.cur_line_width > 0 {
1
} else {
0
};
let projected = self
.cur_line_width
.saturating_add(separator)
.saturating_add(self.word_width);
let must_wrap = projected > self.content_width
&& matches!(self.ws, WhiteSpace::Normal | WhiteSpace::PreWrap);
if must_wrap && !self.cur_fragments.is_empty() {
self.break_line();
self.pending_space = false;
self.pending_space_source = None;
self.emit_word_to_current_line(0);
} else {
self.emit_word_to_current_line(separator);
self.pending_space = false;
self.pending_space_source = None;
}
}
fn emit_word_to_current_line(&mut self, separator_width: u16) {
if separator_width > 0 && !self.word_buffer.is_empty() {
let (sep_owner, sep_text_node, sep_source_offset) =
self.pending_space_source.unwrap_or_else(|| {
let g = &self.word_buffer[0];
(g.owner, g.text_node, g.source_offset)
});
self.append_fragment(sep_owner, sep_text_node, sep_source_offset, " ", 1);
}
let mut idx = 0;
while idx < self.word_buffer.len() {
let g0 = &self.word_buffer[idx];
let owner = g0.owner;
let text_node = g0.text_node;
let source_offset = g0.source_offset;
let mut text = String::new();
let mut width: u16 = 0;
while idx < self.word_buffer.len() {
let g = &self.word_buffer[idx];
if g.owner != owner || g.text_node != text_node {
break;
}
text.push_str(&g.text);
width = width.saturating_add(g.width);
idx += 1;
}
self.append_fragment(owner, text_node, source_offset, &text, width);
}
self.word_buffer.clear();
self.word_width = 0;
self.emitted_any = true;
}
fn append_fragment(
&mut self,
owner: NodeId,
text_node: NodeId,
source_offset: usize,
text: &str,
width: u16,
) {
if let Some(last) = self.cur_fragments.last_mut() {
let contiguous = last.source_byte_offset + last.text.len() == source_offset;
if last.node == owner && last.text_node == text_node && contiguous {
last.text.push_str(text);
last.width = last.width.saturating_add(width);
self.cur_line_width = self.cur_line_width.saturating_add(width);
return;
}
}
let x = self.cur_line_width;
self.cur_fragments.push(InlineFragment {
node: owner,
text_node,
source_byte_offset: source_offset,
x,
width,
text: text.to_string(),
atomic: false,
});
self.cur_line_width = self.cur_line_width.saturating_add(width);
}
pub(super) fn push_atomic_inline_block(&mut self, node: NodeId, width: u16) {
if !self.word_buffer.is_empty() {
self.commit_word();
}
let separator: u16 = if self.pending_space && self.cur_line_width > 0 {
1
} else {
0
};
let projected = self
.cur_line_width
.saturating_add(separator)
.saturating_add(width);
if projected > self.content_width && self.cur_line_width > 0 {
self.break_line();
self.pending_space = false;
self.pending_space_source = None;
} else if separator > 0 {
let (sep_owner, sep_text_node, sep_offset) =
self.pending_space_source.unwrap_or((node, node, 0));
self.append_fragment(sep_owner, sep_text_node, sep_offset, " ", 1);
self.pending_space = false;
self.pending_space_source = None;
}
let x = self.cur_line_width;
self.cur_fragments.push(InlineFragment {
node,
text_node: node, source_byte_offset: 0,
x,
width,
text: String::new(),
atomic: true,
});
self.cur_line_width = self.cur_line_width.saturating_add(width);
self.emitted_any = true;
}
fn break_line(&mut self) {
let fragments = std::mem::take(&mut self.cur_fragments);
let width = self.cur_line_width;
self.cur_line_width = 0;
self.lines.push(LineBox { fragments, width });
}
pub(super) fn finish(&mut self) {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.pending_space = false;
self.pending_space_source = None;
if !self.cur_fragments.is_empty() {
self.break_line();
}
}
}
#[inline]
fn is_collapsible_whitespace(grapheme: &str) -> bool {
matches!(grapheme, " " | "\t" | "\n" | "\r" | "\r\n")
}