use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Debug, Default)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub dim: bool,
pub strikethrough: bool,
pub reverse: bool,
}
impl Style {
pub fn new() -> Self {
Self::default()
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
pub fn reverse(mut self) -> Self {
self.reverse = true;
self
}
pub fn red() -> Self {
Self::new().fg(Color::RED)
}
pub fn green() -> Self {
Self::new().fg(Color::GREEN)
}
pub fn blue() -> Self {
Self::new().fg(Color::BLUE)
}
pub fn yellow() -> Self {
Self::new().fg(Color::YELLOW)
}
pub fn cyan() -> Self {
Self::new().fg(Color::CYAN)
}
pub fn magenta() -> Self {
Self::new().fg(Color::MAGENTA)
}
pub fn white() -> Self {
Self::new().fg(Color::WHITE)
}
fn to_modifier(&self) -> Modifier {
let mut m = Modifier::empty();
if self.bold {
m |= Modifier::BOLD;
}
if self.italic {
m |= Modifier::ITALIC;
}
if self.underline {
m |= Modifier::UNDERLINE;
}
if self.dim {
m |= Modifier::DIM;
}
if self.strikethrough {
m |= Modifier::CROSSED_OUT;
}
if self.reverse {
m |= Modifier::REVERSE;
}
m
}
}
#[derive(Clone, Debug)]
pub struct Span {
pub text: String,
pub style: Style,
pub link: Option<String>,
}
impl Span {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: Style::default(),
link: None,
}
}
pub fn styled(text: impl Into<String>, style: Style) -> Self {
Self {
text: text.into(),
style,
link: None,
}
}
pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
Self {
text: text.into(),
style: Style::new().fg(Color::CYAN).underline(),
link: Some(url.into()),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn href(mut self, url: impl Into<String>) -> Self {
self.link = Some(url.into());
self
}
pub fn width(&self) -> usize {
unicode_width::UnicodeWidthStr::width(self.text.as_str())
}
}
pub struct RichText {
spans: Vec<Span>,
default_style: Style,
props: WidgetProps,
}
impl RichText {
pub fn new() -> Self {
Self {
spans: Vec::new(),
default_style: Style::default(),
props: WidgetProps::new(),
}
}
pub fn plain(text: impl Into<String>) -> Self {
Self::new().push(text, Style::default())
}
pub fn markup(text: &str) -> Self {
let mut rich = Self::new();
rich.parse_markup(text);
rich
}
pub fn push(mut self, text: impl Into<String>, style: Style) -> Self {
self.spans.push(Span::styled(text, style));
self
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.spans.push(Span::new(text));
self
}
pub fn push_link(mut self, text: impl Into<String>, url: impl Into<String>) -> Self {
self.spans.push(Span::link(text, url));
self
}
pub fn span(mut self, span: Span) -> Self {
self.spans.push(span);
self
}
pub fn default_style(mut self, style: Style) -> Self {
self.default_style = style;
self
}
pub fn append(&mut self, text: impl Into<String>, style: Style) {
self.spans.push(Span::styled(text, style));
}
pub fn append_link(&mut self, text: impl Into<String>, url: impl Into<String>) {
self.spans.push(Span::link(text, url));
}
pub fn width(&self) -> usize {
self.spans.iter().map(|s| s.width()).sum()
}
pub fn len(&self) -> usize {
self.spans.len()
}
pub fn is_empty(&self) -> bool {
self.spans.is_empty()
}
pub fn clear(&mut self) {
self.spans.clear();
}
fn parse_markup(&mut self, text: &str) {
let mut current_style = Style::default();
let mut current_link: Option<String> = None;
let mut buffer = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '[' {
if !buffer.is_empty() {
let mut span = Span::styled(buffer.clone(), current_style.clone());
if let Some(ref url) = current_link {
span.link = Some(url.clone());
}
self.spans.push(span);
buffer.clear();
}
let mut tag = String::new();
for c in chars.by_ref() {
if c == ']' {
break;
}
tag.push(c);
}
if tag == "/" {
current_style = Style::default();
current_link = None;
continue;
}
for part in tag.split_whitespace() {
if let Some(link) = part.strip_prefix("link=") {
current_link = Some(link.to_string());
current_style.underline = true;
if current_style.fg.is_none() {
current_style.fg = Some(Color::CYAN);
}
} else {
match part.to_lowercase().as_str() {
"bold" | "b" => current_style.bold = true,
"italic" | "i" => current_style.italic = true,
"underline" | "u" => current_style.underline = true,
"dim" => current_style.dim = true,
"strike" | "s" => current_style.strikethrough = true,
"reverse" | "rev" => current_style.reverse = true,
"red" => current_style.fg = Some(Color::RED),
"green" => current_style.fg = Some(Color::GREEN),
"blue" => current_style.fg = Some(Color::BLUE),
"yellow" => current_style.fg = Some(Color::YELLOW),
"cyan" => current_style.fg = Some(Color::CYAN),
"magenta" => current_style.fg = Some(Color::MAGENTA),
"white" => current_style.fg = Some(Color::WHITE),
"black" => current_style.fg = Some(Color::BLACK),
"on_red" => current_style.bg = Some(Color::RED),
"on_green" => current_style.bg = Some(Color::GREEN),
"on_blue" => current_style.bg = Some(Color::BLUE),
"on_yellow" => current_style.bg = Some(Color::YELLOW),
"on_cyan" => current_style.bg = Some(Color::CYAN),
"on_magenta" => current_style.bg = Some(Color::MAGENTA),
"on_white" => current_style.bg = Some(Color::WHITE),
"on_black" => current_style.bg = Some(Color::BLACK),
_ => {}
}
}
}
} else {
buffer.push(ch);
}
}
if !buffer.is_empty() {
let mut span = Span::styled(buffer, current_style);
if let Some(url) = current_link {
span.link = Some(url);
}
self.spans.push(span);
}
}
}
impl Default for RichText {
fn default() -> Self {
Self::new()
}
}
impl View for RichText {
crate::impl_view_meta!("RichText");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let mut x: u16 = 0;
for span in &self.spans {
let hyperlink_id = span
.link
.as_ref()
.map(|url| ctx.buffer.register_hyperlink(url));
let modifier = span.style.to_modifier();
for ch in span.text.chars() {
if x >= area.width {
break;
}
let char_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
let mut cell = Cell::new(ch);
cell.fg = span.style.fg;
cell.bg = span.style.bg;
cell.modifier = modifier;
cell.hyperlink_id = hyperlink_id;
ctx.set(x, 0, cell);
if char_width == 2 && x + 1 < area.width {
let mut cont = Cell::continuation();
cont.bg = span.style.bg;
cont.hyperlink_id = hyperlink_id;
ctx.set(x + 1, 0, cont);
}
x += char_width;
}
}
}
}
impl_styled_view!(RichText);
impl_props_builders!(RichText);
pub fn rich_text() -> RichText {
RichText::new()
}
pub fn markup(text: &str) -> RichText {
RichText::markup(text)
}
pub fn span(text: impl Into<String>) -> Span {
Span::new(text)
}
pub fn style() -> Style {
Style::new()
}