Skip to content
6 changes: 5 additions & 1 deletion examples/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::time::Instant;
use parley::fontique::Blob;
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, GenericFamily, InlineBox,
Layout, LayoutContext, LineHeight, StyleProperty,
Layout, LayoutContext, LineHeight, StyleProperty, AlignmentBaseline, BaselineShift, BaselineSource,
};
use peniko::Color;

Expand Down Expand Up @@ -209,6 +209,10 @@ pub fn build_rich_layout(
index: 40,
width: 50.0,
height: 50.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});

let mut layout = builder.build(&config.text);
Expand Down
18 changes: 17 additions & 1 deletion examples/swash_render/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use image::codecs::png::PngEncoder;
use image::{self, Pixel, Rgba, RgbaImage};
use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem};
use parley::style::{FontFamily, FontWeight, StyleProperty, TextStyle};
use parley::{AlignmentOptions, FontContext, InlineBox, LayoutContext, LineHeight};
use parley::{AlignmentOptions, AlignmentBaseline, BaselineShift, BaselineSource, FontContext, InlineBox, LayoutContext, LineHeight};
use std::fs::File;
use swash::FontRef;
use swash::scale::image::Content;
Expand Down Expand Up @@ -97,6 +97,10 @@ fn main() {
index: 0,
width: 50.0,
height: 50.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});

builder.push_text(&text[40..50]);
Expand All @@ -106,6 +110,10 @@ fn main() {
index: 50,
width: 50.0,
height: 30.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});

builder.push_text(&text[50..141]);
Expand Down Expand Up @@ -155,12 +163,20 @@ fn main() {
index: 40,
width: 50.0,
height: 50.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});
builder.push_inline_box(InlineBox {
id: 1,
index: 50,
width: 50.0,
height: 30.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});

// Build the builder into a Layout
Expand Down
6 changes: 5 additions & 1 deletion examples/tiny_skia_render/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use parley::{
Alignment, AlignmentOptions, FontContext, FontWeight, GenericFamily, GlyphRun, InlineBox,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, AlignmentBaseline, BaselineShift, BaselineSource,
};
use skrifa::{
GlyphId, MetadataProvider, OutlineGlyph,
Expand Down Expand Up @@ -91,6 +91,10 @@ fn main() {
index: 40,
width: 50.0,
height: 50.0,
alignment_baseline: AlignmentBaseline::default(),
baseline_shift: BaselineShift::default(),
baseline_source: BaselineSource::default(),
first_baseline: None,
});

// Build the builder into a Layout
Expand Down
1 change: 1 addition & 0 deletions parley/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ impl<B: Brush> StyleRunBuilder<'_, B> {
self.cursor == self.len,
"StyleRunBuilder requires runs that cover the full text"
);

