use ratatui_core::style::{Color, Modifier, Style};
use crate::geometry::Padding;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BorderStyle {
#[default]
Rounded,
Plain,
Thick,
None,
}
impl BorderStyle {
pub fn glyphs(self) -> BorderGlyphs {
match self {
BorderStyle::Rounded => BorderGlyphs::new('╭', '╮', '╰', '╯', '─', '│'),
BorderStyle::Plain => BorderGlyphs::new('┌', '┐', '└', '┘', '─', '│'),
BorderStyle::Thick => BorderGlyphs::new('┏', '┓', '┗', '┛', '━', '┃'),
BorderStyle::None => BorderGlyphs::new(' ', ' ', ' ', ' ', ' ', ' '),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BorderGlyphs {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
}
impl BorderGlyphs {
const fn new(
top_left: char,
top_right: char,
bottom_left: char,
bottom_right: char,
horizontal: char,
vertical: char,
) -> Self {
Self {
top_left,
top_right,
bottom_left,
bottom_right,
horizontal,
vertical,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Theme {
pub background: Color,
pub surface: Color,
pub text: Color,
pub muted: Color,
pub dim: Color,
pub accent: Color,
pub accent_alt: Color,
pub border: Color,
pub border_focused: Color,
pub selection_bg: Color,
pub selection_fg: Color,
pub code: CodeTheme,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CodeTheme {
pub heading: Color,
pub link: Color,
pub background: Color,
pub text: Color,
pub label: Color,
pub keyword: Color,
pub function: Color,
pub type_name: Color,
pub constant: Color,
pub string: Color,
pub comment: Color,
pub punctuation: Color,
}
impl Default for Theme {
fn default() -> Self {
Theme {
background: Color::Rgb(20, 18, 20),
surface: Color::Rgb(34, 28, 30),
text: Color::Rgb(235, 230, 230),
muted: Color::Rgb(150, 140, 142),
dim: Color::Rgb(90, 74, 78),
accent: Color::Rgb(200, 60, 70),
accent_alt: Color::Rgb(230, 140, 90),
border: Color::Rgb(90, 74, 78),
border_focused: Color::Rgb(200, 60, 70),
selection_bg: Color::Rgb(120, 30, 40),
selection_fg: Color::Rgb(240, 235, 235),
code: CodeTheme::default(),
}
}
}
impl Default for CodeTheme {
fn default() -> Self {
CodeTheme {
heading: Color::Rgb(235, 230, 230),
link: Color::Rgb(120, 160, 210),
background: Color::Rgb(28, 24, 26),
text: Color::Rgb(210, 205, 205),
label: Color::Rgb(150, 140, 142),
keyword: Color::Rgb(210, 120, 90),
function: Color::Rgb(120, 160, 210),
type_name: Color::Rgb(126, 170, 176),
constant: Color::Rgb(200, 160, 120),
string: Color::Rgb(150, 180, 140),
comment: Color::Rgb(120, 110, 112),
punctuation: Color::Rgb(150, 140, 142),
}
}
}
impl Theme {
pub fn text_style(&self) -> Style {
Style::default().fg(self.text)
}
pub fn muted_style(&self) -> Style {
Style::default().fg(self.muted)
}
pub fn accent_style(&self) -> Style {
Style::default()
.fg(self.accent)
.add_modifier(Modifier::BOLD)
}
pub fn border_color(&self, focused: bool) -> Color {
if focused {
self.border_focused
} else {
self.border
}
}
pub fn selection_style(&self) -> Style {
Style::default()
.bg(self.selection_bg)
.fg(self.selection_fg)
.add_modifier(Modifier::BOLD)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StyleBundle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub add_modifier: Modifier,
pub border: Option<BorderStyle>,
pub padding: Option<Padding>,
}
impl StyleBundle {
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 modifier(mut self, m: Modifier) -> Self {
self.add_modifier |= m;
self
}
pub fn bold(self) -> Self {
self.modifier(Modifier::BOLD)
}
pub fn italic(self) -> Self {
self.modifier(Modifier::ITALIC)
}
pub fn underlined(self) -> Self {
self.modifier(Modifier::UNDERLINED)
}
pub fn crossed_out(self) -> Self {
self.modifier(Modifier::CROSSED_OUT)
}
pub fn border(mut self, border: BorderStyle) -> Self {
self.border = Some(border);
self
}
pub fn padding(mut self, padding: Padding) -> Self {
self.padding = Some(padding);
self
}
pub fn apply(&self, base: Style) -> Style {
let mut style = base;
if let Some(fg) = self.fg {
style = style.fg(fg);
}
if let Some(bg) = self.bg {
style = style.bg(bg);
}
style.add_modifier(self.add_modifier)
}
pub fn to_style(&self) -> Style {
self.apply(Style::default())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
Panel,
Heading,
Link,
InlineCode,
Emphasis,
Strong,
Strikethrough,
ListMarker,
TaskMarker,
Rule,
ImageMarker,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StyleSheet {
pub panel: StyleBundle,
pub heading: StyleBundle,
pub link: StyleBundle,
pub inline_code: StyleBundle,
pub emphasis: StyleBundle,
pub strong: StyleBundle,
pub strikethrough: StyleBundle,
pub list_marker: StyleBundle,
pub task_marker: StyleBundle,
pub rule: StyleBundle,
pub image_marker: StyleBundle,
}
impl StyleSheet {
pub fn from_theme(theme: &Theme) -> Self {
let code = &theme.code;
StyleSheet {
panel: StyleBundle::new(),
heading: StyleBundle::new().fg(code.heading).bold(),
link: StyleBundle::new().fg(code.link).underlined(),
inline_code: StyleBundle::new().fg(code.text).bg(code.background),
emphasis: StyleBundle::new().italic(),
strong: StyleBundle::new().bold(),
strikethrough: StyleBundle::new().crossed_out(),
list_marker: StyleBundle::new().fg(theme.accent_alt),
task_marker: StyleBundle::new().fg(theme.accent),
rule: StyleBundle::new().fg(theme.dim),
image_marker: StyleBundle::new().fg(theme.accent),
}
}
pub fn resolve(&self, role: Role) -> StyleBundle {
match role {
Role::Panel => self.panel,
Role::Heading => self.heading,
Role::Link => self.link,
Role::InlineCode => self.inline_code,
Role::Emphasis => self.emphasis,
Role::Strong => self.strong,
Role::Strikethrough => self.strikethrough,
Role::ListMarker => self.list_marker,
Role::TaskMarker => self.task_marker,
Role::Rule => self.rule,
Role::ImageMarker => self.image_marker,
}
}
}
impl Default for StyleSheet {
fn default() -> Self {
Self::from_theme(&Theme::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::rainbow_theme;
use ratatui_core::style::Modifier;
#[test]
fn theme_helper_styles_map_to_slots() {
let t = rainbow_theme();
assert_eq!(t.text_style().fg, Some(t.text));
assert_eq!(t.muted_style().fg, Some(t.muted));
assert_eq!(t.accent_style().fg, Some(t.accent));
assert!(t.accent_style().add_modifier.contains(Modifier::BOLD));
assert_eq!(t.border_color(false), t.border);
assert_eq!(t.border_color(true), t.border_focused);
let sel = t.selection_style();
assert_eq!(sel.bg, Some(t.selection_bg));
assert_eq!(sel.fg, Some(t.selection_fg));
assert!(sel.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn default_theme_is_the_toolkit_identity_not_a_host_brand() {
use ratatui_core::style::Color;
let t = Theme::default();
assert_eq!(t.accent, Color::Rgb(200, 60, 70));
assert_eq!(t.selection_bg, Color::Rgb(120, 30, 40));
assert_ne!(t.accent, Color::Rgb(45, 91, 158));
}
#[test]
fn bundle_apply_overrides_color_but_only_adds_modifiers() {
use ratatui_core::style::Color;
let base = Style::default().fg(Color::Red);
let emphasized = StyleBundle::new().italic().apply(base);
assert_eq!(emphasized.fg, Some(Color::Red), "color inherited");
assert!(emphasized.add_modifier.contains(Modifier::ITALIC));
let recolored = StyleBundle::new().fg(Color::Green).bold().apply(base);
assert_eq!(recolored.fg, Some(Color::Green));
assert!(recolored.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn from_theme_reproduces_the_pre_stylesheet_styles() {
let t = rainbow_theme();
let s = StyleSheet::from_theme(&t);
assert_eq!(s.heading.fg, Some(t.code.heading));
assert!(s.heading.add_modifier.contains(Modifier::BOLD));
assert_eq!(s.link.fg, Some(t.code.link));
assert!(s.link.add_modifier.contains(Modifier::UNDERLINED));
assert_eq!(s.inline_code.fg, Some(t.code.text));
assert_eq!(s.inline_code.bg, Some(t.code.background));
assert_eq!(s.emphasis.fg, None);
assert!(s.emphasis.add_modifier.contains(Modifier::ITALIC));
}
#[test]
fn resolve_matches_named_fields() {
let s = StyleSheet::from_theme(&rainbow_theme());
assert_eq!(s.resolve(Role::Heading), s.heading);
assert_eq!(s.resolve(Role::Link), s.link);
assert_eq!(s.resolve(Role::Panel), s.panel);
}
#[test]
fn overriding_one_role_leaves_the_rest_at_theme_defaults() {
use ratatui_core::style::Color;
let t = rainbow_theme();
let sheet = StyleSheet {
link: StyleBundle::new().fg(Color::Green).bold(),
..StyleSheet::from_theme(&t)
};
assert_eq!(sheet.link.fg, Some(Color::Green));
assert!(sheet.link.add_modifier.contains(Modifier::BOLD));
assert_eq!(sheet.heading.fg, Some(t.code.heading));
}
}