Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions parley/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ use parley_engine::break_overrides::LineBreakOverrideFn;

use crate::InlineBoxKind;
use crate::inline_box::InlineBox;
use crate::layout::data::StrutMetrics;
use crate::resolve::{ResolvedStyle, StyleRun, tree::ItemKind};
use parley_engine::FontMetrics;

#[derive(Clone, Copy)]
pub(crate) struct BuilderOptions<'a> {
scale: f32,
quantize: bool,
base_direction: BaseDirection,
line_break_override: Option<&'a LineBreakOverrideFn>,
compute_strut: bool,
}

impl BuilderOptions<'_> {
Expand All @@ -33,6 +36,7 @@ impl BuilderOptions<'_> {
quantize,
base_direction: BaseDirection::Auto,
line_break_override: None,
compute_strut: false,
}
}
}
Expand Down Expand Up @@ -84,6 +88,16 @@ impl<'b, B: Brush> RangedBuilder<'b, B> {
self.options.line_break_override = overrides;
}

/// Sets whether each line box's extents are initialized from a "strut" (CSS 2 § 10.8):
/// a zero-width inline box with the root style's primary font and line height.
///
/// When enabled, every line is sized as if it contained a zero-width glyph in the
/// root style, even if it contains no text (e.g. only inline boxes). This also allows
/// text with negative leading to produce lines shorter than the text's ascent + descent.
pub fn set_compute_strut(&mut self, compute_strut: bool) {
self.options.compute_strut = compute_strut;
}

