use crate::base::Rect;
use crate::text;
use super::style::Style;
use super::surface::Surface;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Span {
pub text: String,
pub style: Style,
pub link: Option<String>,
}
impl Span {
pub fn new(text: impl Into<String>, style: Style) -> Span {
Span {
text: text.into(),
style,
link: None,
}
}
pub fn plain(text: impl Into<String>) -> Span {
Span::new(text, Style::EMPTY)
}
pub fn with_link(mut self, url: impl Into<String>) -> Span {
self.link = Some(url.into());
self
}
pub fn width(&self) -> i32 {
text::width(&self.text)
}
fn same_ink(&self, other: &Span) -> bool {
self.style == other.style && self.link == other.link
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RichLine {
pub spans: Vec<Span>,
}
impl RichLine {
pub fn new() -> RichLine {
RichLine::default()
}
pub fn from_spans(spans: Vec<Span>) -> RichLine {
RichLine { spans }
}
pub fn push(&mut self, span: Span) {
if span.text.is_empty() {
return;
}
if let Some(last) = self.spans.last_mut() {
if last.same_ink(&span) {
last.text.push_str(&span.text);
return;
}
}
self.spans.push(span);
}
fn push_run(&mut self, text: &str, style: Style, link: &Option<String>) {
if text.is_empty() {
return;
}
if let Some(last) = self.spans.last_mut() {
if last.style == style && last.link == *link {
last.text.push_str(text);
return;
}
}
self.spans.push(Span {
text: text.to_string(),
style,
link: link.clone(),
});
}
pub fn width(&self) -> i32 {
self.spans.iter().map(Span::width).sum()
}
pub fn is_empty(&self) -> bool {
self.spans.iter().all(|s| s.text.is_empty())
}
pub fn plain(&self) -> String {
self.spans.iter().map(|s| s.text.as_str()).collect()
}
pub fn from_highlighted(
line: &str,
lexer: &dyn text::Highlighter,
base: Style,
map: impl Fn(text::TokenKind) -> Style,
) -> RichLine {
let mut out = RichLine::new();
let mut cursor = 0;
for (range, kind) in lexer.spans(line) {
if range.start > cursor {
out.push(Span::new(&line[cursor..range.start], base));
}
out.push(Span::new(&line[range.clone()], map(kind)));
cursor = range.end;
}
if cursor < line.len() {
out.push(Span::new(&line[cursor..], base));
}
out
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RichText {
pub lines: Vec<RichLine>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum HAlign {
#[default]
Left,
Center,
Right,
}
impl RichText {
pub fn new() -> RichText {
RichText::default()
}
pub fn from_lines(lines: Vec<RichLine>) -> RichText {
RichText { lines }
}
pub fn plain(s: &str, style: Style) -> RichText {
let lines = s
.split('\n')
.map(|l| {
let l = l.strip_suffix('\r').unwrap_or(l);
RichLine::from_spans(if l.is_empty() {
Vec::new()
} else {
vec![Span::new(l, style)]
})
})
.collect();
RichText { lines }
}
pub fn height(&self) -> i32 {
self.lines.len() as i32
}
pub fn width(&self) -> i32 {
self.lines.iter().map(RichLine::width).max().unwrap_or(0)
}
pub fn wrap(&self, max_width: i32) -> RichText {
let max_width = max_width.max(1);
let mut out = Vec::new();
let mut pieces: Vec<Piece<'_>> = Vec::new();
for line in &self.lines {
pieces.clear();
collect_pieces(line, &mut pieces);
wrap_pieces(&pieces, max_width, &mut out);
}
RichText { lines: out }
}
pub fn draw(&self, s: &mut Surface, rect: Rect, align: HAlign) {
let rect = rect.intersect(s.bounds());
if rect.is_empty() {
return;
}
for (i, line) in self.lines.iter().enumerate().take(rect.h as usize) {
let y = rect.y + i as i32;
let line_w = line.width();
let x0 = match align {
HAlign::Left => rect.x,
HAlign::Center => rect.x + (rect.w - line_w).max(0) / 2,
HAlign::Right => rect.x + (rect.w - line_w).max(0),
};
if line_w <= rect.w {
let mut pen = x0;
for span in &line.spans {
pen += draw_span(s, pen, y, span);
}
} else {
draw_truncated(s, rect, y, line);
}
}
}
}
fn draw_span(s: &mut Surface, x: i32, y: i32, span: &Span) -> i32 {
let style = resolve_link(s, span);
s.draw_text(x, y, &span.text, style)
}
fn draw_truncated(s: &mut Surface, rect: Rect, y: i32, line: &RichLine) {
let budget = rect.w - 1; let mut pen = rect.x;
let mut used = 0i32;
let mut ellipsis_style = Style::EMPTY;
'spans: for span in &line.spans {
let style = resolve_link(s, span);
ellipsis_style = style;
for seg in text::segments(&span.text) {
if seg.width <= 0 {
continue;
}
if used + seg.width > budget {
break 'spans;
}
pen += s.draw_text(pen, y, seg.cluster, style);
used += seg.width;
}
}
s.draw_text(pen, y, "\u{2026}", ellipsis_style);
}
fn resolve_link(s: &mut Surface, span: &Span) -> Style {
match &span.link {
Some(url) => span.style.link(s.register_link(url)),
None => span.style,
}
}
struct Piece<'a> {
cluster: &'a str,
width: i32,
style: Style,
link: &'a Option<String>,
is_space: bool,
}
impl Piece<'_> {
fn emit(&self, line: &mut RichLine) {
line.push_run(self.cluster, self.style, self.link);
}
}
fn collect_pieces<'a>(line: &'a RichLine, out: &mut Vec<Piece<'a>>) {
for span in &line.spans {
for seg in text::segments(&span.text) {
if seg.width == 0 && seg.cluster.chars().any(char::is_control) {
continue;
}
out.push(Piece {
cluster: seg.cluster,
width: seg.width,
style: span.style,
link: &span.link,
is_space: seg.cluster.chars().next().is_some_and(char::is_whitespace),
});
}
}
}
fn wrap_pieces(pieces: &[Piece<'_>], max_width: i32, out: &mut Vec<RichLine>) {
let rows_before = out.len();
let mut current = RichLine::new();
let mut current_w = 0i32;
fn flush(current: &mut RichLine, current_w: &mut i32, out: &mut Vec<RichLine>) {
while let Some(last) = current.spans.last_mut() {
let trimmed = last.text.trim_end().len();
last.text.truncate(trimmed);
if last.text.is_empty() {
current.spans.pop();
} else {
break;
}
}
if !current.spans.is_empty() {
out.push(std::mem::take(current));
}
*current_w = 0;
}
let mut i = 0;
while i < pieces.len() {
if pieces[i].is_space {
let start = i;
let mut w = 0;
while i < pieces.len() && pieces[i].is_space {
w += pieces[i].width;
i += 1;
}
if current_w > 0 && current_w + w <= max_width {
for p in &pieces[start..i] {
p.emit(&mut current);
}
current_w += w;
} else if current_w > 0 {
flush(&mut current, &mut current_w, out);
}
continue;
}
let start = i;
let mut w = 0;
while i < pieces.len() && !pieces[i].is_space {
w += pieces[i].width;
i += 1;
}
if current_w > 0 && current_w + w > max_width {
flush(&mut current, &mut current_w, out);
}
if w <= max_width {
for p in &pieces[start..i] {
p.emit(&mut current);
}
current_w += w;
} else {
for p in &pieces[start..i] {
if current_w > 0 && current_w + p.width > max_width {
flush(&mut current, &mut current_w, out);
}
p.emit(&mut current);
current_w += p.width;
}
}
}
flush(&mut current, &mut current_w, out);
if out.len() == rows_before {
out.push(RichLine::new()); }
}
#[cfg(test)]
#[path = "rich_tests.rs"]
mod tests;