magical markdown slides

refactor: unify theme colors for terminal and TUI rendering

+389 -149
+242 -133
core/src/theme.rs
··· 1 1 use owo_colors::{OwoColorize, Style}; 2 2 use terminal_colorsaurus::{QueryOptions, background_color}; 3 3 4 + /// RGB color value for use with both owo-colors and ratatui 5 + #[derive(Debug, Clone, Copy)] 6 + pub struct Color { 7 + pub r: u8, 8 + pub g: u8, 9 + pub b: u8, 10 + } 11 + 12 + impl Color { 13 + pub const fn new(r: u8, g: u8, b: u8) -> Self { 14 + Self { r, g, b } 15 + } 16 + } 17 + 18 + impl From<Color> for Style { 19 + fn from(color: Color) -> Self { 20 + Style::new().truecolor(color.r, color.g, color.b) 21 + } 22 + } 23 + 24 + impl From<&Color> for Style { 25 + fn from(color: &Color) -> Self { 26 + Style::new().truecolor(color.r, color.g, color.b) 27 + } 28 + } 29 + 4 30 /// Detects if the terminal background is dark. 5 31 /// 6 32 /// Uses [terminal_colorsaurus] to query the terminal background color. ··· 18 44 } 19 45 } 20 46 21 - /// Color theme abstraction for slides with owo-colors with semantic roles for consistent theming across the application. 47 + /// Color theme abstraction for slides with semantic roles for consistent theming across the application. 22 48 /// 23 - /// Avoids dynamic dispatch by using compile-time color assignments. 49 + /// Stores RGB colors that can be converted to both owo-colors Style (for terminal output) 50 + /// and ratatui Color (for TUI rendering) via Into implementations. 24 51 #[derive(Debug, Clone)] 25 52 pub struct ThemeColors { 26 - pub heading: Style, 27 - pub body: Style, 28 - pub accent: Style, 29 - pub code: Style, 30 - pub dimmed: Style, 31 - pub code_fence: Style, 32 - pub rule: Style, 33 - pub list_marker: Style, 34 - pub blockquote_border: Style, 35 - pub table_border: Style, 53 + pub heading: Color, 54 + pub heading_bold: bool, 55 + pub body: Color, 56 + pub accent: Color, 57 + pub code: Color, 58 + pub dimmed: Color, 59 + pub code_fence: Color, 60 + pub rule: Color, 61 + pub list_marker: Color, 62 + pub blockquote_border: Color, 63 + pub table_border: Color, 36 64 } 37 65 38 66 impl Default for ThemeColors { ··· 44 72 impl ThemeColors { 45 73 /// Apply heading style to text 46 74 pub fn heading<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 47 - text.style(self.heading) 75 + let mut style: Style = (&self.heading).into(); 76 + if self.heading_bold { 77 + style = style.bold(); 78 + } 79 + text.style(style) 48 80 } 49 81 50 82 /// Apply body style to text 51 83 pub fn body<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 52 - text.style(self.body) 84 + text.style((&self.body).into()) 53 85 } 54 86 55 87 /// Apply accent style to text 56 88 pub fn accent<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 57 - text.style(self.accent) 89 + text.style((&self.accent).into()) 58 90 } 59 91 60 92 /// Apply code style to text 61 93 pub fn code<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 62 - text.style(self.code) 94 + text.style((&self.code).into()) 63 95 } 64 96 65 97 /// Apply dimmed style to text 66 98 pub fn dimmed<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 67 - text.style(self.dimmed) 99 + text.style((&self.dimmed).into()) 68 100 } 69 101 70 102 /// Apply code fence style to text 71 103 pub fn code_fence<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 72 - text.style(self.code_fence) 104 + text.style((&self.code_fence).into()) 73 105 } 74 106 75 107 /// Apply horizontal rule style to text 76 108 pub fn rule<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 77 - text.style(self.rule) 109 + text.style((&self.rule).into()) 78 110 } 79 111 80 112 /// Apply list marker style to text 81 113 pub fn list_marker<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 82 - text.style(self.list_marker) 114 + text.style((&self.list_marker).into()) 83 115 } 84 116 85 117 /// Apply blockquote border style to text 86 118 pub fn blockquote_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 87 - text.style(self.blockquote_border) 119 + text.style((&self.blockquote_border).into()) 88 120 } 89 121 90 122 /// Apply table border style to text 91 123 pub fn table_border<'a, T: OwoColorize>(&self, text: &'a T) -> owo_colors::Styled<&'a T> { 92 - text.style(self.table_border) 124 + text.style((&self.table_border).into()) 93 125 } 94 126 95 127 /// Create an oxocarbon-based theme. 96 128 pub fn basic(is_dark: bool) -> Self { 97 129 if is_dark { 98 130 Self { 99 - // green 100 - heading: Style::new().truecolor(66, 190, 101).bold(), 101 - body: Style::new().truecolor(242, 244, 248), 102 - // pink 103 - accent: Style::new().truecolor(238, 83, 150), 104 - // blue 105 - code: Style::new().truecolor(51, 177, 255), 106 - // gray 107 - dimmed: Style::new().truecolor(82, 82, 82), 108 - code_fence: Style::new().truecolor(82, 82, 82), 109 - rule: Style::new().truecolor(82, 82, 82), 110 - // light blue 111 - list_marker: Style::new().truecolor(120, 169, 255), 112 - blockquote_border: Style::new().truecolor(82, 82, 82), 113 - table_border: Style::new().truecolor(82, 82, 82), 131 + heading: Color::new(66, 190, 101), // green 132 + heading_bold: true, 133 + body: Color::new(242, 244, 248), 134 + accent: Color::new(238, 83, 150), // pink 135 + code: Color::new(51, 177, 255), // blue 136 + dimmed: Color::new(82, 82, 82), // gray 137 + code_fence: Color::new(82, 82, 82), 138 + rule: Color::new(82, 82, 82), 139 + list_marker: Color::new(120, 169, 255), // light blue 140 + blockquote_border: Color::new(82, 82, 82), 141 + table_border: Color::new(82, 82, 82), 114 142 } 115 143 } else { 116 - // Oxocarbon Light variant 117 144 Self { 118 - // green 119 - heading: Style::new().truecolor(66, 190, 101).bold(), 120 - body: Style::new().truecolor(57, 57, 57), 121 - // orange 122 - accent: Style::new().truecolor(255, 111, 0), 123 - // blue 124 - code: Style::new().truecolor(15, 98, 254), 125 - // dark gray 126 - dimmed: Style::new().truecolor(22, 22, 22), 127 - code_fence: Style::new().truecolor(22, 22, 22), 128 - rule: Style::new().truecolor(22, 22, 22), 129 - // pink 130 - list_marker: Style::new().truecolor(238, 83, 150), 131 - blockquote_border: Style::new().truecolor(22, 22, 22), 132 - table_border: Style::new().truecolor(22, 22, 22), 145 + heading: Color::new(66, 190, 101), // green 146 + heading_bold: true, 147 + body: Color::new(57, 57, 57), 148 + accent: Color::new(255, 111, 0), // orange 149 + code: Color::new(15, 98, 254), // blue 150 + dimmed: Color::new(22, 22, 22), // dark gray 151 + code_fence: Color::new(22, 22, 22), 152 + rule: Color::new(22, 22, 22), 153 + list_marker: Color::new(238, 83, 150), // pink 154 + blockquote_border: Color::new(22, 22, 22), 155 + table_border: Color::new(22, 22, 22), 133 156 } 134 157 } 135 158 } ··· 141 164 pub fn monokai(is_dark: bool) -> Self { 142 165 if is_dark { 143 166 Self { 144 - heading: Style::new().truecolor(249, 38, 114).bold(), // pink 145 - body: Style::new().truecolor(248, 248, 242), // off-white 146 - accent: Style::new().truecolor(230, 219, 116), // yellow 147 - code: Style::new().truecolor(166, 226, 46), // green 148 - dimmed: Style::new().truecolor(117, 113, 94), // brown-gray 149 - code_fence: Style::new().truecolor(117, 113, 94), 150 - rule: Style::new().truecolor(117, 113, 94), 151 - list_marker: Style::new().truecolor(230, 219, 116), 152 - blockquote_border: Style::new().truecolor(117, 113, 94), 153 - table_border: Style::new().truecolor(117, 113, 94), 167 + heading: Color::new(249, 38, 114), // pink 168 + heading_bold: true, 169 + body: Color::new(248, 248, 242), // off-white 170 + accent: Color::new(230, 219, 116), // yellow 171 + code: Color::new(166, 226, 46), // green 172 + dimmed: Color::new(117, 113, 94), // brown-gray 173 + code_fence: Color::new(117, 113, 94), 174 + rule: Color::new(117, 113, 94), 175 + list_marker: Color::new(230, 219, 116), 176 + blockquote_border: Color::new(117, 113, 94), 177 + table_border: Color::new(117, 113, 94), 154 178 } 155 179 } else { 156 180 Self { 157 - heading: Style::new().truecolor(200, 30, 90).bold(), // darker pink 158 - body: Style::new().truecolor(39, 40, 34), // dark gray 159 - accent: Style::new().truecolor(180, 170, 80), // darker yellow 160 - code: Style::new().truecolor(100, 150, 30), // darker green 161 - dimmed: Style::new().truecolor(150, 150, 150), // light gray 162 - code_fence: Style::new().truecolor(150, 150, 150), 163 - rule: Style::new().truecolor(150, 150, 150), 164 - list_marker: Style::new().truecolor(180, 170, 80), 165 - blockquote_border: Style::new().truecolor(150, 150, 150), 166 - table_border: Style::new().truecolor(150, 150, 150), 181 + heading: Color::new(200, 30, 90), // darker pink 182 + heading_bold: true, 183 + body: Color::new(39, 40, 34), // dark gray 184 + accent: Color::new(180, 170, 80), // darker yellow 185 + code: Color::new(100, 150, 30), // darker green 186 + dimmed: Color::new(150, 150, 150), // light gray 187 + code_fence: Color::new(150, 150, 150), 188 + rule: Color::new(150, 150, 150), 189 + list_marker: Color::new(180, 170, 80), 190 + blockquote_border: Color::new(150, 150, 150), 191 + table_border: Color::new(150, 150, 150), 167 192 } 168 193 } 169 194 } ··· 175 200 pub fn dracula(is_dark: bool) -> Self { 176 201 if is_dark { 177 202 Self { 178 - heading: Style::new().truecolor(255, 121, 198).bold(), // pink 179 - body: Style::new().truecolor(248, 248, 242), 180 - accent: Style::new().truecolor(139, 233, 253), // cyan 181 - code: Style::new().truecolor(80, 250, 123), // green 182 - dimmed: Style::new().truecolor(98, 114, 164), 183 - code_fence: Style::new().truecolor(98, 114, 164), 184 - rule: Style::new().truecolor(98, 114, 164), 185 - list_marker: Style::new().truecolor(241, 250, 140), // yellow 186 - blockquote_border: Style::new().truecolor(98, 114, 164), 187 - table_border: Style::new().truecolor(98, 114, 164), 203 + heading: Color::new(255, 121, 198), // pink 204 + heading_bold: true, 205 + body: Color::new(248, 248, 242), 206 + accent: Color::new(139, 233, 253), // cyan 207 + code: Color::new(80, 250, 123), // green 208 + dimmed: Color::new(98, 114, 164), 209 + code_fence: Color::new(98, 114, 164), 210 + rule: Color::new(98, 114, 164), 211 + list_marker: Color::new(241, 250, 140), // yellow 212 + blockquote_border: Color::new(98, 114, 164), 213 + table_border: Color::new(98, 114, 164), 188 214 } 189 215 } else { 190 216 Self { 191 - heading: Style::new().truecolor(200, 80, 160).bold(), // darker pink 192 - body: Style::new().truecolor(40, 42, 54), 193 - accent: Style::new().truecolor(80, 150, 180), // darker cyan 194 - code: Style::new().truecolor(50, 160, 80), // darker green 195 - dimmed: Style::new().truecolor(150, 150, 150), // light gray 196 - code_fence: Style::new().truecolor(150, 150, 150), 197 - rule: Style::new().truecolor(150, 150, 150), 198 - list_marker: Style::new().truecolor(180, 170, 90), // darker yellow 199 - blockquote_border: Style::new().truecolor(150, 150, 150), 200 - table_border: Style::new().truecolor(150, 150, 150), 217 + heading: Color::new(200, 80, 160), // darker pink 218 + heading_bold: true, 219 + body: Color::new(40, 42, 54), 220 + accent: Color::new(80, 150, 180), // darker cyan 221 + code: Color::new(50, 160, 80), // darker green 222 + dimmed: Color::new(150, 150, 150), // light gray 223 + code_fence: Color::new(150, 150, 150), 224 + rule: Color::new(150, 150, 150), 225 + list_marker: Color::new(180, 170, 90), // darker yellow 226 + blockquote_border: Color::new(150, 150, 150), 227 + table_border: Color::new(150, 150, 150), 201 228 } 202 229 } 203 230 } ··· 208 235 pub fn solarized(is_dark: bool) -> Self { 209 236 if is_dark { 210 237 Self { 211 - heading: Style::new().truecolor(38, 139, 210).bold(), 212 - body: Style::new().truecolor(131, 148, 150), 213 - accent: Style::new().truecolor(42, 161, 152), 214 - code: Style::new().truecolor(133, 153, 0), 215 - dimmed: Style::new().truecolor(88, 110, 117), 216 - code_fence: Style::new().truecolor(88, 110, 117), 217 - rule: Style::new().truecolor(88, 110, 117), 218 - list_marker: Style::new().truecolor(181, 137, 0), 219 - blockquote_border: Style::new().truecolor(88, 110, 117), 220 - table_border: Style::new().truecolor(88, 110, 117), 238 + heading: Color::new(38, 139, 210), 239 + heading_bold: true, 240 + body: Color::new(131, 148, 150), 241 + accent: Color::new(42, 161, 152), 242 + code: Color::new(133, 153, 0), 243 + dimmed: Color::new(88, 110, 117), 244 + code_fence: Color::new(88, 110, 117), 245 + rule: Color::new(88, 110, 117), 246 + list_marker: Color::new(181, 137, 0), 247 + blockquote_border: Color::new(88, 110, 117), 248 + table_border: Color::new(88, 110, 117), 221 249 } 222 250 } else { 223 251 Self { 224 - heading: Style::new().truecolor(38, 139, 210).bold(), 225 - body: Style::new().truecolor(101, 123, 131), 226 - accent: Style::new().truecolor(42, 161, 152), 227 - code: Style::new().truecolor(133, 153, 0), 228 - dimmed: Style::new().truecolor(147, 161, 161), 229 - code_fence: Style::new().truecolor(147, 161, 161), 230 - rule: Style::new().truecolor(147, 161, 161), 231 - list_marker: Style::new().truecolor(181, 137, 0), 232 - blockquote_border: Style::new().truecolor(147, 161, 161), 233 - table_border: Style::new().truecolor(147, 161, 161), 252 + heading: Color::new(38, 139, 210), 253 + heading_bold: true, 254 + body: Color::new(101, 123, 131), 255 + accent: Color::new(42, 161, 152), 256 + code: Color::new(133, 153, 0), 257 + dimmed: Color::new(147, 161, 161), 258 + code_fence: Color::new(147, 161, 161), 259 + rule: Color::new(147, 161, 161), 260 + list_marker: Color::new(181, 137, 0), 261 + blockquote_border: Color::new(147, 161, 161), 262 + table_border: Color::new(147, 161, 161), 234 263 } 235 264 } 236 265 } ··· 239 268 pub fn nord(is_dark: bool) -> Self { 240 269 if is_dark { 241 270 Self { 242 - heading: Style::new().truecolor(136, 192, 208).bold(), // nord8 - light blue 243 - body: Style::new().truecolor(216, 222, 233), // nord4 244 - accent: Style::new().truecolor(143, 188, 187), // nord7 - teal 245 - code: Style::new().truecolor(163, 190, 140), // nord14 - green 246 - dimmed: Style::new().truecolor(76, 86, 106), // nord3 247 - code_fence: Style::new().truecolor(76, 86, 106), 248 - rule: Style::new().truecolor(76, 86, 106), 249 - list_marker: Style::new().truecolor(235, 203, 139), // nord13 - yellow 250 - blockquote_border: Style::new().truecolor(76, 86, 106), 251 - table_border: Style::new().truecolor(76, 86, 106), 271 + heading: Color::new(136, 192, 208), // nord8 - light blue 272 + heading_bold: true, 273 + body: Color::new(216, 222, 233), // nord4 274 + accent: Color::new(143, 188, 187), // nord7 - teal 275 + code: Color::new(163, 190, 140), // nord14 - green 276 + dimmed: Color::new(76, 86, 106), // nord3 277 + code_fence: Color::new(76, 86, 106), 278 + rule: Color::new(76, 86, 106), 279 + list_marker: Color::new(235, 203, 139), // nord13 - yellow 280 + blockquote_border: Color::new(76, 86, 106), 281 + table_border: Color::new(76, 86, 106), 252 282 } 253 283 } else { 254 284 Self { 255 - heading: Style::new().truecolor(94, 129, 172).bold(), // darker blue 256 - body: Style::new().truecolor(46, 52, 64), 257 - accent: Style::new().truecolor(136, 192, 208), // blue 258 - code: Style::new().truecolor(163, 190, 140), // green 259 - dimmed: Style::new().truecolor(143, 157, 175), 260 - code_fence: Style::new().truecolor(143, 157, 175), 261 - rule: Style::new().truecolor(143, 157, 175), 262 - list_marker: Style::new().truecolor(235, 203, 139), // yellow 263 - blockquote_border: Style::new().truecolor(143, 157, 175), 264 - table_border: Style::new().truecolor(143, 157, 175), 285 + heading: Color::new(94, 129, 172), // darker blue 286 + heading_bold: true, 287 + body: Color::new(46, 52, 64), 288 + accent: Color::new(136, 192, 208), // blue 289 + code: Color::new(163, 190, 140), // green 290 + dimmed: Color::new(143, 157, 175), 291 + code_fence: Color::new(143, 157, 175), 292 + rule: Color::new(143, 157, 175), 293 + list_marker: Color::new(235, 203, 139), // yellow 294 + blockquote_border: Color::new(143, 157, 175), 295 + table_border: Color::new(143, 157, 175), 265 296 } 266 297 } 267 298 } ··· 305 336 #[cfg(test)] 306 337 mod tests { 307 338 use super::*; 339 + 340 + #[test] 341 + fn color_new() { 342 + let color = Color::new(255, 128, 64); 343 + assert_eq!(color.r, 255); 344 + assert_eq!(color.g, 128); 345 + assert_eq!(color.b, 64); 346 + } 347 + 348 + #[test] 349 + fn color_into_style() { 350 + let color = Color::new(100, 150, 200); 351 + let style: Style = color.into(); 352 + let text = "Test"; 353 + let styled = text.style(style); 354 + assert!(styled.to_string().contains("Test")); 355 + } 356 + 357 + #[test] 358 + fn color_ref_into_style() { 359 + let color = Color::new(100, 150, 200); 360 + let style: Style = (&color).into(); 361 + let text = "Test"; 362 + let styled = text.style(style); 363 + assert!(styled.to_string().contains("Test")); 364 + } 365 + 366 + #[test] 367 + fn color_into_style_preserves_rgb() { 368 + let color = Color::new(255, 0, 128); 369 + let style: Style = color.into(); 370 + let styled_text = "Test".style(style); 371 + let output = styled_text.to_string(); 372 + assert!(output.contains("Test")); 373 + } 308 374 309 375 #[test] 310 376 fn theme_colors_default() { ··· 461 527 fn detect_is_dark_returns_bool() { 462 528 let result = detect_is_dark(); 463 529 assert!(result == true || result == false); 530 + } 531 + 532 + #[test] 533 + fn theme_colors_stores_correct_colors() { 534 + let theme = ThemeColors::basic(true); 535 + assert_eq!(theme.heading.r, 66); 536 + assert_eq!(theme.heading.g, 190); 537 + assert_eq!(theme.heading.b, 101); 538 + assert!(theme.heading_bold); 539 + } 540 + 541 + #[test] 542 + fn theme_colors_heading_applies_bold() { 543 + let theme = ThemeColors::basic(true); 544 + let text = "Bold Heading"; 545 + let styled = theme.heading(&text); 546 + assert!(styled.to_string().contains("Bold Heading")); 547 + } 548 + 549 + #[test] 550 + fn theme_colors_all_semantic_roles() { 551 + let theme = ThemeColors::default(); 552 + 553 + assert!(theme.heading(&"Test").to_string().contains("Test")); 554 + assert!(theme.body(&"Test").to_string().contains("Test")); 555 + assert!(theme.accent(&"Test").to_string().contains("Test")); 556 + assert!(theme.code(&"Test").to_string().contains("Test")); 557 + assert!(theme.dimmed(&"Test").to_string().contains("Test")); 558 + assert!(theme.code_fence(&"Test").to_string().contains("Test")); 559 + assert!(theme.rule(&"Test").to_string().contains("Test")); 560 + assert!(theme.list_marker(&"Test").to_string().contains("Test")); 561 + assert!(theme.blockquote_border(&"Test").to_string().contains("Test")); 562 + assert!(theme.table_border(&"Test").to_string().contains("Test")); 563 + } 564 + 565 + #[test] 566 + fn theme_colors_light_vs_dark() { 567 + let dark = ThemeColors::basic(true); 568 + let light = ThemeColors::basic(false); 569 + 570 + assert_eq!(dark.body.r, 242); 571 + assert_eq!(light.body.r, 57); 572 + assert_ne!(dark.body.r, light.body.r); 464 573 } 465 574 }
+52
docs/src/appendices/themes.md
··· 132 132 - `•` for unordered list markers 133 133 134 134 Tables automatically calculate column widths based on content and available terminal width. 135 + 136 + ## Default Theme 137 + 138 + The application's default slide theme is based on Oxocarbon 139 + 140 + ### Dark 141 + 142 + ```yml 143 + - scheme: "Oxocarbon Dark" 144 + author: "shaunsingh/IBM" 145 + palette: 146 + base00: "#161616" 147 + base01: "#262626" 148 + base02: "#393939" 149 + base03: "#525252" 150 + base04: "#dde1e6" 151 + base05: "#f2f4f8" 152 + base06: "#ffffff" 153 + base07: "#08bdba" 154 + base08: "#3ddbd9" 155 + base09: "#78a9ff" 156 + base0A: "#ee5396" 157 + base0B: "#33b1ff" 158 + base0C: "#ff7eb6" 159 + base0D: "#42be65" 160 + base0E: "#be95ff" 161 + base0F: "#82cfff" 162 + ``` 163 + 164 + ### Light 165 + 166 + ```yml 167 + - scheme: "Oxocarbon Light" 168 + author: "shaunsingh/IBM" 169 + palette: 170 + base00: "#f2f4f8" 171 + base01: "#dde1e6" 172 + base02: "#525252" 173 + base03: "#161616" 174 + base04: "#262626" 175 + base05: "#393939" 176 + base06: "#525252" 177 + base07: "#08bdba" 178 + base08: "#ff7eb6" 179 + base09: "#ee5396" 180 + base0A: "#FF6F00" 181 + base0B: "#0f62fe" 182 + base0C: "#673AB7" 183 + base0D: "#42be65" 184 + base0E: "#be95ff" 185 + base0F: "#37474F" 186 + ```
+95 -16
ui/src/renderer.rs
··· 45 45 /// Render a heading with size based on level 46 46 fn render_heading(level: u8, spans: &[TextSpan], theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 47 47 let prefix = get_prefix(level); 48 - let heading_style = to_ratatui_style(&theme.heading); 48 + let heading_style = to_ratatui_style(&theme.heading, theme.heading_bold); 49 49 let mut line_spans = vec![Span::styled(prefix.to_string(), heading_style)]; 50 50 51 51 for span in spans { ··· 63 63 64 64 /// Render a code block with monospace styling 65 65 fn render_code_block(code: &CodeBlock, theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 66 - let fence_style = to_ratatui_style(&theme.code_fence); 67 - let code_style = to_ratatui_style(&theme.code); 66 + let fence_style = to_ratatui_style(&theme.code_fence, false); 67 + let code_style = to_ratatui_style(&theme.code, false); 68 68 69 69 if let Some(lang) = &code.language { 70 70 lines.push(Line::from(Span::styled(format!("```{}", lang), fence_style))); ··· 81 81 82 82 /// Render a list with bullets or numbers 83 83 fn render_list(list: &List, theme: &ThemeColors, lines: &mut Vec<Line<'static>>, indent: usize) { 84 - let marker_style = to_ratatui_style(&theme.list_marker); 84 + let marker_style = to_ratatui_style(&theme.list_marker, false); 85 85 86 86 for (idx, item) in list.items.iter().enumerate() { 87 87 let prefix = if list.ordered { ··· 106 106 107 107 /// Render a horizontal rule 108 108 fn render_rule(theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 109 - let rule_style = to_ratatui_style(&theme.rule); 109 + let rule_style = to_ratatui_style(&theme.rule, false); 110 110 let rule = "─".repeat(60); 111 111 lines.push(Line::from(Span::styled(rule, rule_style))); 112 112 } 113 113 114 114 /// Render a blockquote with indentation 115 115 fn render_blockquote(blocks: &[Block], theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 116 - let border_style = to_ratatui_style(&theme.blockquote_border); 116 + let border_style = to_ratatui_style(&theme.blockquote_border, false); 117 117 118 118 for block in blocks { 119 119 match block { ··· 133 133 134 134 /// Render a table with basic formatting 135 135 fn render_table(table: &Table, theme: &ThemeColors, lines: &mut Vec<Line<'static>>) { 136 - let border_style = to_ratatui_style(&theme.table_border); 136 + let border_style = to_ratatui_style(&theme.table_border, false); 137 137 138 138 if !table.headers.is_empty() { 139 139 let mut header_line = Vec::new(); ··· 174 174 /// Apply theme colors and text styling 175 175 fn apply_theme_style(theme: &ThemeColors, text_style: &TextStyle, is_heading: bool) -> Style { 176 176 let mut style = if is_heading { 177 - to_ratatui_style(&theme.heading) 177 + to_ratatui_style(&theme.heading, theme.heading_bold) 178 178 } else if text_style.code { 179 - to_ratatui_style(&theme.code) 179 + to_ratatui_style(&theme.code, false) 180 180 } else { 181 - to_ratatui_style(&theme.body) 181 + to_ratatui_style(&theme.body, false) 182 182 }; 183 183 184 184 if text_style.bold { ··· 194 194 style 195 195 } 196 196 197 - /// Convert owo-colors Style to ratatui Style 198 - /// 199 - /// Since owo-colors Style is opaque, we return a default ratatui style. 200 - /// The theme provides semantic meaning; actual visual styling is defined here. 201 - fn to_ratatui_style(_owo_style: &owo_colors::Style) -> Style { 202 - Style::default() 197 + /// Convert theme Color to ratatui Style with RGB colors 198 + fn to_ratatui_style(color: &slides_core::theme::Color, bold: bool) -> Style { 199 + let mut style = Style::default().fg(ratatui::style::Color::Rgb(color.r, color.g, color.b)); 200 + 201 + if bold { 202 + style = style.add_modifier(Modifier::BOLD); 203 + } 204 + 205 + style 203 206 } 204 207 205 208 #[cfg(test)] ··· 261 264 let theme = ThemeColors::default(); 262 265 let text = render_slide_content(&blocks, &theme); 263 266 assert!(!text.lines.is_empty()); 267 + } 268 + 269 + #[test] 270 + fn to_ratatui_style_converts_color() { 271 + use slides_core::theme::Color; 272 + 273 + let color = Color::new(255, 128, 64); 274 + let style = to_ratatui_style(&color, false); 275 + 276 + assert_eq!( 277 + style.fg, 278 + Some(ratatui::style::Color::Rgb(255, 128, 64)) 279 + ); 280 + } 281 + 282 + #[test] 283 + fn to_ratatui_style_applies_bold() { 284 + use slides_core::theme::Color; 285 + 286 + let color = Color::new(100, 150, 200); 287 + let style = to_ratatui_style(&color, true); 288 + 289 + assert_eq!( 290 + style.fg, 291 + Some(ratatui::style::Color::Rgb(100, 150, 200)) 292 + ); 293 + assert!(style.add_modifier.contains(Modifier::BOLD)); 294 + } 295 + 296 + #[test] 297 + fn to_ratatui_style_no_bold_when_false() { 298 + use slides_core::theme::Color; 299 + 300 + let color = Color::new(100, 150, 200); 301 + let style = to_ratatui_style(&color, false); 302 + 303 + assert!(!style.add_modifier.contains(Modifier::BOLD)); 304 + } 305 + 306 + #[test] 307 + fn render_heading_uses_theme_colors() { 308 + let theme = ThemeColors::default(); 309 + let blocks = vec![Block::Heading { 310 + level: 1, 311 + spans: vec![TextSpan::plain("Colored Heading")], 312 + }]; 313 + 314 + let text = render_slide_content(&blocks, &theme); 315 + assert!(!text.lines.is_empty()); 316 + assert!(text.lines.len() >= 1); 317 + } 318 + 319 + #[test] 320 + fn apply_theme_style_respects_heading_bold() { 321 + let theme = ThemeColors::default(); 322 + let text_style = TextStyle::default(); 323 + 324 + let style = apply_theme_style(&theme, &text_style, true); 325 + assert!(style.add_modifier.contains(Modifier::BOLD)); 326 + } 327 + 328 + #[test] 329 + fn apply_theme_style_uses_code_color_for_code() { 330 + let theme = ThemeColors::default(); 331 + let mut text_style = TextStyle::default(); 332 + text_style.code = true; 333 + 334 + let style = apply_theme_style(&theme, &text_style, false); 335 + assert_eq!( 336 + style.fg, 337 + Some(ratatui::style::Color::Rgb( 338 + theme.code.r, 339 + theme.code.g, 340 + theme.code.b 341 + )) 342 + ); 264 343 } 265 344 }