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:
1 parent
856ae823fc
commit
94ce64470a
7 files changed
+124
-42
No files matched your search
Generated
+1
@@ -10434,6 +10434,7 @@ dependencies = [
|
||||
"project",
|
||||
"settings",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"theme_settings",
|
||||
"ui",
|
||||
"urlencoding",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user