build_into_layout(
layout,
self.scale,
Expand Down
14 changes: 14 additions & 0 deletions parley/src/inline_box.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2024 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::{AlignmentBaseline, BaselineShift, BaselineSource};

/// A box to be laid out inline with text
#[derive(PartialEq, Debug, Clone)]
pub struct InlineBox {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking, this is orthogonal to this PR (but it may be useful to implement it here anyway, and it could also land before this PR), but:

InlineBox ought to gain a first_baseline: Option field. If that is Some then the inline box ought to be aligned using that as it's baseline. If it is None then it would fallback to aligning using the full height of the box (aligning to the bottom of the box), which is the current behaviour.

In Blitz, we'd source this value from Taffy and set it here before running text layout.

Expand All @@ -14,4 +16,16 @@ pub struct InlineBox {
pub width: f32,
/// The height of the box in pixels
pub height: f32,
/// Which baseline to align to (CSS `alignment-baseline`).
pub alignment_baseline: AlignmentBaseline,
/// How much to shift from the alignment baseline (CSS `baseline-shift`).
pub baseline_shift: BaselineShift,
/// Which baseline set to use (CSS `baseline-source`).
pub baseline_source: BaselineSource,
/// The distance from the top of the box to its internal text baseline, if the
/// box contains text content. When `Some(baseline)`, the box aligns using this
/// as its baseline (giving separate "ascent" and "descent" portions). When `None`,
/// falls back to aligning by the bottom of the box (the entire height is treated
/// as ascent above the baseline). In Blitz, this is sourced from Taffy's layout output.
pub first_baseline: Option<f32>,
}
53 changes: 50 additions & 3 deletions parley/src/layout/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::layout::{ContentWidths, Glyph, LineMetrics, RunMetrics, Style};
use crate::style::Brush;
use crate::util::nearly_zero;
use crate::{FontData, IndentOptions, LineHeight, OverflowWrap, TextWrapMode};
use skrifa::raw::TableProvider;
use core::ops::Range;

use alloc::vec::Vec;
Expand Down Expand Up @@ -189,6 +190,9 @@ pub(crate) struct LineItemData {
/// Advance (size in direction of text flow) for the run.
pub(crate) advance: f32,

/// Offset from the line baseline for vertical alignment (positive = down).
pub(crate) baseline_offset: f32,

// Fields that only apply to text runs (Ignored for boxes)
// TODO: factor this out?
/// True if the run is composed entirely of whitespace.
Expand Down Expand Up @@ -302,6 +306,17 @@ pub(crate) struct LayoutData<B: Brush> {
pub(crate) indent_amount: f32,
/// Options controlling text-indent behavior (each-line, hanging).
pub(crate) indent_options: IndentOptions,

// CSS strut metrics (CSS2.1 §10.8.1): the root style's font metrics
// that establish minimum line box height for every line.
/// Strut font ascent (from root style's font).
pub(crate) strut_ascent: f32,
/// Strut font descent (from root style's font).
pub(crate) strut_descent: f32,
/// Strut line height (from root style's line-height).
pub(crate) strut_line_height: f32,
/// Strut x-height (from root style's font, for vertical-align: middle).
pub(crate) strut_x_height: f32,
}

impl<B: Brush> Default for LayoutData<B> {
Expand Down Expand Up @@ -330,6 +345,10 @@ impl<B: Brush> Default for LayoutData<B> {
alignment_width: 0.0,
indent_amount: 0.0,
indent_options: IndentOptions::default(),
strut_ascent: 0.0,
strut_descent: 0.0,
strut_line_height: 0.0,
strut_x_height: 0.0,
}
}
}
Expand All @@ -353,6 +372,10 @@ impl<B: Brush> LayoutData<B> {
self.glyphs.clear();
self.lines.clear();
self.line_items.clear();
self.strut_ascent = 0.0;
self.strut_descent = 0.0;
self.strut_line_height = 0.0;
self.strut_x_height = 0.0;
}

/// Push an inline box to the list of items
Expand Down Expand Up @@ -398,10 +421,27 @@ impl<B: Brush> LayoutData<B> {
index
});

let metrics = {
let (metrics, subscript_offset, superscript_offset, os2_win_line_height) = {
let font = &self.fonts[font_index];
let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap();
skrifa::metrics::Metrics::new(&font_ref, skrifa::prelude::Size::new(font_size), coords)
let m = skrifa::metrics::Metrics::new(
&font_ref,
skrifa::prelude::Size::new(font_size),
coords,
);
let scale = font_size / m.units_per_em as f32;
// Read subscript/superscript offsets from the OS/2 table
let os2 = font_ref.os2().ok();
let sub_off = os2.as_ref().map(|t| t.y_subscript_y_offset() as f32 * scale);
let sup_off = os2.as_ref().map(|t| t.y_superscript_y_offset() as f32 * scale);
// OS/2 Win metrics (usWinAscent + usWinDescent) are often larger than
// sTypo/hhea metrics and closer to what browsers use for line-height: normal.
let os2_win_lh = os2.as_ref().map(|t| {
let win_ascent = t.us_win_ascent() as f32 * scale;
let win_descent = t.us_win_descent() as f32 * scale;
win_ascent + win_descent
});
(m, sub_off, sup_off, os2_win_lh)
};
let units_per_em = metrics.units_per_em as f32;

Expand All @@ -427,7 +467,12 @@ impl<B: Brush> LayoutData<B> {
LineHeight::Absolute(value) => value,
LineHeight::FontSizeRelative(value) => value * font_size,
LineHeight::MetricsRelative(value) => {
(metrics.ascent - metrics.descent + metrics.leading) * value
// Prefer OS/2 Win metrics (usWinAscent + usWinDescent) when
// available — they're closer to what browsers use for
// line-height: normal. Fall back to skrifa's default metrics.
let base = os2_win_line_height
.unwrap_or(metrics.ascent - metrics.descent + metrics.leading);
base * value
}
};

Expand All @@ -442,6 +487,8 @@ impl<B: Brush> LayoutData<B> {
line_height,
x_height: metrics.x_height,
cap_height: metrics.cap_height,
subscript_offset,
superscript_offset,
}
};

Expand Down
18 changes: 18 additions & 0 deletions parley/src/layout/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ impl<B: Brush> Layout<B> {
})
}

