use crate::{ markdown_elements::{ HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownMermaidDiagram, ParsedMarkdownMermaidDiagramContents, ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, }, markdown_preview_view::MarkdownPreviewView,};use collections::HashMap;use fs::normalize_path;use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, AppContext as _, Context, Div, Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Modifiers, ParentElement, Render, RenderImage, Resource, SharedString, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div, img, pulsating_between, rems,};use settings::Settings;use std::{ ops::{Mul, Range}, sync::{Arc, OnceLock}, time::Duration, vec,};use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};use ui::{CopyButton, LinkPreview, ToggleState, prelude::*, tooltip_container};use workspace::{OpenOptions, OpenVisible, Workspace}; pub struct CheckboxClickedEvent { pub checked: bool, pub source_range: Range<usize>,} impl CheckboxClickedEvent { pub fn source_range(&self) -> Range<usize> { self.source_range.clone() } pub fn checked(&self) -> bool { self.checked }} type CheckboxClickedCallback = Arc<Box<dyn Fn(&CheckboxClickedEvent, &mut Window, &mut App)>>; type MermaidDiagramCache = HashMap<ParsedMarkdownMermaidDiagramContents, CachedMermaidDiagram>; #[derive(Default)]pub(crate) struct MermaidState { cache: MermaidDiagramCache, order: Vec<ParsedMarkdownMermaidDiagramContents>,} impl MermaidState { fn get_fallback_image( idx: usize, old_order: &[ParsedMarkdownMermaidDiagramContents], new_order_len: usize, cache: &MermaidDiagramCache, ) -> Option<Arc<RenderImage>> { // When the diagram count changes e.g. addition or removal, positional matching // is unreliable since a new diagram at index i likely doesn't correspond to the // old diagram at index i. We only allow fallbacks when counts match, which covers // the common case of editing a diagram in-place. // // Swapping two diagrams would briefly show the stale fallback, but that's an edge // case we don't handle. if old_order.len() != new_order_len { return None; } old_order.get(idx).and_then(|old_content| { cache.get(old_content).and_then(|old_cached| { old_cached .render_image .get() .and_then(|result| result.as_ref().ok().cloned()) // Chain fallbacks for rapid edits. .or_else(|| old_cached.fallback_image.clone()) }) }) } pub(crate) fn update( &mut self, parsed: &ParsedMarkdown, cx: &mut Context<MarkdownPreviewView>, ) { use crate::markdown_elements::ParsedMarkdownElement; use std::collections::HashSet; let mut new_order = Vec::new(); for element in parsed.children.iter() { if let ParsedMarkdownElement::MermaidDiagram(mermaid_diagram) = element { new_order.push(mermaid_diagram.contents.clone()); } } for (idx, new_content) in new_order.iter().enumerate() { if !self.cache.contains_key(new_content) { let fallback = Self::get_fallback_image(idx, &self.order, new_order.len(), &self.cache); self.cache.insert( new_content.clone(), CachedMermaidDiagram::new(new_content.clone(), fallback, cx), ); } } let new_order_set: HashSet<_> = new_order.iter().cloned().collect(); self.cache .retain(|content, _| new_order_set.contains(content)); self.order = new_order; }} pub(crate) struct CachedMermaidDiagram { pub(crate) render_image: Arc<OnceLock<anyhow::Result<Arc<RenderImage>>>>, pub(crate) fallback_image: Option<Arc<RenderImage>>, _task: Task<()>,} impl CachedMermaidDiagram { pub(crate) fn new( contents: ParsedMarkdownMermaidDiagramContents, fallback_image: Option<Arc<RenderImage>>, cx: &mut Context<MarkdownPreviewView>, ) -> Self { let result = Arc::new(OnceLock::<anyhow::Result<Arc<RenderImage>>>::new()); let result_clone = result.clone(); let svg_renderer = cx.svg_renderer(); let _task = cx.spawn(async move |this, cx| { let value = cx .background_spawn(async move { let svg_string = mermaid_rs_renderer::render(&contents.contents)?; let scale = contents.scale as f32 / 100.0; svg_renderer .render_single_frame(svg_string.as_bytes(), scale, true) .map_err(|e| anyhow::anyhow!("{}", e)) }) .await; let _ = result_clone.set(value); this.update(cx, |_, cx| { cx.notify(); }) .ok(); }); Self { render_image: result, fallback_image, _task, } } #[cfg(test)] fn new_for_test( render_image: Option<Arc<RenderImage>>, fallback_image: Option<Arc<RenderImage>>, ) -> Self { let result = Arc::new(OnceLock::new()); if let Some(img) = render_image { let _ = result.set(Ok(img)); } Self { render_image: result, fallback_image, _task: Task::ready(()), } }}#[derive(Clone)]pub struct RenderContext<'a> { workspace: Option<WeakEntity<Workspace>>, next_id: usize, buffer_font_family: SharedString, buffer_text_style: TextStyle, text_style: TextStyle, border_color: Hsla, title_bar_background_color: Hsla, panel_background_color: Hsla, text_color: Hsla, link_color: Hsla, window_rem_size: Pixels, text_muted_color: Hsla, code_block_background_color: Hsla, code_span_background_color: Hsla, syntax_theme: Arc<SyntaxTheme>, indent: usize, checkbox_clicked_callback: Option<CheckboxClickedCallback>, is_last_child: bool, mermaid_state: &'a MermaidState,} impl<'a> RenderContext<'a> { pub(crate) fn new( workspace: Option<WeakEntity<Workspace>>, mermaid_state: &'a MermaidState, window: &mut Window, cx: &mut App, ) -> Self { let theme = cx.theme().clone(); let settings = ThemeSettings::get_global(cx); let buffer_font_family = settings.buffer_font.family.clone(); let buffer_font_features = settings.buffer_font.features.clone(); let mut buffer_text_style = window.text_style(); buffer_text_style.font_family = buffer_font_family.clone(); buffer_text_style.font_features = buffer_font_features; buffer_text_style.font_size = AbsoluteLength::from(settings.buffer_font_size(cx)); RenderContext { workspace, next_id: 0, indent: 0, buffer_font_family, buffer_text_style, text_style: window.text_style(), syntax_theme: theme.syntax().clone(), border_color: theme.colors().border, title_bar_background_color: theme.colors().title_bar_background, panel_background_color: theme.colors().panel_background, text_color: theme.colors().text, link_color: theme.colors().text_accent, window_rem_size: window.rem_size(), text_muted_color: theme.colors().text_muted, code_block_background_color: theme.colors().surface_background, code_span_background_color: theme.colors().editor_document_highlight_read_background, checkbox_clicked_callback: None, is_last_child: false, mermaid_state, } } pub fn with_checkbox_clicked_callback( mut self, callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, ) -> Self { self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); self } fn next_id(&mut self, span: &Range<usize>) -> ElementId { let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end); self.next_id += 1; ElementId::from(SharedString::from(id)) } /// HACK: used to have rems relative to buffer font size, so that things scale appropriately as /// buffer font size changes. The callees of this function should be reimplemented to use real /// relative sizing once that is implemented in GPUI pub fn scaled_rems(&self, rems: f32) -> Rems { self.buffer_text_style .font_size .to_rems(self.window_rem_size) .mul(rems) } /// This ensures that children inside of block quotes /// have padding between them. /// /// For example, for this markdown: /// /// ```markdown /// > This is a block quote. /// > /// > And this is the next paragraph. /// ``` /// /// We give padding between "This is a block quote." /// and "And this is the next paragraph." fn with_common_p(&self, element: Div) -> Div { if self.indent > 0 && !self.is_last_child { element.pb(self.scaled_rems(0.75)) } else { element } } /// The is used to indicate that the current element is the last child or not of its parent. /// /// Then we can avoid adding padding to the bottom of the last child. fn with_last_child<R>(&mut self, is_last: bool, render: R) -> AnyElement where R: FnOnce(&mut Self) -> AnyElement, { self.is_last_child = is_last; let element = render(self); self.is_last_child = false; element }} pub fn render_parsed_markdown( parsed: &ParsedMarkdown, workspace: Option<WeakEntity<Workspace>>, window: &mut Window, cx: &mut App,) -> Div { let cache = Default::default(); let mut cx = RenderContext::new(workspace, &cache, window, cx); v_flex().gap_3().children( parsed .children .iter() .map(|block| render_markdown_block(block, &mut cx)), )}pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement { use ParsedMarkdownElement::*; match block { Paragraph(text) => render_markdown_paragraph(text, cx), Heading(heading) => render_markdown_heading(heading, cx), ListItem(list_item) => render_markdown_list_item(list_item, cx), Table(table) => render_markdown_table(table, cx), BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), CodeBlock(code_block) => render_markdown_code_block(code_block, cx), MermaidDiagram(mermaid) => render_mermaid_diagram(mermaid, cx), HorizontalRule(_) => render_markdown_rule(cx), Image(image) => render_markdown_image(image, cx), }} fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement { let size = match parsed.level { HeadingLevel::H1 => 2., HeadingLevel::H2 => 1.5, HeadingLevel::H3 => 1.25, HeadingLevel::H4 => 1., HeadingLevel::H5 => 0.875, HeadingLevel::H6 => 0.85, }; let text_size = cx.scaled_rems(size); // was `DefiniteLength::from(text_size.mul(1.25))` // let line_height = DefiniteLength::from(text_size.mul(1.25)); let line_height = text_size * 1.25; // was `rems(0.15)` // let padding_top = cx.scaled_rems(0.15); let padding_top = rems(0.15); // was `.pb_1()` = `rems(0.25)` // let padding_bottom = cx.scaled_rems(0.25); let padding_bottom = rems(0.25); let color = match parsed.level { HeadingLevel::H6 => cx.text_muted_color, _ => cx.text_color, }; div() .line_height(line_height) .text_size(text_size) .text_color(color) .pt(padding_top) .pb(padding_bottom) .children(render_markdown_text(&parsed.contents, cx)) .whitespace_normal() .into_any()} fn render_markdown_list_item( parsed: &ParsedMarkdownListItem, cx: &mut RenderContext,) -> AnyElement { use ParsedMarkdownListItemType::*; let depth = parsed.depth.saturating_sub(1) as usize; let bullet = match &parsed.item_type { Ordered(order) => list_item_prefix(*order as usize, true, depth).into_any_element(), Unordered => list_item_prefix(1, false, depth).into_any_element(), Task(checked, range) => div() .id(cx.next_id(range)) .mt(cx.scaled_rems(3.0 / 16.0)) .child( MarkdownCheckbox::new( "checkbox", if *checked { ToggleState::Selected } else { ToggleState::Unselected }, cx.clone(), ) .when_some( cx.checkbox_clicked_callback.clone(), |this, callback| { this.on_click({ let range = range.clone(); move |selection, window, cx| { let checked = match selection { ToggleState::Selected => true, ToggleState::Unselected => false, _ => return, }; if window.modifiers().secondary() { callback( &CheckboxClickedEvent { checked, source_range: range.clone(), }, window, cx, ); } } }) }, ), ) .hover(|s| s.cursor_pointer()) .tooltip(|_, cx| { InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into() }) .into_any_element(), }; let bullet = div().mr(cx.scaled_rems(0.5)).child(bullet); let contents: Vec<AnyElement> = parsed .content .iter() .map(|c| render_markdown_block(c, cx)) .collect(); let item = h_flex() .when(!parsed.nested, |this| this.pl(cx.scaled_rems(depth as f32))) .when(parsed.nested && depth > 0, |this| this.ml_neg_1p5()) .items_start() .children(vec![ bullet, v_flex() .children(contents) .when(!parsed.nested, |this| this.gap(cx.scaled_rems(1.0))) .pr(cx.scaled_rems(1.0)) .w_full(), ]); cx.with_common_p(item).into_any()} /// # MarkdownCheckbox ////// HACK: Copied from `ui/src/components/toggle.rs` to deal with scaling issues in markdown preview/// changes should be integrated into `Checkbox` in `toggle.rs` while making sure checkboxes elsewhere in the/// app are not visually affected#[derive(gpui::IntoElement)]struct MarkdownCheckbox { id: ElementId, toggle_state: ToggleState, disabled: bool, placeholder: bool, on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>, filled: bool, style: ui::ToggleStyle, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> gpui::AnyView>>, label: Option<SharedString>, base_rem: Rems,} impl MarkdownCheckbox { /// Creates a new [`Checkbox`]. fn new(id: impl Into<ElementId>, checked: ToggleState, render_cx: RenderContext) -> Self { Self { id: id.into(), toggle_state: checked, disabled: false, on_click: None, filled: false, style: ui::ToggleStyle::default(), tooltip: None, label: None, placeholder: false, base_rem: render_cx.scaled_rems(1.0), } } /// Binds a handler to the [`Checkbox`] that will be called when clicked. fn on_click(mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self } fn bg_color(&self, cx: &App) -> Hsla { let style = self.style.clone(); match (style, self.filled) { (ui::ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, (ui::ToggleStyle::Ghost, true) => cx.theme().colors().element_background, (ui::ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), (ui::ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), (ui::ToggleStyle::Custom(_), false) => gpui::transparent_black(), (ui::ToggleStyle::Custom(color), true) => color.opacity(0.2), } } fn border_color(&self, cx: &App) -> Hsla { if self.disabled { return cx.theme().colors().border_variant;Lines 1–500 of 1498. Load more
use crate::{ markdown_elements::{ HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownMermaidDiagram, ParsedMarkdownMermaidDiagramContents, ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, }, markdown_preview_view::MarkdownPreviewView, }; use collections::HashMap; use fs::normalize_path; use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, AppContext as _, Context, Div, Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Modifiers, ParentElement, Render, RenderImage, Resource, SharedString, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div, img, pulsating_between, rems, }; use settings::Settings; use std::{ ops::{Mul, Range}, sync::{Arc, OnceLock}, time::Duration, vec, }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{CopyButton, LinkPreview, ToggleState, prelude::*, tooltip_container}; use workspace::{OpenOptions, OpenVisible, Workspace}; pub struct CheckboxClickedEvent { pub checked: bool, pub source_range: Range<usize>, } impl CheckboxClickedEvent { pub fn source_range(&self) -> Range<usize> { self.source_range.clone() } pub fn checked(&self) -> bool { self.checked } } type CheckboxClickedCallback = Arc<Box<dyn Fn(&CheckboxClickedEvent, &mut Window, &mut App)>>; type MermaidDiagramCache = HashMap<ParsedMarkdownMermaidDiagramContents, CachedMermaidDiagram>; #[derive(Default)] pub(crate) struct MermaidState { cache: MermaidDiagramCache, order: Vec<ParsedMarkdownMermaidDiagramContents>, } impl MermaidState { fn get_fallback_image( idx: usize, old_order: &[ParsedMarkdownMermaidDiagramContents], new_order_len: usize, cache: &MermaidDiagramCache, ) -> Option<Arc<RenderImage>> { // When the diagram count changes e.g. addition or removal, positional matching // is unreliable since a new diagram at index i likely doesn't correspond to the // old diagram at index i. We only allow fallbacks when counts match, which covers // the common case of editing a diagram in-place. // // Swapping two diagrams would briefly show the stale fallback, but that's an edge // case we don't handle. if old_order.len() != new_order_len { return None; } old_order.get(idx).and_then(|old_content| { cache.get(old_content).and_then(|old_cached| { old_cached .render_image .get() .and_then(|result| result.as_ref().ok().cloned()) // Chain fallbacks for rapid edits. .or_else(|| old_cached.fallback_image.clone()) }) }) } pub(crate) fn update( &mut self, parsed: &ParsedMarkdown, cx: &mut Context<MarkdownPreviewView>, ) { use crate::markdown_elements::ParsedMarkdownElement; use std::collections::HashSet; let mut new_order = Vec::new(); for element in parsed.children.iter() { if let ParsedMarkdownElement::MermaidDiagram(mermaid_diagram) = element { new_order.push(mermaid_diagram.contents.clone()); } } for (idx, new_content) in new_order.iter().enumerate() { if !self.cache.contains_key(new_content) { let fallback = Self::get_fallback_image(idx, &self.order, new_order.len(), &self.cache); self.cache.insert( new_content.clone(), CachedMermaidDiagram::new(new_content.clone(), fallback, cx), ); } } let new_order_set: HashSet<_> = new_order.iter().cloned().collect(); self.cache .retain(|content, _| new_order_set.contains(content)); self.order = new_order; } } pub(crate) struct CachedMermaidDiagram { pub(crate) render_image: Arc<OnceLock<anyhow::Result<Arc<RenderImage>>>>, pub(crate) fallback_image: Option<Arc<RenderImage>>, _task: Task<()>, } impl CachedMermaidDiagram { pub(crate) fn new( contents: ParsedMarkdownMermaidDiagramContents, fallback_image: Option<Arc<RenderImage>>, cx: &mut Context<MarkdownPreviewView>, ) -> Self { let result = Arc::new(OnceLock::<anyhow::Result<Arc<RenderImage>>>::new()); let result_clone = result.clone(); let svg_renderer = cx.svg_renderer(); let _task = cx.spawn(async move |this, cx| { let value = cx .background_spawn(async move { let svg_string = mermaid_rs_renderer::render(&contents.contents)?; let scale = contents.scale as f32 / 100.0; svg_renderer .render_single_frame(svg_string.as_bytes(), scale, true) .map_err(|e| anyhow::anyhow!("{}", e)) }) .await; let _ = result_clone.set(value); this.update(cx, |_, cx| { cx.notify(); }) .ok(); }); Self { render_image: result, fallback_image, _task, } } #[cfg(test)] fn new_for_test( render_image: Option<Arc<RenderImage>>, fallback_image: Option<Arc<RenderImage>>, ) -> Self { let result = Arc::new(OnceLock::new()); if let Some(img) = render_image { let _ = result.set(Ok(img)); } Self { render_image: result, fallback_image, _task: Task::ready(()), } } } #[derive(Clone)] pub struct RenderContext<'a> { workspace: Option<WeakEntity<Workspace>>, next_id: usize, buffer_font_family: SharedString, buffer_text_style: TextStyle, text_style: TextStyle, border_color: Hsla, title_bar_background_color: Hsla, panel_background_color: Hsla, text_color: Hsla, link_color: Hsla, window_rem_size: Pixels, text_muted_color: Hsla, code_block_background_color: Hsla, code_span_background_color: Hsla, syntax_theme: Arc<SyntaxTheme>, indent: usize, checkbox_clicked_callback: Option<CheckboxClickedCallback>, is_last_child: bool, mermaid_state: &'a MermaidState, } impl<'a> RenderContext<'a> { pub(crate) fn new( workspace: Option<WeakEntity<Workspace>>, mermaid_state: &'a MermaidState, window: &mut Window, cx: &mut App, ) -> Self { let theme = cx.theme().clone(); let settings = ThemeSettings::get_global(cx); let buffer_font_family = settings.buffer_font.family.clone(); let buffer_font_features = settings.buffer_font.features.clone(); let mut buffer_text_style = window.text_style(); buffer_text_style.font_family = buffer_font_family.clone(); buffer_text_style.font_features = buffer_font_features; buffer_text_style.font_size = AbsoluteLength::from(settings.buffer_font_size(cx)); RenderContext { workspace, next_id: 0, indent: 0, buffer_font_family, buffer_text_style, text_style: window.text_style(), syntax_theme: theme.syntax().clone(), border_color: theme.colors().border, title_bar_background_color: theme.colors().title_bar_background, panel_background_color: theme.colors().panel_background, text_color: theme.colors().text, link_color: theme.colors().text_accent, window_rem_size: window.rem_size(), text_muted_color: theme.colors().text_muted, code_block_background_color: theme.colors().surface_background, code_span_background_color: theme.colors().editor_document_highlight_read_background, checkbox_clicked_callback: None, is_last_child: false, mermaid_state, } } pub fn with_checkbox_clicked_callback( mut self, callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, ) -> Self { self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); self } fn next_id(&mut self, span: &Range<usize>) -> ElementId { let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end); self.next_id += 1; ElementId::from(SharedString::from(id)) } /// HACK: used to have rems relative to buffer font size, so that things scale appropriately as /// buffer font size changes. The callees of this function should be reimplemented to use real /// relative sizing once that is implemented in GPUI pub fn scaled_rems(&self, rems: f32) -> Rems { self.buffer_text_style .font_size .to_rems(self.window_rem_size) .mul(rems) } /// This ensures that children inside of block quotes /// have padding between them. /// /// For example, for this markdown: /// /// ```markdown /// > This is a block quote. /// > /// > And this is the next paragraph. /// ``` /// /// We give padding between "This is a block quote." /// and "And this is the next paragraph." fn with_common_p(&self, element: Div) -> Div { if self.indent > 0 && !self.is_last_child { element.pb(self.scaled_rems(0.75)) } else { element } } /// The is used to indicate that the current element is the last child or not of its parent. /// /// Then we can avoid adding padding to the bottom of the last child. fn with_last_child<R>(&mut self, is_last: bool, render: R) -> AnyElement where R: FnOnce(&mut Self) -> AnyElement, { self.is_last_child = is_last; let element = render(self); self.is_last_child = false; element } } pub fn render_parsed_markdown( parsed: &ParsedMarkdown, workspace: Option<WeakEntity<Workspace>>, window: &mut Window, cx: &mut App, ) -> Div { let cache = Default::default(); let mut cx = RenderContext::new(workspace, &cache, window, cx); v_flex().gap_3().children( parsed .children .iter() .map(|block| render_markdown_block(block, &mut cx)), ) } pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement { use ParsedMarkdownElement::*; match block { Paragraph(text) => render_markdown_paragraph(text, cx), Heading(heading) => render_markdown_heading(heading, cx), ListItem(list_item) => render_markdown_list_item(list_item, cx), Table(table) => render_markdown_table(table, cx), BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), CodeBlock(code_block) => render_markdown_code_block(code_block, cx), MermaidDiagram(mermaid) => render_mermaid_diagram(mermaid, cx), HorizontalRule(_) => render_markdown_rule(cx), Image(image) => render_markdown_image(image, cx), } } fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement { let size = match parsed.level { HeadingLevel::H1 => 2., HeadingLevel::H2 => 1.5, HeadingLevel::H3 => 1.25, HeadingLevel::H4 => 1., HeadingLevel::H5 => 0.875, HeadingLevel::H6 => 0.85, }; let text_size = cx.scaled_rems(size); // was `DefiniteLength::from(text_size.mul(1.25))` // let line_height = DefiniteLength::from(text_size.mul(1.25)); let line_height = text_size * 1.25; // was `rems(0.15)` // let padding_top = cx.scaled_rems(0.15); let padding_top = rems(0.15); // was `.pb_1()` = `rems(0.25)` // let padding_bottom = cx.scaled_rems(0.25); let padding_bottom = rems(0.25); let color = match parsed.level { HeadingLevel::H6 => cx.text_muted_color, _ => cx.text_color, }; div() .line_height(line_height) .text_size(text_size) .text_color(color) .pt(padding_top) .pb(padding_bottom) .children(render_markdown_text(&parsed.contents, cx)) .whitespace_normal() .into_any() } fn render_markdown_list_item( parsed: &ParsedMarkdownListItem, cx: &mut RenderContext, ) -> AnyElement { use ParsedMarkdownListItemType::*; let depth = parsed.depth.saturating_sub(1) as usize; let bullet = match &parsed.item_type { Ordered(order) => list_item_prefix(*order as usize, true, depth).into_any_element(), Unordered => list_item_prefix(1, false, depth).into_any_element(), Task(checked, range) => div() .id(cx.next_id(range)) .mt(cx.scaled_rems(3.0 / 16.0)) .child( MarkdownCheckbox::new( "checkbox", if *checked { ToggleState::Selected } else { ToggleState::Unselected }, cx.clone(), ) .when_some( cx.checkbox_clicked_callback.clone(), |this, callback| { this.on_click({ let range = range.clone(); move |selection, window, cx| { let checked = match selection { ToggleState::Selected => true, ToggleState::Unselected => false, _ => return, }; if window.modifiers().secondary() { callback( &CheckboxClickedEvent { checked, source_range: range.clone(), }, window, cx, ); } } }) }, ), ) .hover(|s| s.cursor_pointer()) .tooltip(|_, cx| { InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into() }) .into_any_element(), }; let bullet = div().mr(cx.scaled_rems(0.5)).child(bullet); let contents: Vec<AnyElement> = parsed .content .iter() .map(|c| render_markdown_block(c, cx)) .collect(); let item = h_flex() .when(!parsed.nested, |this| this.pl(cx.scaled_rems(depth as f32))) .when(parsed.nested && depth > 0, |this| this.ml_neg_1p5()) .items_start() .children(vec![ bullet, v_flex() .children(contents) .when(!parsed.nested, |this| this.gap(cx.scaled_rems(1.0))) .pr(cx.scaled_rems(1.0)) .w_full(), ]); cx.with_common_p(item).into_any() } /// # MarkdownCheckbox /// /// HACK: Copied from `ui/src/components/toggle.rs` to deal with scaling issues in markdown preview /// changes should be integrated into `Checkbox` in `toggle.rs` while making sure checkboxes elsewhere in the /// app are not visually affected #[derive(gpui::IntoElement)] struct MarkdownCheckbox { id: ElementId, toggle_state: ToggleState, disabled: bool, placeholder: bool, on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>, filled: bool, style: ui::ToggleStyle, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> gpui::AnyView>>, label: Option<SharedString>, base_rem: Rems, } impl MarkdownCheckbox { /// Creates a new [`Checkbox`]. fn new(id: impl Into<ElementId>, checked: ToggleState, render_cx: RenderContext) -> Self { Self { id: id.into(), toggle_state: checked, disabled: false, on_click: None, filled: false, style: ui::ToggleStyle::default(), tooltip: None, label: None, placeholder: false, base_rem: render_cx.scaled_rems(1.0), } } /// Binds a handler to the [`Checkbox`] that will be called when clicked. fn on_click(mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self } fn bg_color(&self, cx: &App) -> Hsla { let style = self.style.clone(); match (style, self.filled) { (ui::ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, (ui::ToggleStyle::Ghost, true) => cx.theme().colors().element_background, (ui::ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), (ui::ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), (ui::ToggleStyle::Custom(_), false) => gpui::transparent_black(), (ui::ToggleStyle::Custom(color), true) => color.opacity(0.2), } } fn border_color(&self, cx: &App) -> Hsla { if self.disabled { return cx.theme().colors().border_variant;