From 235157becaeb821af857460f2a2668fce3bfb22f Mon Sep 17 00:00:00 2001 From: SR0725 Date: Fri, 31 Jul 2026 22:21:48 -0700 Subject: [PATCH 1/2] Make Mermaid previews theme-aware --- .../ai/blocklist/block/view_impl/common.rs | 10 +++- app/src/notebooks/editor/notebook_command.rs | 7 ++- app/src/notebooks/editor/view_tests.rs | 3 +- crates/editor/src/content/edit.rs | 24 ++++++--- crates/editor/src/content/edit_tests.rs | 28 +++++++--- crates/editor/src/content/mermaid_diagram.rs | 23 ++++++-- .../src/content/mermaid_diagram_tests.rs | 52 +++++++++++++++++-- 7 files changed, 125 insertions(+), 22 deletions(-) diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index d5390371c90..b784ba13d7b 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -1514,7 +1514,10 @@ fn lightbox_image_for_mermaid_diagram( return None; } - let asset_source = mermaid_asset_source(&diagram.source); + let asset_source = mermaid_asset_source( + &diagram.source, + Appearance::as_ref(app).theme().inferred_color_scheme(), + ); let asset_state = AssetCache::as_ref(app).load_asset::(asset_source.clone()); if matches!(asset_state, AssetState::FailedToLoad(_)) { return None; @@ -2154,7 +2157,10 @@ fn render_mermaid_diagram_section( return render_visual_markdown_fallback(&diagram.markdown_source, text_color, app); } - let asset_source = mermaid_asset_source(&diagram.source); + let asset_source = mermaid_asset_source( + &diagram.source, + Appearance::as_ref(app).theme().inferred_color_scheme(), + ); let asset_state = AssetCache::as_ref(app).load_asset::(asset_source.clone()); if matches!(asset_state, AssetState::FailedToLoad(_)) { return render_visual_markdown_fallback(&diagram.markdown_source, text_color, app); diff --git a/app/src/notebooks/editor/notebook_command.rs b/app/src/notebooks/editor/notebook_command.rs index 926c8fc8e8e..ad5903d4191 100644 --- a/app/src/notebooks/editor/notebook_command.rs +++ b/app/src/notebooks/editor/notebook_command.rs @@ -754,7 +754,12 @@ impl RunnableCommandModel for NotebookCommand { ctx.dispatch_typed_action(WorkspaceAction::OpenLightbox { images: vec![LightboxImage { source: LightboxImageSource::Resolved { - asset_source: mermaid_asset_source(&source), + asset_source: mermaid_asset_source( + &source, + Appearance::as_ref(app) + .theme() + .inferred_color_scheme(), + ), }, description: None, }], diff --git a/app/src/notebooks/editor/view_tests.rs b/app/src/notebooks/editor/view_tests.rs index 1c42289b1dc..68d1911970c 100644 --- a/app/src/notebooks/editor/view_tests.rs +++ b/app/src/notebooks/editor/view_tests.rs @@ -5,6 +5,7 @@ use async_channel::TryRecvError; use parking_lot::Mutex; use string_offset::CharOffset; use tempfile::tempdir; +use warp_core::ui::theme::ColorScheme; use warp_editor::content::mermaid_diagram::mermaid_asset_source; use warp_editor::render::element::RichTextAction; use warp_editor::render::model::{ @@ -190,7 +191,7 @@ fn test_loaded_mermaid_diagram_with_placeholder_height_needs_relayout() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); let contents = "graph TD\nA[Start] --> B[Finish]\n"; - let asset_source = mermaid_asset_source(contents); + let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark); let pending = app.read(|ctx| { let asset_cache = AssetCache::as_ref(ctx); diff --git a/crates/editor/src/content/edit.rs b/crates/editor/src/content/edit.rs index 93130d07fed..f0fdbf15405 100644 --- a/crates/editor/src/content/edit.rs +++ b/crates/editor/src/content/edit.rs @@ -14,6 +14,7 @@ use string_offset::{ByteOffset, CharOffset}; use urlocator::{UrlLocation, UrlLocator}; use vec1::Vec1; use warp_core::features::FeatureFlag; +use warp_core::ui::appearance::Appearance; use warp_core::ui::theme::Fill as ThemeFill; use warp_errors::report_error; use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState}; @@ -782,7 +783,8 @@ impl LayoutTask { }; } - let asset_source = mermaid_asset_source(&source); + let color_scheme = Appearance::as_ref(app).theme().inferred_color_scheme(); + let asset_source = mermaid_asset_source(&source, color_scheme); let asset_cache = AssetCache::as_ref(app); match asset_cache.load_asset::(asset_source.clone()) { AssetState::Loaded { .. } => { @@ -791,7 +793,7 @@ impl LayoutTask { .block_spacings .from_block_style(&text_block.style); let (asset_source, config) = - mermaid_diagram_layout(&source, layout, spacing, app); + mermaid_diagram_layout(&source, layout, spacing, color_scheme, app); Self::MermaidDiagram { text_block, asset_source, @@ -806,8 +808,13 @@ impl LayoutTask { .rich_text_styles() .block_spacings .from_block_style(&text_block.style); - let (asset_source, config) = - mermaid_diagram_layout(&source, layout, spacing, app); + let (asset_source, config) = mermaid_diagram_layout( + &source, + layout, + spacing, + color_scheme, + app, + ); Self::MermaidDiagram { text_block, asset_source, @@ -828,8 +835,13 @@ impl LayoutTask { .rich_text_styles() .block_spacings .from_block_style(&text_block.style); - let (asset_source, config) = - mermaid_diagram_layout(&source, layout, spacing, app); + let (asset_source, config) = mermaid_diagram_layout( + &source, + layout, + spacing, + color_scheme, + app, + ); Self::MermaidDiagram { text_block, asset_source, diff --git a/crates/editor/src/content/edit_tests.rs b/crates/editor/src/content/edit_tests.rs index a94a3b2dd21..3a8243f4ad4 100644 --- a/crates/editor/src/content/edit_tests.rs +++ b/crates/editor/src/content/edit_tests.rs @@ -6,6 +6,8 @@ use std::time::{SystemTime, UNIX_EPOCH}; use string_offset::CharOffset; use warp_core::features::FeatureFlag; +use warp_core::ui::appearance::Appearance; +use warp_core::ui::theme::ColorScheme; use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState}; use warpui_core::fonts::{Properties, Style, Weight}; use warpui_core::image_cache::ImageType; @@ -297,7 +299,7 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); let content = "graph TD\nA[Start] --> B[Finish]\n"; - let asset_source = mermaid_asset_source(content); + let asset_source = mermaid_asset_source(content, ColorScheme::LightOnDark); let mermaid_load = app.read(|ctx| { let asset_cache = AssetCache::as_ref(ctx); @@ -335,7 +337,13 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() { content_length: CharOffset::from(content.chars().count()), }; let spacing = TEST_STYLES.block_spacings.from_block_style(&block_style); - let mermaid_diagram = mermaid_diagram_layout(content, &text_layout, spacing, ctx); + let mermaid_diagram = mermaid_diagram_layout( + content, + &text_layout, + spacing, + ColorScheme::LightOnDark, + ctx, + ); let (item, _has_trailing_newline) = layout_mermaid_diagram_block( block, @@ -404,8 +412,13 @@ fn test_unloaded_mermaid_diagram_uses_stable_full_width_placeholder_height() { code_block_type: CodeBlockType::Mermaid, }; let spacing = TEST_STYLES.block_spacings.from_block_style(&block_style); - let (_asset_source, config) = - mermaid_diagram_layout(contents, &text_layout, spacing, ctx); + let (_asset_source, config) = mermaid_diagram_layout( + contents, + &text_layout, + spacing, + ColorScheme::LightOnDark, + ctx, + ); let expected_width = 800. - spacing.x_axis_offset().as_f32(); let expected_height = TEST_STYLES.base_line_height().as_f32() * 10.; @@ -486,6 +499,7 @@ fn test_empty_mermaid_block_lays_out_as_code_block() { fn test_non_parseable_mermaid_block_lays_out_as_code_block() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); + app.add_singleton_model(|_| Appearance::mock()); app.read(|ctx| { let layout_cache = LayoutCache::new(); let text_layout = TextLayout::new( @@ -524,8 +538,9 @@ fn test_non_parseable_mermaid_block_lays_out_as_code_block() { fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); + app.add_singleton_model(|_| Appearance::mock()); let contents = "echo hi\n"; - let asset_source = mermaid_asset_source(contents); + let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark); // Drive the asset load to completion (it should fail, since `echo hi` isn't // valid Mermaid). @@ -586,8 +601,9 @@ fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() { fn test_valid_mermaid_block_lays_out_as_diagram_after_load() { App::test((), |app| async move { let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true); + app.add_singleton_model(|_| Appearance::mock()); let contents = "graph TD\nA[Start] --> B[Finish]\n"; - let asset_source = mermaid_asset_source(contents); + let asset_source = mermaid_asset_source(contents, ColorScheme::LightOnDark); // Drive the async Mermaid render to completion. let pending = app.read(|ctx| { diff --git a/crates/editor/src/content/mermaid_diagram.rs b/crates/editor/src/content/mermaid_diagram.rs index d6221d8e1f8..9b00bac9dd8 100644 --- a/crates/editor/src/content/mermaid_diagram.rs +++ b/crates/editor/src/content/mermaid_diagram.rs @@ -2,6 +2,8 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::Arc; use bytes::Bytes; +use mermaid_to_svg::{MermaidTheme, parse_mermaid_frontmatter}; +use warp_core::ui::theme::ColorScheme; use warpui_core::assets::asset_cache::{ AssetCache, AssetSource, AssetState, AsyncAssetId, AsyncAssetType, }; @@ -19,10 +21,12 @@ struct MermaidDiagramAsset; impl AsyncAssetType for MermaidDiagramAsset {} -pub fn mermaid_asset_source(source: &str) -> AssetSource { +pub fn mermaid_asset_source(source: &str, color_scheme: ColorScheme) -> AssetSource { let source = source.to_string(); + let theme = mermaid_theme(&source, color_scheme); let mut hasher = DefaultHasher::new(); source.hash(&mut hasher); + theme.hash(&mut hasher); let id = format!("configured:{:x}", hasher.finish()); let fetch_source = source.clone(); @@ -30,8 +34,9 @@ pub fn mermaid_asset_source(source: &str) -> AssetSource { id: AsyncAssetId::new::(id), fetch: Arc::new(move || { let source = fetch_source.clone(); + let theme = theme.clone(); Box::pin(async move { - mermaid_to_svg::render_mermaid_to_svg(&source, None) + mermaid_to_svg::render_mermaid_to_svg(&source, Some(&theme)) .map(|svg| Bytes::from(svg.into_bytes())) .map_err(Into::into) }) @@ -43,14 +48,26 @@ pub fn mermaid_diagram_layout( source: &str, layout: &TextLayout, spacing: BlockSpacing, + color_scheme: ColorScheme, app: &AppContext, ) -> (AssetSource, ImageBlockConfig) { - let asset_source = mermaid_asset_source(source); + let asset_source = mermaid_asset_source(source, color_scheme); let config = mermaid_diagram_config(&asset_source, layout, spacing, app); (asset_source, config) } +fn mermaid_theme(source: &str, color_scheme: ColorScheme) -> MermaidTheme { + if let Some(configured_theme) = parse_mermaid_frontmatter(source).config.to_mermaid_theme() { + return configured_theme; + } + + match color_scheme { + ColorScheme::LightOnDark => MermaidTheme::dark(), + ColorScheme::DarkOnLight => MermaidTheme::light(), + } +} + fn mermaid_diagram_config( asset_source: &AssetSource, layout: &TextLayout, diff --git a/crates/editor/src/content/mermaid_diagram_tests.rs b/crates/editor/src/content/mermaid_diagram_tests.rs index 95338131caa..43fafcea289 100644 --- a/crates/editor/src/content/mermaid_diagram_tests.rs +++ b/crates/editor/src/content/mermaid_diagram_tests.rs @@ -1,3 +1,4 @@ +use warp_core::ui::theme::ColorScheme; use warpui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState}; use warpui_core::image_cache::ImageType; use warpui_core::text_layout::LayoutCache; @@ -27,8 +28,13 @@ fn loading_mermaid_layout_uses_default_height() { &TEST_STYLES, 800., ); - let (_asset_source, config) = - mermaid_diagram_layout(source, &text_layout, mermaid_block_spacing(), ctx); + let (_asset_source, config) = mermaid_diagram_layout( + source, + &text_layout, + mermaid_block_spacing(), + ColorScheme::LightOnDark, + ctx, + ); let expected_height = TEST_STYLES.base_line_height() * DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER.into_pixels(); @@ -54,7 +60,8 @@ flowchart TD A[Start] --> B[Done] "##; - let AssetSource::Async { fetch, .. } = mermaid_asset_source(source) else { + let AssetSource::Async { fetch, .. } = mermaid_asset_source(source, ColorScheme::LightOnDark) + else { panic!("expected Mermaid diagrams to be async assets"); }; let bytes = match futures_lite::future::block_on(fetch()) { @@ -67,10 +74,49 @@ flowchart TD }; assert!(svg.contains(" String::from_utf8(bytes.to_vec()).expect("expected valid UTF-8"), + Err(error) => panic!("expected dark Mermaid SVG to render: {error:#}"), + }; + let light_svg = match futures_lite::future::block_on(fetch_light()) { + Ok(bytes) => String::from_utf8(bytes.to_vec()).expect("expected valid UTF-8"), + Err(error) => panic!("expected light Mermaid SVG to render: {error:#}"), + }; + + assert!(dark_svg.contains("background-color: #1e1e1e")); + assert!(light_svg.contains("background-color: #ffffff")); +} + +#[test] +fn mermaid_asset_source_cache_key_includes_color_scheme() { + let source = "graph TD\nA[Start] --> B[Finish]\n"; + let dark_asset = mermaid_asset_source(source, ColorScheme::LightOnDark); + let light_asset = mermaid_asset_source(source, ColorScheme::DarkOnLight); + + assert_ne!(dark_asset, light_asset); +} + #[test] fn failed_mermaid_layout_uses_compact_height() { App::test((), |app| async move { From 6b5064860e40f354f723a035995833a823c6778f Mon Sep 17 00:00:00 2001 From: SR0725 Date: Fri, 31 Jul 2026 23:04:20 -0700 Subject: [PATCH 2/2] Match Mermaid canvas to the active theme --- app/src/ai/blocklist/block/view_impl/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/ai/blocklist/block/view_impl/common.rs b/app/src/ai/blocklist/block/view_impl/common.rs index b784ba13d7b..bdd3b06f716 100644 --- a/app/src/ai/blocklist/block/view_impl/common.rs +++ b/app/src/ai/blocklist/block/view_impl/common.rs @@ -2193,7 +2193,7 @@ fn render_mermaid_diagram_section( app, ); let mermaid_canvas = Container::new(mermaid_block) - .with_background(theme.foreground()) + .with_background(theme.background()) .with_uniform_padding(MERMAID_CANVAS_PADDING) .finish();