use crate::buffer::Buffer;
use crate::geom::{Pos, Rect};
use crate::node::NodeId;
use crate::style::{Border, Style, TextAlign, TextTruncate, TextWrap};
pub struct RenderCx<'a> {
pub rect: Rect,
pub buffer: &'a mut Buffer,
pub cursor: Pos,
pub style: Style,
pub focused_id: Option<NodeId>,
pub clip_rect: Option<Rect>,
pub wrap: TextWrap,
pub truncate: TextTruncate,
pub align: TextAlign,
}
impl<'a> RenderCx<'a> {
pub fn new(rect: Rect, buffer: &'a mut Buffer, style: Style) -> Self {
let cursor = Pos {
x: rect.x,
y: rect.y,
};
Self {
rect,
buffer,
cursor,
style,
focused_id: None,
clip_rect: None,
wrap: TextWrap::None,
truncate: TextTruncate::None,
align: TextAlign::Left,
}
}
pub fn is_focused(&self, id: NodeId) -> bool {
self.focused_id == Some(id)
}
pub fn align_offset(&self, text_width: u16) -> u16 {
let clip = self.effective_clip();
let available = clip.x.saturating_add(clip.width).saturating_sub(self.rect.x);
match self.align {
TextAlign::Left => 0,
TextAlign::Center => available.saturating_sub(text_width) / 2,
TextAlign::Right => available.saturating_sub(text_width),
}
}
fn effective_clip(&self) -> Rect {
self.clip_rect.unwrap_or(self.rect)
}
fn wrap_clip(&self) -> Rect {
let mut clip = self.effective_clip();
if self.wrap != TextWrap::None {
clip.height = u16::MAX.saturating_sub(clip.y); }
clip
}
pub fn text(&mut self, text: impl AsRef<str>) {
let text = text.as_ref();
let clip = self.effective_clip();
let available = clip.x.saturating_add(clip.width).saturating_sub(self.cursor.x);
if self.wrap == TextWrap::None && self.truncate == TextTruncate::Ellipsis {
let total: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
if total > available && available >= 1 {
let mut used: u16 = 0;
let mut bytes = 0;
for ch in text.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if used + w > available.saturating_sub(1) { break; }
used += w;
bytes += ch.len_utf8();
}
if bytes > 0 {
self.buffer.write_text(self.cursor, clip, &text[..bytes], &self.style);
self.cursor.x = self.cursor.x.saturating_add(used);
}
self.buffer.write_text(self.cursor, clip, "…", &self.style);
self.cursor.x = self.cursor.x.saturating_add(1);
return;
}
}
self.buffer.write_text(self.cursor, clip, text, &self.style);
let width: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
self.cursor.x = self.cursor.x.saturating_add(width);
}
pub fn line(&mut self, text: impl AsRef<str>) {
let text = text.as_ref();
let clip = self.wrap_clip();
let available = if clip.width >= self.cursor.x.saturating_sub(clip.x) {
clip.width.saturating_sub(self.cursor.x.saturating_sub(clip.x))
} else {
0
};
let saved_clip = self.clip_rect;
if self.wrap != TextWrap::None {
self.clip_rect = Some(clip); }
match self.wrap {
TextWrap::None => {
let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
self.cursor.x = self.rect.x.saturating_add(self.align_offset(tw));
match self.truncate {
TextTruncate::None => {
self.text(text);
}
TextTruncate::Ellipsis => {
let total = tw;
if total > available && available >= 1 {
let mut used: u16 = 0;
let mut bytes = 0;
for ch in text.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
if used + w > available.saturating_sub(1) { break; }
used += w;
bytes += ch.len_utf8();
}
if bytes > 0 {
self.text(&text[..bytes]);
}
self.text("…");
} else {
self.text(text);
}
}
}
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
TextWrap::Char => {
let mut remaining = text;
loop {
let line_widths: Vec<(usize, u16)> = remaining
.char_indices()
.map(|(i, c)| (i, unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16))
.filter(|&(_, w)| w > 0)
.collect();
let mut used: u16 = 0;
let mut bytes = 0;
for &(byte_idx, w) in &line_widths {
if used + w > available { break; }
used += w;
bytes = byte_idx + remaining[byte_idx..].chars().next().map(|c| c.len_utf8()).unwrap_or(0);
}
if bytes == 0 { break; }
let line_text = &remaining[..bytes];
let lw: u16 = line_text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
self.cursor.x = self.rect.x.saturating_add(self.align_offset(lw));
self.text(line_text);
remaining = &remaining[bytes..];
if remaining.is_empty() { break; }
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
if text.is_empty() {
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
}
TextWrap::Word => {
let mut remaining = text;
loop {
let (word, rest) = next_word(remaining);
if word.is_empty() { break; }
let ww: u16 = word.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
let cur_w: u16 = self.cursor.x.saturating_sub(self.rect.x);
if cur_w + ww > available && cur_w > 0 {
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
self.text(word);
remaining = rest;
}
if text.is_empty() {
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
self.cursor.y = self.cursor.y.saturating_add(1);
self.cursor.x = self.rect.x;
}
}
self.clip_rect = saved_clip;
}
pub fn set_style(&mut self, style: Style) {
self.style = style;
}
pub fn draw_border(&mut self, border: Border) {
let clip = self.effective_clip();
self.buffer.draw_border(clip, border, &self.style);
}
}
fn next_word(text: &str) -> (&str, &str) {
let mut chars = text.char_indices().peekable();
while let Some(&(_i, c)) = chars.peek() {
if c == ' ' { chars.next(); } else { break; }
}
let start = chars.peek().map(|&(i, _)| i).unwrap_or(text.len());
if start >= text.len() { return ("", ""); }
if let Some(&(_, c)) = chars.peek() {
let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if w > 1 {
let end = start + c.len_utf8();
return (&text[start..end], &text[end..]);
}
}
let mut end = text.len();
for (i, c) in text[start..].char_indices() {
if c == ' ' {
end = start + i + c.len_utf8(); break;
}
let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if w > 1 {
if i == 0 {
end = start + c.len_utf8();
} else {
end = start + i;
}
break;
}
}
(&text[start..end], &text[end..])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::Buffer;
use crate::geom::{Rect, Size};
#[test]
fn test_word_wrap_simple() {
let mut buf = Buffer::new(Size { width: 20, height: 3 });
let mut cx = RenderCx::new(Rect { x: 0, y: 0, width: 8, height: 3 }, &mut buf, Style::default());
cx.wrap = TextWrap::Word;
cx.line("hello world");
assert_eq!(&buf.cells[0].symbol, "h");
assert_eq!(&buf.cells[20].symbol, "w");
}
#[test]
fn test_next_word_english() {
let (w, r) = next_word("hello world");
assert_eq!(w, "hello ");
assert_eq!(r, "world");
}
#[test]
fn test_next_word_cjk() {
let (w, r) = next_word("你好世界");
assert_eq!(w, "你");
assert_eq!(r, "好世界");
}
#[test]
fn test_word_wrap_long() {
let mut buf = Buffer::new(Size { width: 20, height: 2 });
let mut cx = RenderCx::new(Rect { x: 0, y: 0, width: 5, height: 2 }, &mut buf, Style::default());
cx.wrap = TextWrap::Word;
cx.line("superlongword");
assert_eq!(&buf.cells[0].symbol, "s");
}
}