use rdom_core::NodeId;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::layout::WhiteSpace;
use super::{GeneratedFragment, InlineFragment, LineBox};
use crate::ext::PseudoSlot;
pub(super) struct PendingGrapheme<'a> {
origin: Origin,
source_offset: usize,
text: &'a str,
width: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Origin {
owner: NodeId,
text_node: NodeId,
generated: Option<PseudoSlot>,
}
impl Origin {
fn text(owner: NodeId, text_node: NodeId) -> Self {
Origin {
owner,
text_node,
generated: None,
}
}
}
pub(super) struct LinePacker<'a> {
content_width: u16,
ws: WhiteSpace,
lines: Vec<LineBox>,
cur_fragments: Vec<InlineFragment>,
cur_generated: Vec<GeneratedFragment>,
cur_line_width: u16,
word_buffer: Vec<PendingGrapheme<'a>>,
word_width: u16,
pending_space: bool,
pending_space_source: Option<(Origin, usize)>,
emitted_any: bool,
}
impl<'a> LinePacker<'a> {
pub(super) fn new(content_width: u16, ws: WhiteSpace) -> Self {
Self {
content_width,
ws,
lines: Vec::new(),
cur_fragments: Vec::new(),
cur_generated: 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 content_width(&self) -> u16 {
self.content_width
}
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: &'a str) {
self.push_str(Origin::text(owner, text_node), text);
}
pub(super) fn push_generated(&mut self, host: NodeId, slot: PseudoSlot, text: &'a str) {
let origin = Origin {
owner: host,
text_node: host,
generated: Some(slot),
};
self.push_str(origin, text);
}
fn push_str(&mut self, origin: Origin, text: &'a str) {
let mut source_offset = 0usize;
for g in text.graphemes(true) {
self.push_grapheme(origin, 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, origin: Origin, source_offset: usize, g: &'a 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(origin.owner);
return;
}
"\r" => return,
"\t" => {
self.word_buffer.push(PendingGrapheme {
origin,
source_offset,
text: " ",
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((origin, source_offset));
}
return;
}
}
}
let w = UnicodeWidthStr::width(g) as u16;
if w == 0 {
return;
}
match self.ws {
WhiteSpace::Pre => {
self.word_buffer.push(PendingGrapheme {
origin,
source_offset,
text: g,
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 {
origin,
source_offset,
text: " ",
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 {
origin,
source_offset,
text: g,
width: w,
});
self.word_width = self.word_width.saturating_add(w);
self.commit_word();
} else {
self.word_buffer.push(PendingGrapheme {
origin,
source_offset,
text: g,
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((origin, source_offset));
}
return;
}
if w == 2 {
if !self.word_buffer.is_empty() {
self.commit_word();
}
self.word_buffer.push(PendingGrapheme {
origin,
source_offset,
text: g,
width: w,
});
self.word_width = self.word_width.saturating_add(w);
self.commit_word();
return;
}
self.word_buffer.push(PendingGrapheme {
origin,
source_offset,
text: g,
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.line_has_content() {
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_origin, sep_source_offset) = self.pending_space_source.unwrap_or_else(|| {
let g = &self.word_buffer[0];
(g.origin, g.source_offset)
});
self.append_fragment(sep_origin, sep_source_offset, " ", 1);
}
let mut idx = 0;
while idx < self.word_buffer.len() {
let g0 = &self.word_buffer[idx];
let origin = g0.origin;
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.origin != origin {
break;
}
text.push_str(g.text);
width = width.saturating_add(g.width);
idx += 1;
}
self.append_fragment(origin, source_offset, &text, width);
}
self.word_buffer.clear();
self.word_width = 0;
self.emitted_any = true;
}
fn append_fragment(&mut self, origin: Origin, source_offset: usize, text: &str, width: u16) {
let x = self.cur_line_width;
self.cur_line_width = self.cur_line_width.saturating_add(width);
if let Some(slot) = origin.generated {
if let Some(last) = self.cur_generated.last_mut()
&& last.host == origin.owner
&& last.slot == slot
&& last.x + last.width == x
{
last.text.push_str(text);
last.width = last.width.saturating_add(width);
return;
}
self.cur_generated.push(GeneratedFragment {
host: origin.owner,
slot,
x,
width,
text: text.to_string(),
});
return;
}
let Origin {
owner, text_node, ..
} = origin;
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.x + last.width == x
{
last.text.push_str(text);
last.width = last.width.saturating_add(width);
return;
}
}
self.cur_fragments.push(InlineFragment {
node: owner,
text_node,
source_byte_offset: source_offset,
x,
width,
text: text.to_string(),
atomic: false,
});
}
fn line_has_content(&self) -> bool {
!self.cur_fragments.is_empty() || !self.cur_generated.is_empty()
}
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_origin, sep_offset) = self
.pending_space_source
.unwrap_or((Origin::text(node, node), 0));
self.append_fragment(sep_origin, 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 generated = std::mem::take(&mut self.cur_generated);
let width = self.cur_line_width;
self.cur_line_width = 0;
self.lines.push(LineBox {
fragments,
generated,
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.line_has_content() {
self.break_line();
}
}
}
#[inline]
fn is_collapsible_whitespace(grapheme: &str) -> bool {
matches!(grapheme, " " | "\t" | "\n" | "\r" | "\r\n")
}