Documents: strong text keeps a heading's weight; sizes are tested
Headings and strong text share one bold weight, so strong text in a heading no longer draws lighter. A heading's line height is relative to its own size, and tests check every level's size against the body, a larger body and a zoomed document. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
acbbe1dbe5
commit
90bd37b550
2 files changed
+155
-77
No files matched your search
+64
-59
@@ -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 {
|
||||
|
||||
@@ -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<Self>) -> 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:?}");
|
||||
}
|
||||
Reference in new issue
Block a user