From c7b66b7ca3b6aac3b6be801ec4d2d1d50c36605a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 21:33:15 +0000 Subject: [PATCH 1/5] feat: add per-slide background color via comment command Add support for setting a background color on individual slides using the `` comment command. This allows users to customize specific slides with different background colors. The color can be specified as: - A hex RGB value (e.g., `ff0000` for red) - A named color (e.g., `red`, `blue`) - A palette reference (e.g., `palette:primary`) When the command is encountered, the slide is re-cleared with the new background color, ensuring all subsequent content renders on that background. --- src/presentation/builder/comment.rs | 55 ++++++++++++++++++++++++++++- src/presentation/builder/error.rs | 3 ++ src/presentation/builder/mod.rs | 10 ++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/presentation/builder/comment.rs b/src/presentation/builder/comment.rs index a949e4ea..2b5d1e43 100644 --- a/src/presentation/builder/comment.rs +++ b/src/presentation/builder/comment.rs @@ -2,7 +2,7 @@ use crate::{ markdown::elements::{MarkdownElement, SourcePosition}, presentation::builder::{BuildResult, LayoutState, PresentationBuilder, error::InvalidPresentation}, render::operation::{LayoutGrid, RenderOperation}, - theme::{Alignment, ElementType}, + theme::{Alignment, ElementType, raw::RawColor}, }; use serde::Deserialize; use std::{fmt, num::NonZeroU8, path::PathBuf, str::FromStr}; @@ -116,6 +116,13 @@ impl PresentationBuilder<'_, '_> { self.push_detached_code_execution(handle)?; return Ok(()); } + CommentCommand::BackgroundColor(color) => { + let color = color.resolve(&self.theme.palette).map_err(|e| { + self.invalid_presentation(source_position, InvalidPresentation::InvalidColor(e)) + })?; + self.slide_state.background_color = color; + self.apply_slide_background_color(); + } }; // Don't push line breaks for any comments. self.slide_state.ignore_element_line_break = true; @@ -186,6 +193,7 @@ impl PresentationBuilder<'_, '_> { #[serde(rename_all = "snake_case")] pub(crate) enum CommentCommand { Alignment(CommentCommandAlignment), + BackgroundColor(RawColor), Column(usize), EndSlide, FontSize(u8), @@ -231,6 +239,7 @@ impl CommentCommand { format!(""), format!(""), format!(""), + format!(""), ] } } @@ -295,6 +304,14 @@ mod tests { assert_eq!(parsed, expected); } + #[rstest] + #[case::hex("background_color: ff0000")] + #[case::named("background_color: red")] + fn background_color_parsing(#[case] input: &str) { + let parsed: CommentCommand = input.parse().expect("deserialization failed"); + assert!(matches!(parsed, CommentCommand::BackgroundColor(_))); + } + #[rstest] #[case::multiline("hello\nworld")] #[case::many_open_braces("{{{")] @@ -762,4 +779,40 @@ hi let err = Test::new(input).resources_path(path).expect_invalid(); assert!(err.to_string().contains("was already imported"), "{err:?}"); } + + #[test] + fn background_color() { + use crate::markdown::text_style::Color; + + let input = " + + +hello +"; + let (lines, styles) = Test::new(input) + .render() + .rows(4) + .columns(10) + .map_background(Color::Rgb { r: 255, g: 0, b: 0 }, 'R') + .into_parts(); + + // Find the line containing "hello" and verify it has red background + let hello_line_idx = lines.iter().position(|l| l.contains("hello")).expect("hello not found"); + assert!( + styles[hello_line_idx].starts_with("RRRRR"), + "Expected text to have red background.\nLines: {:?}\nStyles: {:?}", + lines, + styles + ); + } + + #[test] + fn background_color_invalid() { + let input = " + + +hello +"; + Test::new(input).expect_invalid(); + } } diff --git a/src/presentation/builder/error.rs b/src/presentation/builder/error.rs index e14bf117..e33571c0 100644 --- a/src/presentation/builder/error.rs +++ b/src/presentation/builder/error.rs @@ -122,6 +122,9 @@ pub(crate) enum InvalidPresentation { #[error("snippet id '{0}' already exists")] SnippetAlreadyExists(String), + + #[error("invalid color: {0}")] + InvalidColor(#[from] UndefinedPaletteColorError), } #[derive(Clone, Debug)] diff --git a/src/presentation/builder/mod.rs b/src/presentation/builder/mod.rs index 2c4fe361..5ef357e5 100644 --- a/src/presentation/builder/mod.rs +++ b/src/presentation/builder/mod.rs @@ -339,6 +339,15 @@ impl<'a, 'b> PresentationBuilder<'a, 'b> { self.chunk_operations.push(RenderOperation::SetColors(colors)); } + fn apply_slide_background_color(&mut self) { + if let Some(bg_color) = self.slide_state.background_color { + let style = self.theme.default_style.style; + let colors = Colors { background: Some(bg_color), foreground: style.colors.foreground }; + self.set_colors(colors); + self.chunk_operations.push(RenderOperation::ClearScreen); + } + } + fn push_slide_prelude(&mut self) { let style = self.theme.default_style.style; self.set_colors(style.colors); @@ -600,6 +609,7 @@ struct SlideState { alignment: Option, skip_slide: bool, last_layout_comment: Option, + background_color: Option, } #[derive(Clone, Debug, Default)] From fd0279592feab01976a5307a92af523209dcd41c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 21:55:06 +0000 Subject: [PATCH 2/5] fix: apply background_color to entire slide retroactively Instead of applying background color immediately when the command is encountered (which would clear content rendered before the command), now the background color is stored and applied retroactively to all SetColors operations in the slide when the slide is terminated. This ensures the entire slide uses the custom background color, regardless of where the `` command appears in the slide content. --- src/presentation/builder/mod.rs | 27 ++++++++++++++++++++------- src/presentation/mod.rs | 8 ++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/presentation/builder/mod.rs b/src/presentation/builder/mod.rs index 5ef357e5..3217e558 100644 --- a/src/presentation/builder/mod.rs +++ b/src/presentation/builder/mod.rs @@ -340,12 +340,10 @@ impl<'a, 'b> PresentationBuilder<'a, 'b> { } fn apply_slide_background_color(&mut self) { - if let Some(bg_color) = self.slide_state.background_color { - let style = self.theme.default_style.style; - let colors = Colors { background: Some(bg_color), foreground: style.colors.foreground }; - self.set_colors(colors); - self.chunk_operations.push(RenderOperation::ClearScreen); - } + // We only store the background color here; it will be applied retroactively + // to the slide's initial SetColors operation when the slide is terminated. + // This ensures the entire slide has the background color, regardless of + // where the command appears in the slide content. } fn push_slide_prelude(&mut self) { @@ -518,8 +516,23 @@ impl<'a, 'b> PresentationBuilder<'a, 'b> { } fn terminate_slide(&mut self) { - let operations = mem::take(&mut self.chunk_operations); + let mut operations = mem::take(&mut self.chunk_operations); let mutators = mem::take(&mut self.chunk_mutators); + + // Apply background color to all SetColors operations in the current chunk + // so the entire slide uses the custom background + if let Some(bg_color) = self.slide_state.background_color { + for op in &mut operations { + if let RenderOperation::SetColors(colors) = op { + colors.background = Some(bg_color); + } + } + // Also apply to any previously created chunks (from pause commands) + for chunk in &mut self.slide_chunks { + chunk.apply_background_color(bg_color); + } + } + // Don't allow a last empty pause in slide since it adds nothing if self.slide_chunks.is_empty() || !Self::is_chunk_empty(&operations) { self.slide_chunks.push(SlideChunk::new(operations, mutators)); diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index 4e1938ce..00dd8cf7 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -377,6 +377,14 @@ impl SlideChunk { mutator.apply_all_mutations(); } } + + pub(crate) fn apply_background_color(&mut self, bg_color: crate::markdown::text_style::Color) { + for op in &mut self.operations { + if let RenderOperation::SetColors(colors) = op { + colors.background = Some(bg_color); + } + } + } } pub(crate) trait ChunkMutator: Debug { From 9d33c1e7490fbf6e29b81fdae51cafea2c25f3b0 Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 23 Jan 2026 14:06:25 -0800 Subject: [PATCH 3/5] chore(lint): format --- src/presentation/builder/comment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/presentation/builder/comment.rs b/src/presentation/builder/comment.rs index 2b5d1e43..a5edf966 100644 --- a/src/presentation/builder/comment.rs +++ b/src/presentation/builder/comment.rs @@ -117,9 +117,9 @@ impl PresentationBuilder<'_, '_> { return Ok(()); } CommentCommand::BackgroundColor(color) => { - let color = color.resolve(&self.theme.palette).map_err(|e| { - self.invalid_presentation(source_position, InvalidPresentation::InvalidColor(e)) - })?; + let color = color + .resolve(&self.theme.palette) + .map_err(|e| self.invalid_presentation(source_position, InvalidPresentation::InvalidColor(e)))?; self.slide_state.background_color = color; self.apply_slide_background_color(); } From b7c2806460822a7bbbb97254be4fdb8ef639632d Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 23 Jan 2026 19:50:53 -0800 Subject: [PATCH 4/5] remove dead code --- src/presentation/builder/comment.rs | 1 - src/presentation/builder/mod.rs | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/presentation/builder/comment.rs b/src/presentation/builder/comment.rs index a5edf966..b0ed7917 100644 --- a/src/presentation/builder/comment.rs +++ b/src/presentation/builder/comment.rs @@ -121,7 +121,6 @@ impl PresentationBuilder<'_, '_> { .resolve(&self.theme.palette) .map_err(|e| self.invalid_presentation(source_position, InvalidPresentation::InvalidColor(e)))?; self.slide_state.background_color = color; - self.apply_slide_background_color(); } }; // Don't push line breaks for any comments. diff --git a/src/presentation/builder/mod.rs b/src/presentation/builder/mod.rs index 3217e558..6d31dbbd 100644 --- a/src/presentation/builder/mod.rs +++ b/src/presentation/builder/mod.rs @@ -339,13 +339,6 @@ impl<'a, 'b> PresentationBuilder<'a, 'b> { self.chunk_operations.push(RenderOperation::SetColors(colors)); } - fn apply_slide_background_color(&mut self) { - // We only store the background color here; it will be applied retroactively - // to the slide's initial SetColors operation when the slide is terminated. - // This ensures the entire slide has the background color, regardless of - // where the command appears in the slide content. - } - fn push_slide_prelude(&mut self) { let style = self.theme.default_style.style; self.set_colors(style.colors); From df20dba6f576edf5d9de240112bd585a045a4070 Mon Sep 17 00:00:00 2001 From: Alyssa Evans Date: Fri, 23 Jan 2026 20:01:09 -0800 Subject: [PATCH 5/5] rename BackgroundColor to SlideBackgroundColor for clarity --- src/presentation/builder/comment.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/presentation/builder/comment.rs b/src/presentation/builder/comment.rs index b0ed7917..8e7b735f 100644 --- a/src/presentation/builder/comment.rs +++ b/src/presentation/builder/comment.rs @@ -116,7 +116,7 @@ impl PresentationBuilder<'_, '_> { self.push_detached_code_execution(handle)?; return Ok(()); } - CommentCommand::BackgroundColor(color) => { + CommentCommand::SlideBackgroundColor(color) => { let color = color .resolve(&self.theme.palette) .map_err(|e| self.invalid_presentation(source_position, InvalidPresentation::InvalidColor(e)))?; @@ -192,7 +192,7 @@ impl PresentationBuilder<'_, '_> { #[serde(rename_all = "snake_case")] pub(crate) enum CommentCommand { Alignment(CommentCommandAlignment), - BackgroundColor(RawColor), + SlideBackgroundColor(RawColor), Column(usize), EndSlide, FontSize(u8), @@ -238,7 +238,7 @@ impl CommentCommand { format!(""), format!(""), format!(""), - format!(""), + format!(""), ] } } @@ -304,11 +304,11 @@ mod tests { } #[rstest] - #[case::hex("background_color: ff0000")] - #[case::named("background_color: red")] + #[case::hex("slide_background_color: ff0000")] + #[case::named("slide_background_color: red")] fn background_color_parsing(#[case] input: &str) { let parsed: CommentCommand = input.parse().expect("deserialization failed"); - assert!(matches!(parsed, CommentCommand::BackgroundColor(_))); + assert!(matches!(parsed, CommentCommand::SlideBackgroundColor(_))); } #[rstest] @@ -784,7 +784,7 @@ hi use crate::markdown::text_style::Color; let input = " - + hello "; @@ -808,7 +808,7 @@ hello #[test] fn background_color_invalid() { let input = " - + hello ";