use duat_core::{
opts::PrintOpts,
text::{Text, TextPart, TextPlace, TwoPoints},
};
use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone, Copy)]
pub struct PrintedPlace {
pub x: u32,
pub len: u32,
pub wrap: bool,
}
#[inline(never)]
pub fn print_iter<'t>(
text: &'t Text,
points: TwoPoints,
width: u32,
opts: PrintOpts,
) -> impl Iterator<Item = (PrintedPlace, TextPlace<'t>)> + 't {
let start_points = text.visual_line_start(points, 0);
let cap = opts.wrap_width(width).unwrap_or(width);
let max_indent = if opts.indent_wraps { cap } else { 0 };
let (mut x, mut spacers) = (0, 0);
let (mut indent, mut on_indent, mut wrapped_indent) = (0, true, 0);
if start_points != points {
for item in text
.iter_fwd(start_points)
.take_while(|item| item.points() < points)
{
let old_indent = indent;
let indent = (&mut indent, &mut on_indent);
let len = process_part(item, x, opts, indent, &mut spacers);
x += len;
if x > cap && opts.wrap_lines {
spacers = 0;
if let TextPart::Char('\t') = item.part {
let desired = old_indent + x - cap;
wrapped_indent = if desired < max_indent {
desired
} else {
x - cap
};
x = wrapped_indent;
} else {
wrapped_indent = old_indent * (old_indent < max_indent) as u32;
x = wrapped_indent + len;
}
}
}
if x == cap && opts.wrap_lines {
x = indent;
}
}
inner_iter(
text.iter_fwd(points),
(x, spacers),
(indent, on_indent, wrapped_indent),
(cap, opts),
)
}
#[inline(never)]
pub fn rev_print_iter<'t>(
text: &'t Text,
points: TwoPoints,
cap: u32,
opts: PrintOpts,
) -> impl Iterator<Item = (PrintedPlace, TextPlace<'t>)> + 't {
let mut iter = text.iter_rev(points);
let mut returns = Vec::new();
let mut items = Vec::new();
let mut discard_first_empty_wrap = {
let mut went_through_non_empty = false;
move |(mut place, item): (PrintedPlace, _)| {
went_through_non_empty |= place.len > 0;
place.wrap &= went_through_non_empty;
(place, item)
}
};
std::iter::from_fn(move || {
if let Some(next) = returns.pop().map(&mut discard_first_empty_wrap) {
Some(next)
} else {
let iter = loop {
if let Some(item) = iter.next() {
items.push(item);
if let TextPart::Char('\n') = item.part {
let len = items.len();
if len > 1 {
break items.drain(..len - 1).rev();
}
}
} else {
break items.drain(..).rev();
}
};
returns.extend(inner_iter(iter, (0, 0), (0, true, 0), (cap, opts)));
returns.pop().map(&mut discard_first_empty_wrap)
}
})
}
#[inline(always)]
fn inner_iter<'t, 'i>(
iter: impl Iterator<Item = TextPlace<'t>>,
(mut x, mut spacers): (u32, usize),
(mut indent, mut on_indent, mut wrapped_indent): (u32, bool, u32),
(cap, opts): (u32, PrintOpts),
) -> impl Iterator<Item = (PrintedPlace, TextPlace<'t>)>
where
't: 'i,
{
let max_indent = if opts.indent_wraps { cap } else { 0 };
let (mut line, mut leftover_nl) = (Vec::<(u32, TextPlace)>::new(), None);
let (mut printed_x, mut i) = (0, 0);
let mut iter = iter.peekable();
let mut initial_printed_x = x;
std::iter::from_fn(move || {
loop {
if let Some(&(len, item)) = line.get(i) {
let caret = PrintedPlace { x: printed_x, len, wrap: i == 0 };
i += 1;
printed_x += len;
break Some((caret, item));
}
line.clear();
i = 0;
if let Some((x, item)) = leftover_nl.take() {
break Some((PrintedPlace { x, len: 1, wrap: true }, item));
}
loop {
let Some(&item) = iter.peek() else {
if line.is_empty() {
return None;
} else {
printed_x = initial_printed_x.max(wrapped_indent);
space_line(spacers, &mut line, cap, x);
break;
}
};
let mut next = || _ = unsafe { iter.next().unwrap_unchecked() };
let old_indent = indent;
let indent = (&mut indent, &mut on_indent);
let len = process_part(item, x, opts, indent, &mut spacers);
let must_wrap = (x >= cap || x + len > cap) && opts.wrap_lines;
x += len;
if item.part == TextPart::Char('\n') || must_wrap {
printed_x = initial_printed_x.max(wrapped_indent);
space_line(spacers, &mut line, cap, x);
spacers = 0;
let leftover = x.saturating_sub(cap);
wrapped_indent = match item.part {
TextPart::Char('\t') if leftover > 0 => {
line.push((len - leftover, item));
next();
if old_indent + leftover < max_indent && opts.indent_wraps {
old_indent + leftover
} else {
leftover
}
}
TextPart::Char('\n') => {
if len > 0 && must_wrap {
let position = old_indent
* (old_indent < max_indent && opts.indent_wraps) as u32;
leftover_nl = Some((position, item))
} else {
line.push((len, item));
}
next();
0
}
_ => {
if cap == 0 && !line.iter().any(|(_, item)| item.part.is_char()) {
line.push((len, item));
next();
}
old_indent * (old_indent < max_indent && opts.indent_wraps) as u32
}
};
x = wrapped_indent;
break;
} else {
line.push((len, item));
next();
}
}
initial_printed_x = 0;
}
})
}
pub fn is_starting_points(text: &Text, points: TwoPoints, width: u32, opts: PrintOpts) -> bool {
let start_points = text.visual_line_start(points, 0);
let max_indent = if opts.indent_wraps { width } else { 0 };
let cap = opts.wrap_width(width).unwrap_or(width);
let (mut x, mut spacers) = (0, 0);
let (mut indent, mut on_indent) = (0, true);
if start_points == points {
true
} else if !opts.wrap_lines {
false
} else {
let mut wrapped = true;
for item in text
.iter_fwd(start_points)
.take_while(|item| item.points() <= points)
{
wrapped = false;
let old_indent = indent;
let indent = (&mut indent, &mut on_indent);
let len = process_part(item, x, opts, indent, &mut spacers);
x += len;
if x > cap && opts.wrap_lines {
wrapped = true;
x = if let TextPart::Char('\t') = item.part {
let desired = old_indent + x - cap;
if desired < max_indent {
desired
} else {
x - cap
}
} else {
old_indent * (old_indent < max_indent) as u32 + len
};
}
}
wrapped
}
}
#[inline(always)]
fn _words<'t, 'i>(
iter: impl Iterator<Item = TextPlace<'t>> + Clone + 'i,
(mut total_len, mut spacers): (u32, usize),
(mut indent, mut on_indent, mut wrapped_indent): (u32, bool, u32),
(cap, opts): (u32, PrintOpts),
) -> impl Iterator<Item = (PrintedPlace, TextPlace<'t>)> + Clone {
let max_indent = if opts.indent_wraps { cap } else { 0 };
let (mut line, mut leftover_nl): (Vec<(u32, TextPlace)>, _) = (Vec::new(), None);
let (mut x, mut i, mut first_char_was_printed) = (0, 0, false);
let mut iter = iter.peekable();
let mut word = Vec::new();
let (mut new_x, mut first_x) = (0, total_len * (total_len < cap) as u32);
std::iter::from_fn(move || {
loop {
if let Some(&(len, item)) = line.get(i) {
let wrap = !first_char_was_printed && item.part.is_char();
if wrap {
x = new_x;
}
let caret = PrintedPlace { x, len, wrap };
i += 1;
x += len;
first_char_was_printed |= item.part.is_char();
break Some((caret, item));
}
line.clear();
i = 0;
first_char_was_printed = false;
if let Some((x, item)) = leftover_nl.take() {
break Some((PrintedPlace { x, len: 1, wrap: true }, item));
}
loop {
let Some(&item) = iter.peek() else {
if line.is_empty() {
return None;
} else {
break;
}
};
let old_indent = indent;
let indent = (&mut indent, &mut on_indent);
let len = process_part(item, x, opts, indent, &mut spacers);
total_len += len;
if let TextPart::Char(char) = item.part
&& ((total_len >= cap && opts.wrap_lines) || char == '\n')
{
new_x = first_x + wrapped_indent;
space_line(spacers, &mut line, cap, total_len);
spacers = 0;
let leftover = total_len.saturating_sub(cap);
wrapped_indent = match char {
'\t' if leftover > 0 => {
line.push((len - leftover, item));
iter.next();
if old_indent + leftover < max_indent && opts.indent_wraps {
old_indent + leftover
} else {
leftover
}
}
'\n' => {
match (opts.print_new_line, total_len > cap) {
(true, true) => {
let position = old_indent
* (old_indent < max_indent && opts.indent_wraps) as u32;
leftover_nl = Some((position, iter.next().unwrap()))
}
(true, false) => line.push((1, iter.next().unwrap())),
(false, _) => line.push((0, iter.next().unwrap())),
}
0
}
_ => old_indent * (old_indent < max_indent && opts.indent_wraps) as u32,
};
total_len = wrapped_indent;
break;
} else {
word.push((len, item));
}
iter.next();
}
first_x = 0;
}
})
}
#[inline(always)]
pub fn len_from(char: char, start: u32, opts: PrintOpts) -> u32 {
match char {
'\t' => (opts.tabstop_spaces_at(start)).max(1),
'\n' => 0,
'\0'..='\u{1f}' => 2,
'\u{80}'..='\u{9f}' => 4,
char => UnicodeWidthChar::width(char).unwrap_or(0) as u32,
}
}
fn process_part(
item: TextPlace,
x: u32,
opts: PrintOpts,
indent: (&mut u32, &mut bool),
spacers: &mut usize,
) -> u32 {
match item.part {
TextPart::Char(char) => process_char(indent, x, char, opts),
TextPart::Spacer => {
*spacers += 1;
0
}
_ => 0,
}
}
#[inline(always)]
fn process_char(
(indent, on_indent): (&mut u32, &mut bool),
x: u32,
char: char,
opts: PrintOpts,
) -> u32 {
match char {
'\n' => {
*on_indent = true;
*indent = 0;
opts.print_new_line as u32
}
char => {
let len = len_from(char, x, opts);
*on_indent &= char == ' ' || char == '\t';
*indent += len * *on_indent as u32;
len
}
}
}
pub fn space_line(spacers: usize, line: &mut [(u32, TextPlace)], cap: u32, total_len: u32) {
if spacers == 0 {
return;
}
let space = cap.saturating_sub(total_len) as usize;
let mut enough_space = space;
while !enough_space.is_multiple_of(spacers) {
enough_space += 1;
}
let diff = enough_space - space;
for (i, (len, _)) in line
.iter_mut()
.rev()
.filter(|(_, item)| matches!(item.part, TextPart::Spacer))
.enumerate()
{
*len = ((enough_space / spacers) - (i < diff) as usize) as u32;
}
}