/// Sets the CSS strut metrics for the layout (CSS2.1 §10.8.1).
///
/// The strut is an imaginary zero-width inline box on each line with the root
/// element's font metrics and line-height. It establishes the minimum line box
/// height, ensuring that lines containing only inline boxes (e.g. icons) still
/// get the height dictated by the parent's font.
///
/// All values should be in physical pixels (i.e. already scaled by the device
/// pixel ratio).
///
/// This must be called before [`Layout::break_all_lines`] or [`Layout::break_lines`].
pub fn set_strut(&mut self, ascent: f32, descent: f32, line_height: f32, x_height: f32) {
self.data.strut_ascent = ascent;
self.data.strut_descent = descent;
self.data.strut_line_height = line_height;
self.data.strut_x_height = x_height;
}

/// Sets the text-indent for the layout.
///
/// The indent is applied as a margin on the start edge of indented lines, reducing the
Expand Down
25 changes: 23 additions & 2 deletions parley/src/layout/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ impl<'a, B: Brush> Line<'a, B> {
})
}

/// Returns the baseline offset for the item at the given line-relative index.
fn baseline_offset(&self, item_index: usize) -> f32 {
let abs_idx = self.data.item_range.start + item_index;
self.layout
.data
.line_items
.get(abs_idx)
.map(|li| li.baseline_offset)
.unwrap_or(0.0)
}

/// Returns an iterator over the glyph runs for the line.
pub fn items(&self) -> impl Iterator<Item = PositionedLayoutItem<'a, B>> + 'a + Clone {
GlyphRunIter {
Expand Down Expand Up @@ -252,13 +263,21 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
match item {
LineItem::InlineBox(inline_box) => {
let x = self.offset + self.line.data.metrics.offset;
let baseline_offset = self.line.baseline_offset(self.item_index);

self.item_index += 1;
self.glyph_start = 0;
self.offset += inline_box.width;
let y = self.line.data.metrics.baseline + baseline_offset - inline_box.height;
// #[cfg(feature = "std")]
// std::eprintln!(
// "[pos ibox] id={} x={:.1} y={:.1} w={:.1} h={:.1} baseline={:.1} offset={:.1}",
// inline_box.id, x, y, inline_box.width, inline_box.height,
// self.line.data.metrics.baseline, baseline_offset
// );
return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox {
x,
y: self.line.data.metrics.baseline - inline_box.height,
y,
width: inline_box.width,
height: inline_box.height,
id: inline_box.id,
Expand All @@ -279,6 +298,8 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
advance += glyph.advance;
}
let style = run.layout.data.styles.get(style_index)?;
let baseline_offset = self.line.baseline_offset(self.item_index);

let glyph_start = self.glyph_start;
self.glyph_start += glyph_count;
let offset = self.offset;
Expand All @@ -289,7 +310,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
glyph_start,
glyph_count,
offset: offset + self.line.data.metrics.offset,
baseline: self.line.data.metrics.baseline,
baseline: self.line.data.metrics.baseline + baseline_offset,
advance,
}));
}
Expand Down
Loading
Loading