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::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
||||||
IntoElement, ParentElement, RenderOnce, SharedString, StrikethroughStyle, Styled, StyledText,
|
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StrikethroughStyle, Styled,
|
||||||
UnderlineStyle, Window, div, prelude::*,
|
StyledText, UnderlineStyle, Window, div, prelude::*, relative,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -16,7 +16,7 @@ use crate::{
|
|||||||
editor::{code_colors, code_colors_with},
|
editor::{code_colors, code_colors_with},
|
||||||
navigation::{Anchor, Sections},
|
navigation::{Anchor, Sections},
|
||||||
primitives::FocusRing as _,
|
primitives::FocusRing as _,
|
||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Palette, Radius, TextSize},
|
||||||
typography::{Latex, failed},
|
typography::{Latex, failed},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,6 +101,64 @@ pub(crate) struct Hooks<'a> {
|
|||||||
pub cell: Option<&'a OpenCell>,
|
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.
|
/// Draws nodes under one owner's ids and hooks.
|
||||||
struct Draw<'a> {
|
struct Draw<'a> {
|
||||||
id: &'a ElementId,
|
id: &'a ElementId,
|
||||||
@@ -135,60 +193,7 @@ impl Draw<'_> {
|
|||||||
let styles: Vec<_> = inline
|
let styles: Vec<_> = inline
|
||||||
.styles
|
.styles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(range, style)| {
|
.map(|(range, style)| (range.clone(), highlight(style, colors, rule)))
|
||||||
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)
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let styled =
|
let styled =
|
||||||
StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles));
|
StyledText::new(inline.text.clone()).with_highlights(crate::editor::stack(styles));
|
||||||
@@ -244,8 +249,8 @@ impl Draw<'_> {
|
|||||||
.debug_selector(move || format!("markdown-heading-{ordinal}"))
|
.debug_selector(move || format!("markdown-heading-{ordinal}"))
|
||||||
.pt_2()
|
.pt_2()
|
||||||
.text_size(size)
|
.text_size(size)
|
||||||
.line_height(size * 1.3)
|
.line_height(relative(1.3))
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(HEAVY)
|
||||||
.text_color(if *level == 6 {
|
.text_color(if *level == 6 {
|
||||||
colors.fg_muted
|
colors.fg_muted
|
||||||
} else {
|
} 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 super::{HEAVY, MarkdownRenderer, Style, highlight};
|
||||||
use crate::theme::Theme;
|
use crate::theme::{ActiveTheme, TextSize, Theme};
|
||||||
|
|
||||||
/// `source` drawn in a 400-point column.
|
/// `source` drawn in a 400-point column.
|
||||||
struct Page(&'static str);
|
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:?}");
|
assert!(two.top() > one.top(), "{one:?} {two:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
/// One heading of every level.
|
||||||
fn each_heading_level_stands_above_the_next(cx: &mut TestAppContext) {
|
const HEADINGS: &str = "# a
|
||||||
cx.update(Theme::init);
|
|
||||||
let source = "# a
|
|
||||||
|
|
||||||
## a
|
## a
|
||||||
|
|
||||||
@@ -64,17 +62,92 @@ fn each_heading_level_stands_above_the_next(cx: &mut TestAppContext) {
|
|||||||
#### a
|
#### a
|
||||||
|
|
||||||
##### 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();
|
cx.run_until_parked();
|
||||||
let high = |ordinal: usize| {
|
assert!(cx.debug_bounds("markdown-heading-0").is_some(), "drawn");
|
||||||
let selector = Box::leak(format!("markdown-heading-{ordinal}").into_boxed_str());
|
let strong = cx.update(|_, cx| {
|
||||||
cx.debug_bounds(selector).expect("drawn").size.height
|
let theme = cx.theme();
|
||||||
};
|
highlight(&Style::Strong, &theme.colors, theme.underline_thickness()).font_weight
|
||||||
let heights: Vec<_> = (0..5).map(high).collect();
|
});
|
||||||
// Level 5 is body size; each above is taller.
|
assert_eq!(strong, Some(HEAVY), "strong never lightens a heading");
|
||||||
assert!(
|
}
|
||||||
heights.windows(2).all(|pair| pair[0] > pair[1]),
|
|
||||||
"{heights:?}"
|
#[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