diff --git a/src/documents/render.rs b/src/documents/render.rs index f4a59e7..841e5c5 100644 --- a/src/documents/render.rs +++ b/src/documents/render.rs @@ -2,8 +2,8 @@ use std::{path::Path, rc::Rc, sync::Arc}; use gpui::{ AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText, - IntoElement, ParentElement, RenderOnce, SharedString, StrikethroughStyle, Styled, StyledText, - UnderlineStyle, Window, div, prelude::*, + IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StrikethroughStyle, Styled, + StyledText, UnderlineStyle, Window, div, prelude::*, relative, }; use super::{ @@ -16,7 +16,7 @@ use crate::{ editor::{code_colors, code_colors_with}, navigation::{Anchor, Sections}, primitives::FocusRing as _, - theme::{ActiveTheme, Radius, TextSize}, + theme::{ActiveTheme, Palette, Radius, TextSize}, typography::{Latex, failed}, }; @@ -101,6 +101,64 @@ pub(crate) struct Hooks<'a> { pub cell: Option<&'a OpenCell>, } +/// Headings and strong text share one weight. +const HEAVY: FontWeight = FontWeight::BOLD; + +/// How one inline style draws. +fn highlight(style: &Style, colors: &Palette, rule: Pixels) -> HighlightStyle { + match style { + Style::Strong => HighlightStyle { + font_weight: Some(HEAVY), + ..HighlightStyle::default() + }, + Style::Emphasis => HighlightStyle { + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + Style::Strike => HighlightStyle { + strikethrough: Some(StrikethroughStyle { + thickness: rule, + color: Some(colors.fg_muted), + }), + ..HighlightStyle::default() + }, + Style::Code | Style::Math => HighlightStyle { + background_color: Some(colors.sunken), + color: Some(if *style == Style::Math { + colors.syntax.function + } else { + colors.fg + }), + ..HighlightStyle::default() + }, + Style::Link => HighlightStyle { + color: Some(colors.link), + underline: Some(UnderlineStyle { + thickness: rule, + color: Some(colors.link.opacity(0.4)), + wavy: false, + }), + ..HighlightStyle::default() + }, + Style::Footnote => HighlightStyle { + color: Some(colors.fg_subtle), + ..HighlightStyle::default() + }, + Style::Underline => HighlightStyle { + underline: Some(UnderlineStyle { + thickness: rule, + color: None, + wavy: false, + }), + ..HighlightStyle::default() + }, + Style::Mark => HighlightStyle { + background_color: Some(colors.warning_subtle), + ..HighlightStyle::default() + }, + } +} + /// Draws nodes under one owner's ids and hooks. struct Draw<'a> { id: &'a ElementId, @@ -135,60 +193,7 @@ impl Draw<'_> { let styles: Vec<_> = inline .styles .iter() - .map(|(range, style)| { - let highlight = match style { - Style::Strong => HighlightStyle { - font_weight: Some(FontWeight::SEMIBOLD), - ..HighlightStyle::default() - }, - Style::Emphasis => HighlightStyle { - font_style: Some(FontStyle::Italic), - ..HighlightStyle::default() - }, - Style::Strike => HighlightStyle { - strikethrough: Some(StrikethroughStyle { - thickness: rule, - color: Some(colors.fg_muted), - }), - ..HighlightStyle::default() - }, - Style::Code | Style::Math => HighlightStyle { - background_color: Some(colors.sunken), - color: Some(if *style == Style::Math { - colors.syntax.function - } else { - colors.fg - }), - ..HighlightStyle::default() - }, - Style::Link => HighlightStyle { - color: Some(colors.link), - underline: Some(UnderlineStyle { - thickness: rule, - color: Some(colors.link.opacity(0.4)), - wavy: false, - }), - ..HighlightStyle::default() - }, - Style::Footnote => HighlightStyle { - color: Some(colors.fg_subtle), - ..HighlightStyle::default() - }, - Style::Underline => HighlightStyle { - underline: Some(UnderlineStyle { - thickness: rule, - color: None, - wavy: false, - }), - ..HighlightStyle::default() - }, - Style::Mark => HighlightStyle { - background_color: Some(colors.warning_subtle), - ..HighlightStyle::default() - }, - }; - (range.clone(), highlight) - }) + .map(|(range, style)| (range.clone(), highlight(style, colors, rule))) .collect(); let styled = StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles)); @@ -244,8 +249,8 @@ impl Draw<'_> { .debug_selector(move || format!("markdown-heading-{ordinal}")) .pt_2() .text_size(size) - .line_height(size * 1.3) - .font_weight(FontWeight::BOLD) + .line_height(relative(1.3)) + .font_weight(HEAVY) .text_color(if *level == 6 { colors.fg_muted } else { diff --git a/src/documents/render/tests.rs b/src/documents/render/tests.rs index aec42e3..6ba3a18 100644 --- a/src/documents/render/tests.rs +++ b/src/documents/render/tests.rs @@ -1,7 +1,7 @@ -use gpui::{Context, IntoElement, Render, TestAppContext, Window, div, prelude::*, px}; +use gpui::{Context, IntoElement, Render, TestAppContext, Window, div, prelude::*, px, rems}; -use super::MarkdownRenderer; -use crate::theme::Theme; +use super::{HEAVY, MarkdownRenderer, Style, highlight}; +use crate::theme::{ActiveTheme, TextSize, Theme}; /// `source` drawn in a 400-point column. struct Page(&'static str); @@ -52,10 +52,8 @@ fn alerts_lists_of_headings_and_front_matter_draw(cx: &mut TestAppContext) { assert!(two.top() > one.top(), "{one:?} {two:?}"); } -#[gpui::test] -fn each_heading_level_stands_above_the_next(cx: &mut TestAppContext) { - cx.update(Theme::init); - let source = "# a +/// One heading of every level. +const HEADINGS: &str = "# a ## a @@ -64,17 +62,92 @@ fn each_heading_level_stands_above_the_next(cx: &mut TestAppContext) { #### a ##### a + +###### a "; - let (_, cx) = cx.add_window_view(|_, _| Page(source)); + +/// `source` in a 400-point column, body at `size`. +struct Sized(&'static str, TextSize); + +impl Render for Sized { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .w(px(400.0)) + .child(MarkdownRenderer::new("page", self.0).text_size(self.1)) + } +} + +#[gpui::test] +fn headings_step_up_from_the_body_by_typoras_ratios(cx: &mut TestAppContext) { + cx.update(Theme::init); + for body in [TextSize::Base, TextSize::Lg] { + let (_, cx) = cx.add_window_view(move |_, _| Sized(HEADINGS, body)); + cx.run_until_parked(); + let (base, pad) = cx.update(|window, cx| { + let rem = window.rem_size(); + ( + cx.theme().text_size(body).to_pixels(rem), + rems(0.5).to_pixels(rem), + ) + }); + for (ordinal, step) in [2.25, 1.75, 1.5, 1.25, 1.0, 1.0].into_iter().enumerate() { + let selector = Box::leak(format!("markdown-heading-{ordinal}").into_boxed_str()); + let high = cx.debug_bounds(selector).expect("drawn").size.height; + let off = high - pad - base * step * 1.3; + assert!( + off.abs() < px(0.5), + "{body:?} level {}: {high:?}", + ordinal + 1 + ); + } + } +} + +#[gpui::test] +fn strong_text_in_a_heading_keeps_its_weight(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| { + Page( + "### Plain **Strong** plain +", + ) + }); cx.run_until_parked(); - let high = |ordinal: usize| { - let selector = Box::leak(format!("markdown-heading-{ordinal}").into_boxed_str()); - cx.debug_bounds(selector).expect("drawn").size.height - }; - let heights: Vec<_> = (0..5).map(high).collect(); - // Level 5 is body size; each above is taller. - assert!( - heights.windows(2).all(|pair| pair[0] > pair[1]), - "{heights:?}" - ); + assert!(cx.debug_bounds("markdown-heading-0").is_some(), "drawn"); + let strong = cx.update(|_, cx| { + let theme = cx.theme(); + highlight(&Style::Strong, &theme.colors, theme.underline_thickness()).font_weight + }); + assert_eq!(strong, Some(HEAVY), "strong never lightens a heading"); +} + +#[gpui::test] +fn a_zoomed_document_scales_its_headings(cx: &mut TestAppContext) { + use crate::documents::{DocumentLook, MarkdownDocument, MarkdownMode}; + cx.update(crate::init_for_tests); + let (document, cx) = cx.add_window_view(|window, cx| { + MarkdownDocument::new("### a\n", MarkdownMode::Preview, window, cx) + }); + document.update(cx, |document, cx| { + let look = DocumentLook { + scale: 1.5, + ..DocumentLook::default() + }; + document.set_look(look, cx); + }); + cx.run_until_parked(); + let (base, pad) = cx.update(|window, cx| { + let rem = window.rem_size(); + ( + cx.theme().text_size(TextSize::Base).to_pixels(rem), + rems(0.5).to_pixels(rem), + ) + }); + let high = cx + .debug_bounds("markdown-heading-0") + .expect("drawn") + .size + .height; + let off = high - pad - base * 1.5 * 1.5 * 1.3; + assert!(off.abs() < px(0.5), "{high:?}"); }