markdown_preview: Independent font family and theme (#54003)

## Summary

Adds two user-facing settings that let the markdown preview render with
a font family and theme independent from the editor. Both are optional
and fall back to the editor defaults when unset.

```json
{
  "markdown_preview_font_family": "Georgia",
  "markdown_preview_theme": "Solarized Light"
}
```

## Details

- New `MarkdownFont::Preview` variant, used only by the preview surface.
- New `MarkdownStyle::themed_with_overrides()` that accepts explicit
`ThemeColors` and `SyntaxTheme` so the preview can render with a theme
other than the active editor theme. Existing `themed()` callers are
unchanged.
- The preview pane background adopts the chosen theme's
`editor_background`.
- Follows the pattern already used by `agent_ui_font_size` /
`agent_buffer_font_size`.

## Scope

Sizing-related changes (`markdown_preview_font_size`,
`markdown_preview_line_height`, typography tweaks, responsive max-width
container) were in an earlier revision of this PR and have been removed
per review feedback — they interact with the Cmd+/- zoom behavior in
ways that still need design work, and will land in a follow-up.

## Files Changed

- `crates/settings_content/src/theme.rs` — schema
- `crates/theme_settings/src/settings.rs` — runtime + accessor
- `crates/markdown/src/markdown.rs` — `MarkdownFont::Preview` +
`themed_with_overrides()`
- `crates/markdown_preview/src/markdown_preview_view.rs` — theme
resolution, style selection, background
- `crates/markdown_preview/Cargo.toml` — `theme` dependency
- `crates/settings/src/vscode_import.rs` — new fields in struct literal

Release Notes:

- Added `markdown_preview_font_family` and `markdown_preview_theme`
settings to customize the markdown preview independently from the
editor.

Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
robinojwandChris Biscardi authored and GitHub committed 2026-04-23 23:41:07 +00:00
1 parent 856ae823fc
commit 94ce64470a
7 files changed
+124 -42

No files matched your search

Generated
+1
View File
@@ -10434,6 +10434,7 @@ dependencies = [
"project",
"settings",
"tempfile",
"theme",
"theme_settings",
"ui",
"urlencoding",
+27 -4
View File
@@ -113,15 +113,32 @@ impl Default for MarkdownStyle {
}
}
#[derive(Clone, Copy)]
pub enum MarkdownFont {
Agent,
Editor,
Preview,
}
impl MarkdownStyle {
pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let syntax = cx.theme().syntax().clone();
Self::themed_with_overrides(font, colors, &syntax, window, cx)
}
/// Like [`Self::themed`], but takes explicit [`ThemeColors`] and
/// [`SyntaxTheme`] so callers (e.g. the markdown preview) can render the
/// markdown using a theme other than the active editor theme.
pub fn themed_with_overrides(
font: MarkdownFont,
colors: &theme::ThemeColors,
syntax: &Arc<SyntaxTheme>,
window: &Window,
cx: &App,
) -> Self {
let theme_settings = ThemeSettings::get_global(cx);
let is_preview = matches!(font, MarkdownFont::Preview);
let buffer_font_weight = theme_settings.buffer_font.weight;
let (buffer_font_size, ui_font_size) = match font {
@@ -129,19 +146,25 @@ impl MarkdownStyle {
theme_settings.agent_buffer_font_size(cx),
theme_settings.agent_ui_font_size(cx),
),
MarkdownFont::Editor => (
MarkdownFont::Editor | MarkdownFont::Preview => (
theme_settings.buffer_font_size(cx),
theme_settings.ui_font_size(cx),
),
};
let body_font_family = if is_preview {
theme_settings.markdown_preview_font_family().clone()
} else {
theme_settings.ui_font.family.clone()
};
let text_color = colors.text;
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_family: Some(body_font_family),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
@@ -152,7 +175,7 @@ impl MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
syntax: syntax.clone(),
selection_background_color: colors.element_selection_background,
rule_color: colors.border,
block_quote_border_color: colors.border,
+1
View File
@@ -23,6 +23,7 @@ log.workspace = true
markdown.workspace = true
project.workspace = true
settings.workspace = true
theme.workspace = true
theme_settings.workspace = true
ui.workspace = true
urlencoding.workspace = true
@@ -20,6 +20,7 @@ use markdown::{
};
use project::search::SearchQuery;
use settings::Settings;
use theme::{SystemAppearance, Theme, ThemeRegistry};
use theme_settings::ThemeSettings;
use ui::{ContextMenu, WithScrollbar, prelude::*, right_click_menu};
use util::markdown::split_local_url_fragment;
@@ -578,8 +579,18 @@ impl MarkdownPreviewView {
cx.notify();
}
/// Returns the theme chosen in `markdown_preview_theme`, or `None` if the
/// user hasn't set one or it can't be resolved.
fn resolve_preview_theme(&self, cx: &App) -> Option<Arc<Theme>> {
let theme_settings = ThemeSettings::get_global(cx);
let theme_selection = theme_settings.markdown_preview_theme.as_ref()?;
let theme_name = theme_selection.name(SystemAppearance::global(cx).0);
ThemeRegistry::global(cx).get(&theme_name.0).ok()
}
fn render_markdown_element(
&self,
preview_theme: &Option<Arc<Theme>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> MarkdownElement {
@@ -596,41 +607,50 @@ impl MarkdownPreviewView {
}
}
let mut markdown_element = MarkdownElement::new(
self.markdown.clone(),
MarkdownStyle::themed(MarkdownFont::Editor, window, cx),
)
.code_block_renderer(CodeBlockRenderer::Default {
copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
border: false,
})
.scroll_handle(self.scroll_handle.clone())
.show_root_block_markers()
.image_resolver({
let base_directory = self.base_directory.clone();
move |dest_url| {
resolve_preview_image(
dest_url,
base_directory.as_deref(),
workspace_directory.as_deref(),
)
}
})
.on_url_click({
let view_handle = cx.entity().downgrade();
let workspace = self.workspace.clone();
let base_directory = self.base_directory.clone();
move |url, window, cx| {
handle_url_click(
url,
&view_handle,
base_directory.clone(),
&workspace,
window,
cx,
);
}
});
let markdown_style = if let Some(theme) = preview_theme {
MarkdownStyle::themed_with_overrides(
MarkdownFont::Preview,
theme.colors(),
theme.syntax(),
window,
cx,
)
} else {
MarkdownStyle::themed(MarkdownFont::Preview, window, cx)
};
let mut markdown_element = MarkdownElement::new(self.markdown.clone(), markdown_style)
.code_block_renderer(CodeBlockRenderer::Default {
copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
border: false,
})
.scroll_handle(self.scroll_handle.clone())
.show_root_block_markers()
.image_resolver({
let base_directory = self.base_directory.clone();
move |dest_url| {
resolve_preview_image(
dest_url,
base_directory.as_deref(),
workspace_directory.as_deref(),
)
}
})
.on_url_click({
let view_handle = cx.entity().downgrade();
let workspace = self.workspace.clone();
let base_directory = self.base_directory.clone();
move |url, window, cx| {
handle_url_click(
url,
&view_handle,
base_directory.clone(),
&workspace,
window,
cx,
);
}
});
if let Some(active_editor) = active_editor {
let editor_for_checkbox = active_editor.clone();
@@ -878,6 +898,11 @@ impl Item for MarkdownPreviewView {
impl Render for MarkdownPreviewView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let preview_theme = self.resolve_preview_theme(cx);
let bg_color = preview_theme
.as_ref()
.map(|theme| theme.colors().editor_background)
.unwrap_or_else(|| cx.theme().colors().editor_background);
div()
.image_cache(self.image_cache.clone())
.id("MarkdownPreview")
@@ -892,7 +917,7 @@ impl Render for MarkdownPreviewView {
.on_action(cx.listener(MarkdownPreviewView::scroll_to_top))
.on_action(cx.listener(MarkdownPreviewView::scroll_to_bottom))
.size_full()
.bg(cx.theme().colors().editor_background)
.bg(bg_color)
.child(
div()
.id("markdown-preview-scroll-container")
@@ -901,7 +926,8 @@ impl Render for MarkdownPreviewView {
.track_scroll(&self.scroll_handle)
.p_4()
.child({
let markdown_element = self.render_markdown_element(window, cx);
let markdown_element =
self.render_markdown_element(&preview_theme, window, cx);
let markdown = self.markdown.clone();
right_click_menu("markdown-preview-context-menu")
.trigger(move |_, _, _| markdown_element)
+2
View File
@@ -975,6 +975,8 @@ impl VsCodeSettings {
buffer_font_features: None,
agent_ui_font_size: None,
agent_buffer_font_size: None,
markdown_preview_font_family: None,
markdown_preview_theme: None,
theme: None,
icon_theme: None,
ui_density: None,
+6
View File
@@ -149,6 +149,12 @@ pub struct ThemeSettingsContent {
pub agent_ui_font_size: Option<FontSize>,
/// The font size for user messages in the agent panel.
pub agent_buffer_font_size: Option<FontSize>,
/// The name of a font to use for rendering in the markdown preview.
/// Falls back to the UI font if unset.
pub markdown_preview_font_family: Option<FontFamilyName>,
/// The theme to use for the markdown preview.
/// Falls back to the main editor theme if unset.
pub markdown_preview_theme: Option<ThemeSelection>,
/// The name of the Zed theme to use.
pub theme: Option<ThemeSelection>,
/// The name of the icon theme to use.
+24 -1
View File
@@ -4,7 +4,8 @@ use crate::schema::{status_colors_refinement, syntax_overrides, theme_colors_ref
use crate::{merge_accent_colors, merge_player_colors};
use collections::HashMap;
use gpui::{
App, Context, Font, FontFallbacks, FontStyle, Global, Pixels, Subscription, Window, px,
App, Context, Font, FontFallbacks, FontStyle, Global, Pixels, SharedString, Subscription,
Window, px,
};
use refineable::Refineable;
use schemars::JsonSchema;
@@ -55,6 +56,12 @@ pub struct ThemeSettings {
agent_ui_font_size: Option<Pixels>,
/// The agent buffer font size. Determines the size of user messages in the agent panel.
agent_buffer_font_size: Option<Pixels>,
/// The font family to use for rendering in the markdown preview.
/// Falls back to the UI font family if unset.
markdown_preview_font_family: Option<SharedString>,
/// The theme to use for the markdown preview.
/// Falls back to the main editor theme if unset.
pub markdown_preview_theme: Option<ThemeSelection>,
/// The line height for buffers, and the terminal.
///
/// Changing this may affect the spacing of some UI elements.
@@ -394,6 +401,14 @@ impl ThemeSettings {
.unwrap_or_else(|| self.buffer_font_size(cx))
}
/// Returns the font family to use in the markdown preview,
/// falling back to the UI font family when unset.
pub fn markdown_preview_font_family(&self) -> &SharedString {
self.markdown_preview_font_family
.as_ref()
.unwrap_or(&self.ui_font.family)
}
/// Returns the buffer font size, read from the settings.
///
/// The real buffer font size is stored in-memory, to support temporary font size changes.
@@ -624,6 +639,14 @@ impl settings::Settings for ThemeSettings {
buffer_line_height: content.buffer_line_height.unwrap().into(),
agent_ui_font_size: content.agent_ui_font_size.map(|s| s.into_gpui()),
agent_buffer_font_size: content.agent_buffer_font_size.map(|s| s.into_gpui()),
markdown_preview_font_family: content
.markdown_preview_font_family
.as_ref()
.map(|f| f.0.clone().into()),
markdown_preview_theme: content
.markdown_preview_theme
.clone()
.map(ThemeSelection::from),
theme: theme_selection,
experimental_theme_overrides: content.experimental_theme_overrides.clone(),
theme_overrides: content.theme_overrides.clone(),