From 3af164bc1d740e40ebf90673007673b038d8ecb0 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Mon, 17 Nov 2025 19:24:28 -0600 Subject: [PATCH] refactor: unify theme colors for terminal and TUI rendering --- core/src/theme.rs | 375 ++++++++++++++++++++++------------ docs/src/appendices/themes.md | 52 +++++ ui/src/renderer.rs | 111 ++++++++-- 3 files changed, 389 insertions(+), 149 deletions(-) diff --git a/core/src/theme.rs b/core/src/theme.rs index 69fd506..bdf4c8a 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -1,6 +1,32 @@ use owo_colors::{OwoColorize, Style}; use terminal_colorsaurus::{QueryOptions, background_color}; +/// RGB color value for use with both owo-colors and ratatui +#[derive(Debug, Clone, Copy)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Color { + pub const fn new(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } +} + +impl From for Style { + fn from(color: Color) -> Self { + Style::new().truecolor(color.r, color.g, color.b) + } +} + +impl From<&Color> for Style { + fn from(color: &Color) -> Self { + Style::new().truecolor(color.r, color.g, color.b) + } +} + /// Detects if the terminal background is dark. /// /// Uses [terminal_colorsaurus] to query the terminal background color. @@ -18,21 +44,23 @@ pub fn detect_is_dark() -> bool { } } -/// Color theme abstraction for slides with owo-colors with semantic roles for consistent theming across the application. +/// Color theme abstraction for slides with semantic roles for consistent theming across the application. /// -/// Avoids dynamic dispatch by using compile-time color assignments. +/// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) +/// and ratatui Color (for TUI rendering) via Into implementations. #[derive(Debug, Clone)] pub struct ThemeColors { - pub heading: Style, - pub body: Style, - pub accent: Style, - pub code: Style, - pub dimmed: Style, - pub code_fence: Style, - pub rule: Style, - pub list_marker: Style, - pub blockquote_border: Style, - pub table_border: Style, + pub heading: Color, + pub heading_bold: bool, + pub body: Color, + pub accent: Color, + pub code: Color, + pub dimmed: Color, + pub code_fence: Color, + pub rule: Color, + pub list_marker: Color, + pub blockquote_border: Color, + pub table_border: Color, } impl Default for ThemeColors { @@ -44,92 +72,87 @@ impl Default for ThemeColors { impl ThemeColors { /// Apply heading style to text pub fn heading<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.heading) + let mut style: Style = (&self.heading).into(); + if self.heading_bold { + style = style.bold(); + } + text.style(style) } /// Apply body style to text pub fn body<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.body) + text.style((&self.body).into()) } /// Apply accent style to text pub fn accent<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.accent) + text.style((&self.accent).into()) } /// Apply code style to text pub fn code<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.code) + text.style((&self.code).into()) } /// Apply dimmed style to text pub fn dimmed<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.dimmed) + text.style((&self.dimmed).into()) } /// Apply code fence style to text pub fn code_fence<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.code_fence) + text.style((&self.code_fence).into()) } /// Apply horizontal rule style to text pub fn rule<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.rule) + text.style((&self.rule).into()) } /// Apply list marker style to text pub fn list_marker<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.list_marker) + text.style((&self.list_marker).into()) } /// Apply blockquote border style to text pub fn blockquote_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.blockquote_border) + text.style((&self.blockquote_border).into()) } /// Apply table border style to text pub fn table_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { - text.style(self.table_border) + text.style((&self.table_border).into()) } /// Create an oxocarbon-based theme. pub fn basic(is_dark: bool) -> Self { if is_dark { Self { - // green - heading: Style::new().truecolor(66, 190, 101).bold(), - body: Style::new().truecolor(242, 244, 248), - // pink - accent: Style::new().truecolor(238, 83, 150), - // blue - code: Style::new().truecolor(51, 177, 255), - // gray - dimmed: Style::new().truecolor(82, 82, 82), - code_fence: Style::new().truecolor(82, 82, 82), - rule: Style::new().truecolor(82, 82, 82), - // light blue - list_marker: Style::new().truecolor(120, 169, 255), - blockquote_border: Style::new().truecolor(82, 82, 82), - table_border: Style::new().truecolor(82, 82, 82), + heading: Color::new(66, 190, 101), // green + heading_bold: true, + body: Color::new(242, 244, 248), + accent: Color::new(238, 83, 150), // pink + code: Color::new(51, 177, 255), // blue + dimmed: Color::new(82, 82, 82), // gray + code_fence: Color::new(82, 82, 82), + rule: Color::new(82, 82, 82), + list_marker: Color::new(120, 169, 255), // light blue + blockquote_border: Color::new(82, 82, 82), + table_border: Color::new(82, 82, 82), } } else { - // Oxocarbon Light variant Self { - // green - heading: Style::new().truecolor(66, 190, 101).bold(), - body: Style::new().truecolor(57, 57, 57), - // orange - accent: Style::new().truecolor(255, 111, 0), - // blue - code: Style::new().truecolor(15, 98, 254), - // dark gray - dimmed: Style::new().truecolor(22, 22, 22), - code_fence: Style::new().truecolor(22, 22, 22), - rule: Style::new().truecolor(22, 22, 22), - // pink - list_marker: Style::new().truecolor(238, 83, 150), - blockquote_border: Style::new().truecolor(22, 22, 22), - table_border: Style::new().truecolor(22, 22, 22), + heading: Color::new(66, 190, 101), // green + heading_bold: true, + body: Color::new(57, 57, 57), + accent: Color::new(255, 111, 0), // orange + code: Color::new(15, 98, 254), // blue + dimmed: Color::new(22, 22, 22), // dark gray + code_fence: Color::new(22, 22, 22), + rule: Color::new(22, 22, 22), + list_marker: Color::new(238, 83, 150), // pink + blockquote_border: Color::new(22, 22, 22), + table_border: Color::new(22, 22, 22), } } } @@ -141,29 +164,31 @@ impl ThemeColors { pub fn monokai(is_dark: bool) -> Self { if is_dark { Self { - heading: Style::new().truecolor(249, 38, 114).bold(), // pink - body: Style::new().truecolor(248, 248, 242), // off-white - accent: Style::new().truecolor(230, 219, 116), // yellow - code: Style::new().truecolor(166, 226, 46), // green - dimmed: Style::new().truecolor(117, 113, 94), // brown-gray - code_fence: Style::new().truecolor(117, 113, 94), - rule: Style::new().truecolor(117, 113, 94), - list_marker: Style::new().truecolor(230, 219, 116), - blockquote_border: Style::new().truecolor(117, 113, 94), - table_border: Style::new().truecolor(117, 113, 94), + heading: Color::new(249, 38, 114), // pink + heading_bold: true, + body: Color::new(248, 248, 242), // off-white + accent: Color::new(230, 219, 116), // yellow + code: Color::new(166, 226, 46), // green + dimmed: Color::new(117, 113, 94), // brown-gray + code_fence: Color::new(117, 113, 94), + rule: Color::new(117, 113, 94), + list_marker: Color::new(230, 219, 116), + blockquote_border: Color::new(117, 113, 94), + table_border: Color::new(117, 113, 94), } } else { Self { - heading: Style::new().truecolor(200, 30, 90).bold(), // darker pink - body: Style::new().truecolor(39, 40, 34), // dark gray - accent: Style::new().truecolor(180, 170, 80), // darker yellow - code: Style::new().truecolor(100, 150, 30), // darker green - dimmed: Style::new().truecolor(150, 150, 150), // light gray - code_fence: Style::new().truecolor(150, 150, 150), - rule: Style::new().truecolor(150, 150, 150), - list_marker: Style::new().truecolor(180, 170, 80), - blockquote_border: Style::new().truecolor(150, 150, 150), - table_border: Style::new().truecolor(150, 150, 150), + heading: Color::new(200, 30, 90), // darker pink + heading_bold: true, + body: Color::new(39, 40, 34), // dark gray + accent: Color::new(180, 170, 80), // darker yellow + code: Color::new(100, 150, 30), // darker green + dimmed: Color::new(150, 150, 150), // light gray + code_fence: Color::new(150, 150, 150), + rule: Color::new(150, 150, 150), + list_marker: Color::new(180, 170, 80), + blockquote_border: Color::new(150, 150, 150), + table_border: Color::new(150, 150, 150), } } } @@ -175,29 +200,31 @@ impl ThemeColors { pub fn dracula(is_dark: bool) -> Self { if is_dark { Self { - heading: Style::new().truecolor(255, 121, 198).bold(), // pink - body: Style::new().truecolor(248, 248, 242), - accent: Style::new().truecolor(139, 233, 253), // cyan - code: Style::new().truecolor(80, 250, 123), // green - dimmed: Style::new().truecolor(98, 114, 164), - code_fence: Style::new().truecolor(98, 114, 164), - rule: Style::new().truecolor(98, 114, 164), - list_marker: Style::new().truecolor(241, 250, 140), // yellow - blockquote_border: Style::new().truecolor(98, 114, 164), - table_border: Style::new().truecolor(98, 114, 164), + heading: Color::new(255, 121, 198), // pink + heading_bold: true, + body: Color::new(248, 248, 242), + accent: Color::new(139, 233, 253), // cyan + code: Color::new(80, 250, 123), // green + dimmed: Color::new(98, 114, 164), + code_fence: Color::new(98, 114, 164), + rule: Color::new(98, 114, 164), + list_marker: Color::new(241, 250, 140), // yellow + blockquote_border: Color::new(98, 114, 164), + table_border: Color::new(98, 114, 164), } } else { Self { - heading: Style::new().truecolor(200, 80, 160).bold(), // darker pink - body: Style::new().truecolor(40, 42, 54), - accent: Style::new().truecolor(80, 150, 180), // darker cyan - code: Style::new().truecolor(50, 160, 80), // darker green - dimmed: Style::new().truecolor(150, 150, 150), // light gray - code_fence: Style::new().truecolor(150, 150, 150), - rule: Style::new().truecolor(150, 150, 150), - list_marker: Style::new().truecolor(180, 170, 90), // darker yellow - blockquote_border: Style::new().truecolor(150, 150, 150), - table_border: Style::new().truecolor(150, 150, 150), + heading: Color::new(200, 80, 160), // darker pink + heading_bold: true, + body: Color::new(40, 42, 54), + accent: Color::new(80, 150, 180), // darker cyan + code: Color::new(50, 160, 80), // darker green + dimmed: Color::new(150, 150, 150), // light gray + code_fence: Color::new(150, 150, 150), + rule: Color::new(150, 150, 150), + list_marker: Color::new(180, 170, 90), // darker yellow + blockquote_border: Color::new(150, 150, 150), + table_border: Color::new(150, 150, 150), } } } @@ -208,29 +235,31 @@ impl ThemeColors { pub fn solarized(is_dark: bool) -> Self { if is_dark { Self { - heading: Style::new().truecolor(38, 139, 210).bold(), - body: Style::new().truecolor(131, 148, 150), - accent: Style::new().truecolor(42, 161, 152), - code: Style::new().truecolor(133, 153, 0), - dimmed: Style::new().truecolor(88, 110, 117), - code_fence: Style::new().truecolor(88, 110, 117), - rule: Style::new().truecolor(88, 110, 117), - list_marker: Style::new().truecolor(181, 137, 0), - blockquote_border: Style::new().truecolor(88, 110, 117), - table_border: Style::new().truecolor(88, 110, 117), + heading: Color::new(38, 139, 210), + heading_bold: true, + body: Color::new(131, 148, 150), + accent: Color::new(42, 161, 152), + code: Color::new(133, 153, 0), + dimmed: Color::new(88, 110, 117), + code_fence: Color::new(88, 110, 117), + rule: Color::new(88, 110, 117), + list_marker: Color::new(181, 137, 0), + blockquote_border: Color::new(88, 110, 117), + table_border: Color::new(88, 110, 117), } } else { Self { - heading: Style::new().truecolor(38, 139, 210).bold(), - body: Style::new().truecolor(101, 123, 131), - accent: Style::new().truecolor(42, 161, 152), - code: Style::new().truecolor(133, 153, 0), - dimmed: Style::new().truecolor(147, 161, 161), - code_fence: Style::new().truecolor(147, 161, 161), - rule: Style::new().truecolor(147, 161, 161), - list_marker: Style::new().truecolor(181, 137, 0), - blockquote_border: Style::new().truecolor(147, 161, 161), - table_border: Style::new().truecolor(147, 161, 161), + heading: Color::new(38, 139, 210), + heading_bold: true, + body: Color::new(101, 123, 131), + accent: Color::new(42, 161, 152), + code: Color::new(133, 153, 0), + dimmed: Color::new(147, 161, 161), + code_fence: Color::new(147, 161, 161), + rule: Color::new(147, 161, 161), + list_marker: Color::new(181, 137, 0), + blockquote_border: Color::new(147, 161, 161), + table_border: Color::new(147, 161, 161), } } } @@ -239,29 +268,31 @@ impl ThemeColors { pub fn nord(is_dark: bool) -> Self { if is_dark { Self { - heading: Style::new().truecolor(136, 192, 208).bold(), // nord8 - light blue - body: Style::new().truecolor(216, 222, 233), // nord4 - accent: Style::new().truecolor(143, 188, 187), // nord7 - teal - code: Style::new().truecolor(163, 190, 140), // nord14 - green - dimmed: Style::new().truecolor(76, 86, 106), // nord3 - code_fence: Style::new().truecolor(76, 86, 106), - rule: Style::new().truecolor(76, 86, 106), - list_marker: Style::new().truecolor(235, 203, 139), // nord13 - yellow - blockquote_border: Style::new().truecolor(76, 86, 106), - table_border: Style::new().truecolor(76, 86, 106), + heading: Color::new(136, 192, 208), // nord8 - light blue + heading_bold: true, + body: Color::new(216, 222, 233), // nord4 + accent: Color::new(143, 188, 187), // nord7 - teal + code: Color::new(163, 190, 140), // nord14 - green + dimmed: Color::new(76, 86, 106), // nord3 + code_fence: Color::new(76, 86, 106), + rule: Color::new(76, 86, 106), + list_marker: Color::new(235, 203, 139), // nord13 - yellow + blockquote_border: Color::new(76, 86, 106), + table_border: Color::new(76, 86, 106), } } else { Self { - heading: Style::new().truecolor(94, 129, 172).bold(), // darker blue - body: Style::new().truecolor(46, 52, 64), - accent: Style::new().truecolor(136, 192, 208), // blue - code: Style::new().truecolor(163, 190, 140), // green - dimmed: Style::new().truecolor(143, 157, 175), - code_fence: Style::new().truecolor(143, 157, 175), - rule: Style::new().truecolor(143, 157, 175), - list_marker: Style::new().truecolor(235, 203, 139), // yellow - blockquote_border: Style::new().truecolor(143, 157, 175), - table_border: Style::new().truecolor(143, 157, 175), + heading: Color::new(94, 129, 172), // darker blue + heading_bold: true, + body: Color::new(46, 52, 64), + accent: Color::new(136, 192, 208), // blue + code: Color::new(163, 190, 140), // green + dimmed: Color::new(143, 157, 175), + code_fence: Color::new(143, 157, 175), + rule: Color::new(143, 157, 175), + list_marker: Color::new(235, 203, 139), // yellow + blockquote_border: Color::new(143, 157, 175), + table_border: Color::new(143, 157, 175), } } } @@ -306,6 +337,41 @@ impl ThemeRegistry { mod tests { use super::*; + #[test] + fn color_new() { + let color = Color::new(255, 128, 64); + assert_eq!(color.r, 255); + assert_eq!(color.g, 128); + assert_eq!(color.b, 64); + } + + #[test] + fn color_into_style() { + let color = Color::new(100, 150, 200); + let style: Style = color.into(); + let text = "Test"; + let styled = text.style(style); + assert!(styled.to_string().contains("Test")); + } + + #[test] + fn color_ref_into_style() { + let color = Color::new(100, 150, 200); + let style: Style = (&color).into(); + let text = "Test"; + let styled = text.style(style); + assert!(styled.to_string().contains("Test")); + } + + #[test] + fn color_into_style_preserves_rgb() { + let color = Color::new(255, 0, 128); + let style: Style = color.into(); + let styled_text = "Test".style(style); + let output = styled_text.to_string(); + assert!(output.contains("Test")); + } + #[test] fn theme_colors_default() { let theme = ThemeColors::default(); @@ -462,4 +528,47 @@ mod tests { let result = detect_is_dark(); assert!(result == true || result == false); } + + #[test] + fn theme_colors_stores_correct_colors() { + let theme = ThemeColors::basic(true); + assert_eq!(theme.heading.r, 66); + assert_eq!(theme.heading.g, 190); + assert_eq!(theme.heading.b, 101); + assert!(theme.heading_bold); + } + + #[test] + fn theme_colors_heading_applies_bold() { + let theme = ThemeColors::basic(true); + let text = "Bold Heading"; + let styled = theme.heading(&text); + assert!(styled.to_string().contains("Bold Heading")); + } + + #[test] + fn theme_colors_all_semantic_roles() { + let theme = ThemeColors::default(); + + assert!(theme.heading(&"Test").to_string().contains("Test")); + assert!(theme.body(&"Test").to_string().contains("Test")); + assert!(theme.accent(&"Test").to_string().contains("Test")); + assert!(theme.code(&"Test").to_string().contains("Test")); + assert!(theme.dimmed(&"Test").to_string().contains("Test")); + assert!(theme.code_fence(&"Test").to_string().contains("Test")); + assert!(theme.rule(&"Test").to_string().contains("Test")); + assert!(theme.list_marker(&"Test").to_string().contains("Test")); + assert!(theme.blockquote_border(&"Test").to_string().contains("Test")); + assert!(theme.table_border(&"Test").to_string().contains("Test")); + } + + #[test] + fn theme_colors_light_vs_dark() { + let dark = ThemeColors::basic(true); + let light = ThemeColors::basic(false); + + assert_eq!(dark.body.r, 242); + assert_eq!(light.body.r, 57); + assert_ne!(dark.body.r, light.body.r); + } } diff --git a/docs/src/appendices/themes.md b/docs/src/appendices/themes.md index 9654e10..1f8fa0d 100644 --- a/docs/src/appendices/themes.md +++ b/docs/src/appendices/themes.md @@ -132,3 +132,55 @@ The printer uses Unicode box-drawing characters for clean visual output: - `•` for unordered list markers Tables automatically calculate column widths based on content and available terminal width. + +## Default Theme + +The application's default slide theme is based on Oxocarbon + +### Dark + +```yml +- scheme: "Oxocarbon Dark" + author: "shaunsingh/IBM" + palette: + base00: "#161616" + base01: "#262626" + base02: "#393939" + base03: "#525252" + base04: "#dde1e6" + base05: "#f2f4f8" + base06: "#ffffff" + base07: "#08bdba" + base08: "#3ddbd9" + base09: "#78a9ff" + base0A: "#ee5396" + base0B: "#33b1ff" + base0C: "#ff7eb6" + base0D: "#42be65" + base0E: "#be95ff" + base0F: "#82cfff" +``` + +### Light + +```yml +- scheme: "Oxocarbon Light" + author: "shaunsingh/IBM" + palette: + base00: "#f2f4f8" + base01: "#dde1e6" + base02: "#525252" + base03: "#161616" + base04: "#262626" + base05: "#393939" + base06: "#525252" + base07: "#08bdba" + base08: "#ff7eb6" + base09: "#ee5396" + base0A: "#FF6F00" + base0B: "#0f62fe" + base0C: "#673AB7" + base0D: "#42be65" + base0E: "#be95ff" + base0F: "#37474F" +``` diff --git a/ui/src/renderer.rs b/ui/src/renderer.rs index fe0ba56..3f18e2c 100644 --- a/ui/src/renderer.rs +++ b/ui/src/renderer.rs @@ -45,7 +45,7 @@ fn get_prefix(level: u8) -> &'static str { /// Render a heading with size based on level fn render_heading(level: u8, spans: &[TextSpan], theme: &ThemeColors, lines: &mut Vec>) { let prefix = get_prefix(level); - let heading_style = to_ratatui_style(&theme.heading); + let heading_style = to_ratatui_style(&theme.heading, theme.heading_bold); let mut line_spans = vec![Span::styled(prefix.to_string(), heading_style)]; for span in spans { @@ -63,8 +63,8 @@ fn render_paragraph(spans: &[TextSpan], theme: &ThemeColors, lines: &mut Vec>) { - let fence_style = to_ratatui_style(&theme.code_fence); - let code_style = to_ratatui_style(&theme.code); + let fence_style = to_ratatui_style(&theme.code_fence, false); + let code_style = to_ratatui_style(&theme.code, false); if let Some(lang) = &code.language { lines.push(Line::from(Span::styled(format!("```{}", lang), fence_style))); @@ -81,7 +81,7 @@ fn render_code_block(code: &CodeBlock, theme: &ThemeColors, lines: &mut Vec>, indent: usize) { - let marker_style = to_ratatui_style(&theme.list_marker); + let marker_style = to_ratatui_style(&theme.list_marker, false); for (idx, item) in list.items.iter().enumerate() { let prefix = if list.ordered { @@ -106,14 +106,14 @@ fn render_list(list: &List, theme: &ThemeColors, lines: &mut Vec>, /// Render a horizontal rule fn render_rule(theme: &ThemeColors, lines: &mut Vec>) { - let rule_style = to_ratatui_style(&theme.rule); + let rule_style = to_ratatui_style(&theme.rule, false); let rule = "─".repeat(60); lines.push(Line::from(Span::styled(rule, rule_style))); } /// Render a blockquote with indentation fn render_blockquote(blocks: &[Block], theme: &ThemeColors, lines: &mut Vec>) { - let border_style = to_ratatui_style(&theme.blockquote_border); + let border_style = to_ratatui_style(&theme.blockquote_border, false); for block in blocks { match block { @@ -133,7 +133,7 @@ fn render_blockquote(blocks: &[Block], theme: &ThemeColors, lines: &mut Vec>) { - let border_style = to_ratatui_style(&theme.table_border); + let border_style = to_ratatui_style(&theme.table_border, false); if !table.headers.is_empty() { let mut header_line = Vec::new(); @@ -174,11 +174,11 @@ fn create_span(text_span: &TextSpan, theme: &ThemeColors, is_heading: bool) -> S /// Apply theme colors and text styling fn apply_theme_style(theme: &ThemeColors, text_style: &TextStyle, is_heading: bool) -> Style { let mut style = if is_heading { - to_ratatui_style(&theme.heading) + to_ratatui_style(&theme.heading, theme.heading_bold) } else if text_style.code { - to_ratatui_style(&theme.code) + to_ratatui_style(&theme.code, false) } else { - to_ratatui_style(&theme.body) + to_ratatui_style(&theme.body, false) }; if text_style.bold { @@ -194,12 +194,15 @@ fn apply_theme_style(theme: &ThemeColors, text_style: &TextStyle, is_heading: bo style } -/// Convert owo-colors Style to ratatui Style -/// -/// Since owo-colors Style is opaque, we return a default ratatui style. -/// The theme provides semantic meaning; actual visual styling is defined here. -fn to_ratatui_style(_owo_style: &owo_colors::Style) -> Style { - Style::default() +/// Convert theme Color to ratatui Style with RGB colors +fn to_ratatui_style(color: &slides_core::theme::Color, bold: bool) -> Style { + let mut style = Style::default().fg(ratatui::style::Color::Rgb(color.r, color.g, color.b)); + + if bold { + style = style.add_modifier(Modifier::BOLD); + } + + style } #[cfg(test)] @@ -262,4 +265,80 @@ mod tests { let text = render_slide_content(&blocks, &theme); assert!(!text.lines.is_empty()); } + + #[test] + fn to_ratatui_style_converts_color() { + use slides_core::theme::Color; + + let color = Color::new(255, 128, 64); + let style = to_ratatui_style(&color, false); + + assert_eq!( + style.fg, + Some(ratatui::style::Color::Rgb(255, 128, 64)) + ); + } + + #[test] + fn to_ratatui_style_applies_bold() { + use slides_core::theme::Color; + + let color = Color::new(100, 150, 200); + let style = to_ratatui_style(&color, true); + + assert_eq!( + style.fg, + Some(ratatui::style::Color::Rgb(100, 150, 200)) + ); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn to_ratatui_style_no_bold_when_false() { + use slides_core::theme::Color; + + let color = Color::new(100, 150, 200); + let style = to_ratatui_style(&color, false); + + assert!(!style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn render_heading_uses_theme_colors() { + let theme = ThemeColors::default(); + let blocks = vec![Block::Heading { + level: 1, + spans: vec![TextSpan::plain("Colored Heading")], + }]; + + let text = render_slide_content(&blocks, &theme); + assert!(!text.lines.is_empty()); + assert!(text.lines.len() >= 1); + } + + #[test] + fn apply_theme_style_respects_heading_bold() { + let theme = ThemeColors::default(); + let text_style = TextStyle::default(); + + let style = apply_theme_style(&theme, &text_style, true); + assert!(style.add_modifier.contains(Modifier::BOLD)); + } + + #[test] + fn apply_theme_style_uses_code_color_for_code() { + let theme = ThemeColors::default(); + let mut text_style = TextStyle::default(); + text_style.code = true; + + let style = apply_theme_style(&theme, &text_style, false); + assert_eq!( + style.fg, + Some(ratatui::style::Color::Rgb( + theme.code.r, + theme.code.g, + theme.code.b + )) + ); + } } -- 2.51.2