use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum UiColor {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
Gray,
Rgb {
r: u8,
g: u8,
b: u8,
},
Indexed {
index: u8,
},
}
impl From<UiColor> for peniko::Color {
fn from(c: UiColor) -> Self {
match c {
UiColor::Reset => peniko::Color::from_rgba8(0, 0, 0, 0),
UiColor::Black => peniko::Color::from_rgb8(12, 12, 12),
UiColor::Red => peniko::Color::from_rgb8(197, 15, 31),
UiColor::Green => peniko::Color::from_rgb8(19, 161, 14),
UiColor::Yellow => peniko::Color::from_rgb8(193, 156, 0),
UiColor::Blue => peniko::Color::from_rgb8(0, 55, 218),
UiColor::Magenta => peniko::Color::from_rgb8(136, 23, 152),
UiColor::Cyan => peniko::Color::from_rgb8(58, 150, 221),
UiColor::White => peniko::Color::from_rgb8(204, 204, 204),
UiColor::DarkGray => peniko::Color::from_rgb8(118, 118, 118),
UiColor::LightRed => peniko::Color::from_rgb8(231, 72, 86),
UiColor::LightGreen => peniko::Color::from_rgb8(22, 198, 12),
UiColor::LightYellow => peniko::Color::from_rgb8(249, 241, 165),
UiColor::LightBlue => peniko::Color::from_rgb8(59, 120, 255),
UiColor::LightMagenta => peniko::Color::from_rgb8(180, 0, 158),
UiColor::LightCyan => peniko::Color::from_rgb8(97, 214, 214),
UiColor::Gray => peniko::Color::from_rgb8(242, 242, 242),
UiColor::Rgb { r, g, b } => peniko::Color::from_rgb8(r, g, b),
UiColor::Indexed { index } => ansi256_to_peniko(index),
}
}
}
fn ansi256_to_peniko(idx: u8) -> peniko::Color {
match idx {
0 => peniko::Color::from_rgb8(12, 12, 12),
1 => peniko::Color::from_rgb8(197, 15, 31),
2 => peniko::Color::from_rgb8(19, 161, 14),
3 => peniko::Color::from_rgb8(193, 156, 0),
4 => peniko::Color::from_rgb8(0, 55, 218),
5 => peniko::Color::from_rgb8(136, 23, 152),
6 => peniko::Color::from_rgb8(58, 150, 221),
7 => peniko::Color::from_rgb8(204, 204, 204),
8 => peniko::Color::from_rgb8(118, 118, 118),
9 => peniko::Color::from_rgb8(231, 72, 86),
10 => peniko::Color::from_rgb8(22, 198, 12),
11 => peniko::Color::from_rgb8(249, 241, 165),
12 => peniko::Color::from_rgb8(59, 120, 255),
13 => peniko::Color::from_rgb8(180, 0, 158),
14 => peniko::Color::from_rgb8(97, 214, 214),
15 => peniko::Color::from_rgb8(242, 242, 242),
16..=231 => {
let n = idx - 16;
let b = n % 6;
let g = (n / 6) % 6;
let r = n / 36;
let channel = |v: u8| if v == 0 { 0 } else { 55 + v * 40 };
peniko::Color::from_rgb8(channel(r), channel(g), channel(b))
}
232..=255 => {
let v = 8 + (idx - 232) * 10;
peniko::Color::from_rgb8(v, v, v)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TextModifier {
Bold,
Dim,
Italic,
Underlined,
SlowBlink,
RapidBlink,
Reversed,
Hidden,
CrossedOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct FontWeight(pub f32);
impl FontWeight {
pub const THIN: Self = Self(100.0);
pub const EXTRA_LIGHT: Self = Self(200.0);
pub const LIGHT: Self = Self(300.0);
pub const NORMAL: Self = Self(400.0);
pub const MEDIUM: Self = Self(500.0);
pub const SEMI_BOLD: Self = Self(600.0);
pub const BOLD: Self = Self(700.0);
pub const EXTRA_BOLD: Self = Self(800.0);
pub const BLACK: Self = Self(900.0);
}
impl From<FontWeight> for parley::style::FontWeight {
fn from(w: FontWeight) -> Self {
Self::new(w.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum FontStyle {
Normal,
Italic,
Oblique(Option<f32>),
}
impl From<FontStyle> for parley::style::FontStyle {
fn from(s: FontStyle) -> Self {
match s {
FontStyle::Normal => Self::Normal,
FontStyle::Italic => Self::Italic,
FontStyle::Oblique(angle) => Self::Oblique(angle),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TextDecoration {
Underline,
Strikethrough,
Overline,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextStyle {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fg: Option<UiColor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bg: Option<UiColor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modifiers: Vec<TextModifier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font_weight: Option<FontWeight>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub decorations: Vec<TextDecoration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextSpan {
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<TextStyle>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextLine {
pub spans: Vec<TextSpan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<TextStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<TextAlign>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RichText {
pub lines: Vec<TextLine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<TextStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alignment: Option<TextAlign>,
}
#[derive(Debug, Clone, PartialEq, JsonSchema)]
pub enum ParagraphText {
Plain(String),
Rich(RichText),
}
impl serde::Serialize for ParagraphText {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Plain(s) => s.serialize(serializer),
Self::Rich(t) => t.serialize(serializer),
}
}
}
impl<'de> serde::Deserialize<'de> for ParagraphText {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => Ok(Self::Plain(s)),
serde_json::Value::Object(_) => serde_json::from_value::<RichText>(value)
.map(Self::Rich)
.map_err(serde::de::Error::custom),
other => Err(serde::de::Error::custom(format!(
"expected string or object for ParagraphText, got {other}"
))),
}
}
}
impl ParagraphText {
pub fn to_plain_string(&self) -> String {
match self {
Self::Plain(s) => s.clone(),
Self::Rich(t) => t
.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_str())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n"),
}
}
}
impl From<String> for ParagraphText {
fn from(s: String) -> Self {
Self::Plain(s)
}
}
impl From<&str> for ParagraphText {
fn from(s: &str) -> Self {
Self::Plain(s.to_string())
}
}
impl From<RichText> for ParagraphText {
fn from(t: RichText) -> Self {
Self::Rich(t)
}
}