pub fn build_into(self, layout: &mut Layout<B>, text: impl AsRef<str>) {
// Apply RangedStyleBuilder styles directly to style-table/style-run state.
self.lcx
Expand Down Expand Up @@ -288,6 +302,16 @@ impl<'b, B: Brush> TreeBuilder<'b, B> {
self.options.line_break_override = overrides;
}

/// Sets whether each line box's extents are initialized from a "strut" (CSS 2 § 10.8):
/// a zero-width inline box with the root style's primary font and line height.
///
/// When enabled, every line is sized as if it contained a zero-width glyph in the
/// root style, even if it contains no text (e.g. only inline boxes). This also allows
/// text with negative leading to produce lines shorter than the text's ascent + descent.
pub fn set_compute_strut(&mut self, compute_strut: bool) {
self.options.compute_strut = compute_strut;
}

#[inline]
pub fn build_into(self, layout: &mut Layout<B>) -> String {
// Apply TreeStyleBuilder styles to LayoutContext.
Expand Down Expand Up @@ -342,6 +366,13 @@ fn build_into_layout<B: Brush>(
layout.data.base_level = lcx.analysis.paragraph_level();
layout.data.text_len = text.len();

if options.compute_strut {
layout.data.strut = lcx
.root_style
.as_ref()
.and_then(|style| compute_strut(style, &lcx.rcx, fcx));
}

lcx.char_style_indices
.resize(lcx.analysis.char_info().len(), 0);
let mut char_index = 0;
Expand Down Expand Up @@ -382,6 +413,57 @@ fn build_into_layout<B: Brush>(
core::mem::swap(&mut layout.data.inline_boxes, &mut lcx.inline_boxes);
}

/// Compute the metrics of the "strut": a zero-width inline box with the root style's
/// primary font and line height (CSS 2 § 10.8).
fn compute_strut<B: Brush>(
style: &ResolvedStyle<B>,
rcx: &crate::resolve::ResolveContext,
fcx: &mut FontContext,
) -> Option<StrutMetrics> {
let families = rcx.stack(style.font_family).unwrap_or(&[]);
let mut query = fcx.collection.query(&mut fcx.source_cache);
query.set_families(families.iter().copied());
query.set_attributes(fontique::Attributes {
width: style.font_width,
weight: style.font_weight,
style: style.font_style,
});

// The strut takes its metrics from the "first available font": the first font in the
// family stack that contains the space character (U+0020), or the first font at all if
// none do. See <https://drafts.csswg.org/css-fonts/#first-available-font>.
let mut first_font = None;
let mut font = None;
query.matches_with(|f| {
if first_font.is_none() {
first_font = Some(f.clone());
}
let has_space = FontMetrics::font_covers_char(f.blob.as_ref(), f.index, ' ');
if has_space {
font = Some(f.clone());
fontique::QueryStatus::Stop
} else {
fontique::QueryStatus::Continue
}
});
let font = font.or(first_font)?;

let metrics = FontMetrics::from_font(font.blob.as_ref(), font.index, style.font_size)?;
let line_height = match style.line_height {
crate::LineHeight::Absolute(value) => value,
crate::LineHeight::FontSizeRelative(value) => value * style.font_size,
crate::LineHeight::MetricsRelative(value) => {
(metrics.ascent + metrics.descent + metrics.leading) * value
}
};

Some(StrutMetrics {
ascent: metrics.ascent,
descent: metrics.descent,
line_height,
})
}

fn resolve_range(range: impl RangeBounds<usize>, len: usize) -> Range<usize> {
let start = match range.start_bound() {
Bound::Unbounded => 0,
Expand Down
6 changes: 6 additions & 0 deletions parley/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use crate::inline_box::InlineBox;
/// This type is designed to be a global resource with only one per-application (or per-thread).
pub struct LayoutContext<B: Brush = [u8; 4]> {
pub(crate) rcx: ResolveContext,
/// The resolved root style of the current builder (if the builder has one).
pub(crate) root_style: Option<ResolvedStyle<B>>,
pub(crate) style_table: Vec<ResolvedStyle<B>>,
pub(crate) style_runs: Vec<StyleRun>,
pub(crate) inline_boxes: Vec<InlineBox>,
Expand All @@ -49,6 +51,7 @@ impl<B: Brush> LayoutContext<B> {
pub fn new() -> Self {
Self {
rcx: ResolveContext::default(),
root_style: None,
style_table: vec![],
style_runs: vec![],
inline_boxes: vec![],
Expand Down Expand Up @@ -102,6 +105,7 @@ impl<B: Brush> LayoutContext<B> {
self.begin();

let resolved_root_style = self.resolve_style_set(fcx, scale, &TextStyle::default());
self.root_style = Some(resolved_root_style.clone());
self.ranged_style_builder
.begin(resolved_root_style, text.len());

Expand Down Expand Up @@ -171,6 +175,7 @@ impl<B: Brush> LayoutContext<B> {
self.begin();

let resolved_root_style = self.resolve_style_set(fcx, scale, root_style);
self.root_style = Some(resolved_root_style.clone());
self.tree_style_builder.begin(resolved_root_style);

fcx.source_cache.prune(128, false);
Expand All @@ -184,6 +189,7 @@ impl<B: Brush> LayoutContext<B> {

fn begin(&mut self) {
self.rcx.clear();
self.root_style = None;
self.style_table.clear();
self.style_runs.clear();
self.inline_boxes.clear();
Expand Down
17 changes: 17 additions & 0 deletions parley/src/layout/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ pub(crate) struct RunData {
pub(crate) spacing: Spacing,
}

/// The metrics of the "strut": a zero-width inline box with the root style's primary font
/// and line height (CSS 2 § 10.8), used to initialize each line box's extents.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct StrutMetrics {
/// Distance from the baseline to the top of the strut's alignment box.
pub(crate) ascent: f32,
/// Distance from the baseline to the bottom of the strut's alignment box.
pub(crate) descent: f32,
/// The strut's line height.
pub(crate) line_height: f32,
}

#[derive(Copy, Clone, Default, PartialEq, Debug)]
pub enum BreakReason {
#[default]
Expand Down Expand Up @@ -190,6 +202,9 @@ pub(crate) struct LayoutData<B: Brush> {
/// The length of the text in the layout
pub(crate) text_len: usize,

/// The strut whose metrics initialize each line box's extents (if any).
pub(crate) strut: Option<StrutMetrics>,

// Output of style resolution (input to line breaking)
pub(crate) styles: Vec<Style<B>>,
pub(crate) inline_boxes: Vec<InlineBox>,
Expand Down Expand Up @@ -231,6 +246,7 @@ impl<B: Brush> Default for LayoutData<B> {
quantize: true,
base_level: BidiLevel::new(0),
text_len: 0,
strut: None,
width: 0.,
full_width: 0.,
height: 0.,
Expand All @@ -256,6 +272,7 @@ impl<B: Brush> LayoutData<B> {
self.quantize = true;
self.base_level = BidiLevel::new(0);
self.text_len = 0;
self.strut = None;
self.width = 0.;
self.full_width = 0.;
self.height = 0.;
Expand Down
18 changes: 16 additions & 2 deletions parley/src/layout/line_break.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,8 +1300,22 @@ impl<'a, B: Brush> BreakLines<'a, B> {
// Whether metrics should be quantized to pixel boundaries
let quantize = self.layout.data.quantize;

let mut line_box_extents = self.state.line.box_metrics.line_box.or_zero();
let mut content_box_extents = self.state.line.box_metrics.content_box.or_zero();
let mut line_box_extents = self.state.line.box_metrics.line_box;
let mut content_box_extents = self.state.line.box_metrics.content_box;

// Lines with content include the layout's strut (see `LayoutData::strut`): the extents
// of a zero-width text run in the layout's root style. Lines without content (e.g. a
// trailing empty line after a newline, or a line holding only out-of-flow boxes) do not
// get the strut, matching CSS's treatment of line boxes with no inline-level content.
if have_metrics && let Some(strut) = self.layout.data.strut {
let strut_extents =
text_extents(strut.ascent, strut.descent, strut.line_height, quantize);
line_box_extents.over = line_box_extents.over.max(strut_extents.over);
line_box_extents.under = line_box_extents.under.max(strut_extents.under);
}

let mut line_box_extents = line_box_extents.or_zero();
let mut content_box_extents = content_box_extents.or_zero();
if !have_metrics
&& line.item_range.is_empty()
&& let Some(metrics) = prev_line_metrics
Expand Down
97 changes: 65 additions & 32 deletions parley_engine/src/shape/shaped_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,65 @@ pub struct FontMetrics {
pub x_height: Option<f32>,
}

impl FontMetrics {
/// Compute the metrics of a font at a given size, at the default variation location.
///
/// Returns `None` if the font data cannot be read.
pub fn from_font(font_data: &[u8], font_index: u32, font_size: f32) -> Option<Self> {
let font_ref = skrifa::FontRef::from_index(font_data, font_index).ok()?;
let metrics = skrifa::metrics::Metrics::new(
&font_ref,
skrifa::prelude::Size::new(font_size),
skrifa::instance::LocationRef::default(),
);
Some(Self::from_skrifa_metrics(&metrics))
}

/// Whether a font has a glyph for the given character.
///
/// Returns `false` if the font data cannot be read.
pub fn font_covers_char(font_data: &[u8], font_index: u32, ch: char) -> bool {
skrifa::FontRef::from_index(font_data, font_index).is_ok_and(|font_ref| {
skrifa::MetadataProvider::charmap(&font_ref)
.map(ch)
.is_some()
})
}

fn from_skrifa_metrics(metrics: &skrifa::metrics::Metrics) -> Self {
let units_per_em = metrics.units_per_em as f32;

// TODO: The following seems to be in the wrong scale, as its staying in design units rather
// than scaled to the font size like the other fields for `FontMetrics`.
let (underline_offset, underline_size) = if let Some(underline) = metrics.underline {
(underline.offset, underline.thickness)
} else {
// Default values from Harfbuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334
let default = units_per_em / 18.0;
(default, default)
};
let (strikethrough_offset, strikethrough_size) = if let Some(strikeout) = metrics.strikeout
{
(strikeout.offset, strikeout.thickness)
} else {
// Default values from HarfBuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334-L347
(metrics.ascent / 2.0, units_per_em / 18.0)
};

Self {
ascent: metrics.ascent,
descent: -metrics.descent,
leading: metrics.leading,
underline_offset,
underline_size,
strikethrough_offset,
strikethrough_size,
x_height: metrics.x_height,
cap_height: metrics.cap_height,
}
}
}

/// The result of shaping.
///
/// After [analyzing][crate::Analysis] your text, [shape the text][crate::Shaper::shape_text],
Expand Down Expand Up @@ -252,46 +311,20 @@ impl ShapedText {
index
});

let metrics = {
let (font_metrics, units_per_em) = {
let font = &self.fonts[font_index];
let font_ref =
skrifa::FontRef::from_index(font.font.data.as_ref(), font.font.index).unwrap();
skrifa::metrics::Metrics::new(
let metrics = skrifa::metrics::Metrics::new(
&font_ref,
skrifa::prelude::Size::new(options.font_size),
normalized_coords,
);
(
FontMetrics::from_skrifa_metrics(&metrics),
metrics.units_per_em as f32,
)
};
let units_per_em = metrics.units_per_em as f32;

// TODO: The following seems to be in the wrong scale, as its staying in design units rather
// than scaled to the font size like the other fields for `FontMetrics`.
let (underline_offset, underline_size) = if let Some(underline) = metrics.underline {
(underline.offset, underline.thickness)
} else {
// Default values from Harfbuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334
let default = units_per_em / 18.0;
(default, default)
};
let (strikethrough_offset, strikethrough_size) = if let Some(strikeout) = metrics.strikeout
{
(strikeout.offset, strikeout.thickness)
} else {
// Default values from HarfBuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334-L347
(metrics.ascent / 2.0, units_per_em / 18.0)
};

let font_metrics = FontMetrics {
ascent: metrics.ascent,
descent: -metrics.descent,
leading: metrics.leading,
underline_offset,
underline_size,
strikethrough_offset,
strikethrough_size,
x_height: metrics.x_height,
cap_height: metrics.cap_height,
};

// `HarfRust` returns glyphs in visual order, so we need to process them as such while
// maintaining logical ordering of clusters.
Expand Down