From f506abbe9ef37b87a8078e41fd8ca0f65827cd4f Mon Sep 17 00:00:00 2001
From: valadaptive <valadaptive@protonmail.com>
Date: Tue, 4 Mar 2025 19:32:04 -0500
Subject: [PATCH 1/4] Add vertical alignment properties

---
 examples/swash_render/src/main.rs     | 28 ++--------
 examples/tiny_skia_render/src/main.rs |  7 +--
 parley/src/inline_box.rs              | 19 +++++++
 parley/src/lib.rs                     |  2 +-
 parley/src/resolve/mod.rs             | 36 ++++++++++++-
 parley/src/style/mod.rs               | 36 +++++++++++++
 parley/src/tests/test_basic.rs        | 77 ++++-----------------------
 7 files changed, 107 insertions(+), 98 deletions(-)

diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs
index fbe8fdfa..ad4d6312 100644
--- a/examples/swash_render/src/main.rs
+++ b/examples/swash_render/src/main.rs
@@ -92,21 +92,11 @@ fn main() {
 
         builder.push_text(&text[5..40]);
 
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: 0,
-            width: 50.0,
-            height: 50.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, 0, 50.0, 50.0));
 
         builder.push_text(&text[40..50]);
 
-        builder.push_inline_box(InlineBox {
-            id: 1,
-            index: 50,
-            width: 50.0,
-            height: 30.0,
-        });
+        builder.push_inline_box(InlineBox::new(1, 50, 50.0, 30.0));
 
         builder.push_text(&text[50..141]);
 
@@ -147,18 +137,8 @@ fn main() {
         builder.push(underline_style, 141..150);
         builder.push(strikethrough_style, 155..168);
 
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: 40,
-            width: 50.0,
-            height: 50.0,
-        });
-        builder.push_inline_box(InlineBox {
-            id: 1,
-            index: 50,
-            width: 50.0,
-            height: 30.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, 40, 50.0, 50.0));
+        builder.push_inline_box(InlineBox::new(1, 50, 50.0, 30.0));
 
         // Build the builder into a Layout
         // let mut layout: Layout<ColorBrush> = builder.build(&text);
diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs
index 734d4fdc..465b0426 100644
--- a/examples/tiny_skia_render/src/main.rs
+++ b/examples/tiny_skia_render/src/main.rs
@@ -87,12 +87,7 @@ fn main() {
     builder.push(StyleProperty::Underline(true), 141..150);
     builder.push(StyleProperty::Strikethrough(true), 155..168);
 
-    builder.push_inline_box(InlineBox {
-        id: 0,
-        index: 40,
-        width: 50.0,
-        height: 50.0,
-    });
+    builder.push_inline_box(InlineBox::new(0, 40, 50.0, 50.0));
 
     // Build the builder into a Layout
     let mut layout: Layout<ColorBrush> = builder.build(&text);
diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs
index 9731fd9e..ccdb5508 100644
--- a/parley/src/inline_box.rs
+++ b/parley/src/inline_box.rs
@@ -1,6 +1,8 @@
 // Copyright 2024 the Parley Authors
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
+use crate::{BaselineShift, VerticalAlign};
+
 /// A box to be laid out inline with text
 #[derive(Debug, Clone)]
 pub struct InlineBox {
@@ -14,4 +16,21 @@ pub struct InlineBox {
     pub width: f32,
     /// The height of the box in pixels
     pub height: f32,
+    /// The baseline along which this item is aligned.
+    pub vertical_align: VerticalAlign,
+    /// Additional baseline alignment applied afterwards.
+    pub baseline_shift: BaselineShift,
+}
+
+impl InlineBox {
+    pub fn new(id: u64, index: usize, width: f32, height: f32) -> Self {
+        Self {
+            id,
+            index,
+            width,
+            height,
+            vertical_align: Default::default(),
+            baseline_shift: Default::default(),
+        }
+    }
 }
diff --git a/parley/src/lib.rs b/parley/src/lib.rs
index 42a6fc74..96be0f75 100644
--- a/parley/src/lib.rs
+++ b/parley/src/lib.rs
@@ -45,7 +45,7 @@
 //! builder.push(StyleProperty::FontWeight(FontWeight::new(600.0)), 0..4);
 //!
 //! // Add a box to be laid out inline with the text
-//! builder.push_inline_box(InlineBox { id: 0, index: 5, width: 50.0, height: 50.0 });
+//! builder.push_inline_box(InlineBox::new(0, 5, 50.0, 50.0));
 //!
 //! // Build the builder into a Layout
 //! let mut layout: Layout<()> = builder.build(&TEXT);
diff --git a/parley/src/resolve/mod.rs b/parley/src/resolve/mod.rs
index a05eaa8c..4436b2e8 100644
--- a/parley/src/resolve/mod.rs
+++ b/parley/src/resolve/mod.rs
@@ -15,9 +15,9 @@ use super::style::{
     FontWidth, StyleProperty,
 };
 use crate::font::FontContext;
-use crate::layout;
 use crate::style::TextStyle;
 use crate::util::nearly_eq;
+use crate::{layout, BaselineShift, VerticalAlign};
 use core::borrow::Borrow;
 use core::ops::Range;
 use fontique::FamilyId;
@@ -155,6 +155,16 @@ impl ResolveContext {
             StyleProperty::LineHeight(value) => LineHeight(*value),
             StyleProperty::WordSpacing(value) => WordSpacing(*value * scale),
             StyleProperty::LetterSpacing(value) => LetterSpacing(*value * scale),
+            StyleProperty::VerticalAlign(value) => VerticalAlign(*value),
+            StyleProperty::BaselineShift(value) => BaselineShift(match value {
+                crate::BaselineShift::Absolute(value) => {
+                    crate::BaselineShift::Absolute(*value * scale)
+                }
+                crate::BaselineShift::Relative(value) => {
+                    crate::BaselineShift::Relative(*value * scale)
+                }
+                _ => *value,
+            }),
         }
     }
 
@@ -189,6 +199,8 @@ impl ResolveContext {
             line_height: raw_style.line_height,
             word_spacing: raw_style.word_spacing * scale,
             letter_spacing: raw_style.letter_spacing * scale,
+            vertical_align: raw_style.vertical_align,
+            baseline_shift: raw_style.baseline_shift,
         }
     }
 
@@ -366,6 +378,10 @@ pub(crate) enum ResolvedProperty<B: Brush> {
     WordSpacing(f32),
     /// Extra spacing between letters.
     LetterSpacing(f32),
+    /// The baseline along which this item is aligned.
+    VerticalAlign(VerticalAlign),
+    /// Additional baseline alignment applied afterwards.
+    BaselineShift(BaselineShift),
 }
 
 /// Flattened group of style properties.
@@ -399,6 +415,10 @@ pub(crate) struct ResolvedStyle<B: Brush> {
     pub(crate) word_spacing: f32,
     /// Extra spacing between letters.
     pub(crate) letter_spacing: f32,
+    /// The baseline along which this item is aligned.
+    pub vertical_align: VerticalAlign,
+    /// Additional baseline alignment applied afterwards.
+    pub baseline_shift: BaselineShift,
 }
 
 impl<B: Brush> Default for ResolvedStyle<B> {
@@ -418,6 +438,8 @@ impl<B: Brush> Default for ResolvedStyle<B> {
             line_height: 1.,
             word_spacing: 0.,
             letter_spacing: 0.,
+            vertical_align: Default::default(),
+            baseline_shift: Default::default(),
         }
     }
 }
@@ -447,6 +469,8 @@ impl<B: Brush> ResolvedStyle<B> {
             LineHeight(value) => self.line_height = value,
             WordSpacing(value) => self.word_spacing = value,
             LetterSpacing(value) => self.letter_spacing = value,
+            VerticalAlign(value) => self.vertical_align = value,
+            BaselineShift(value) => self.baseline_shift = value,
         }
     }
 
@@ -473,6 +497,16 @@ impl<B: Brush> ResolvedStyle<B> {
             LineHeight(value) => nearly_eq(self.line_height, *value),
             WordSpacing(value) => nearly_eq(self.word_spacing, *value),
             LetterSpacing(value) => nearly_eq(self.letter_spacing, *value),
+            VerticalAlign(value) => self.vertical_align == *value,
+            BaselineShift(value) => match (self.baseline_shift, value) {
+                (crate::BaselineShift::Absolute(ours), crate::BaselineShift::Absolute(value)) => {
+                    nearly_eq(ours, *value)
+                }
+                (crate::BaselineShift::Relative(ours), crate::BaselineShift::Relative(value)) => {
+                    nearly_eq(ours, *value)
+                }
+                _ => false,
+            },
         }
     }
 
diff --git a/parley/src/style/mod.rs b/parley/src/style/mod.rs
index 841b5b29..54ea096f 100644
--- a/parley/src/style/mod.rs
+++ b/parley/src/style/mod.rs
@@ -22,6 +22,32 @@ pub enum WhiteSpaceCollapse {
     Preserve,
 }
 
+#[derive(Clone, Copy, PartialEq, Debug, Default)]
+pub enum VerticalAlign {
+    #[default]
+    Baseline,
+    TextBottom,
+    Middle,
+    TextTop,
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum BaselineShift {
+    Absolute(f32),
+    Relative(f32),
+    Sub,
+    Super,
+    Top,
+    Center,
+    Bottom,
+}
+
+impl Default for BaselineShift {
+    fn default() -> Self {
+        BaselineShift::Absolute(0.0)
+    }
+}
+
 /// Properties that define a style.
 #[derive(Clone, PartialEq, Debug)]
 pub enum StyleProperty<'a, B: Brush> {
@@ -65,6 +91,10 @@ pub enum StyleProperty<'a, B: Brush> {
     WordSpacing(f32),
     /// Extra spacing between letters.
     LetterSpacing(f32),
+    /// The baseline along which this item is aligned.
+    VerticalAlign(VerticalAlign),
+    /// Additional baseline alignment applied afterwards.
+    BaselineShift(BaselineShift),
 }
 
 /// Unresolved styles.
@@ -110,6 +140,10 @@ pub struct TextStyle<'a, B: Brush> {
     pub word_spacing: f32,
     /// Extra spacing between letters.
     pub letter_spacing: f32,
+    /// The baseline along which this item is aligned.
+    pub vertical_align: VerticalAlign,
+    /// Additional baseline alignment applied afterwards.
+    pub baseline_shift: BaselineShift,
 }
 
 impl<B: Brush> Default for TextStyle<'_, B> {
@@ -135,6 +169,8 @@ impl<B: Brush> Default for TextStyle<'_, B> {
             line_height: 1.2,
             word_spacing: Default::default(),
             letter_spacing: Default::default(),
+            vertical_align: Default::default(),
+            baseline_shift: Default::default(),
         }
     }
 }
diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs
index bdd671af..4b46850a 100644
--- a/parley/src/tests/test_basic.rs
+++ b/parley/src/tests/test_basic.rs
@@ -30,12 +30,7 @@ fn placing_inboxes() {
     ] {
         let text = "Hello world!\nLine 2\nLine 4";
         let mut builder = env.ranged_builder(text);
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: position,
-            width: 10.0,
-            height: 10.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, position, 10.0, 10.0));
         let mut layout = builder.build(text);
         layout.break_all_lines(None);
         layout.align(None, Alignment::Start, AlignmentOptions::default());
@@ -50,12 +45,7 @@ fn only_inboxes_wrap() {
     let text = "";
     let mut builder = env.ranged_builder(text);
     for id in 0..10 {
-        builder.push_inline_box(InlineBox {
-            id,
-            index: 0,
-            width: 10.0,
-            height: 10.0,
-        });
+        builder.push_inline_box(InlineBox::new(id, 0, 10.0, 10.0));
     }
     let mut layout = builder.build(text);
     layout.break_all_lines(Some(40.0));
@@ -71,24 +61,9 @@ fn full_width_inbox() {
     for (width, test_case_name) in [(99., "smaller"), (100., "exact"), (101., "larger")] {
         let text = "ABC";
         let mut builder = env.ranged_builder(text);
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: 1,
-            width: 10.,
-            height: 10.0,
-        });
-        builder.push_inline_box(InlineBox {
-            id: 1,
-            index: 1,
-            width,
-            height: 10.0,
-        });
-        builder.push_inline_box(InlineBox {
-            id: 2,
-            index: 2,
-            width,
-            height: 10.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, 1, 10., 10.0));
+        builder.push_inline_box(InlineBox::new(1, 1, width, 10.0));
+        builder.push_inline_box(InlineBox::new(2, 2, width, 10.0));
         let mut layout = builder.build(text);
         layout.break_all_lines(Some(100.));
         layout.align(None, Alignment::Start, AlignmentOptions::default());
@@ -101,33 +76,13 @@ fn inbox_separated_by_whitespace() {
     let mut env = testenv!();
 
     let mut builder = env.tree_builder();
-    builder.push_inline_box(InlineBox {
-        id: 0,
-        index: 0,
-        width: 10.,
-        height: 10.0,
-    });
+    builder.push_inline_box(InlineBox::new(0, 0, 10., 10.0));
     builder.push_text(" ");
-    builder.push_inline_box(InlineBox {
-        id: 1,
-        index: 1,
-        width: 10.0,
-        height: 10.0,
-    });
+    builder.push_inline_box(InlineBox::new(1, 1, 10.0, 10.0));
     builder.push_text(" ");
-    builder.push_inline_box(InlineBox {
-        id: 2,
-        index: 2,
-        width: 10.0,
-        height: 10.0,
-    });
+    builder.push_inline_box(InlineBox::new(2, 2, 10.0, 10.0));
     builder.push_text(" ");
-    builder.push_inline_box(InlineBox {
-        id: 3,
-        index: 3,
-        width: 10.0,
-        height: 10.0,
-    });
+    builder.push_inline_box(InlineBox::new(3, 3, 10.0, 10.0));
     let (mut layout, _text) = builder.build();
     layout.break_all_lines(Some(100.));
     layout.align(None, Alignment::Start, AlignmentOptions::default());
@@ -276,12 +231,7 @@ fn inbox_content_width() {
     {
         let text = "Hello world!";
         let mut builder = env.ranged_builder(text);
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: 3,
-            width: 100.0,
-            height: 10.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, 3, 100.0, 10.0));
         let mut layout = builder.build(text);
         layout.break_all_lines(Some(layout.min_content_width()));
         layout.align(None, Alignment::Start, AlignmentOptions::default());
@@ -292,12 +242,7 @@ fn inbox_content_width() {
     {
         let text = "A ";
         let mut builder = env.ranged_builder(text);
-        builder.push_inline_box(InlineBox {
-            id: 0,
-            index: 2,
-            width: 10.0,
-            height: 10.0,
-        });
+        builder.push_inline_box(InlineBox::new(0, 2, 10.0, 10.0));
         let mut layout = builder.build(text);
         layout.break_all_lines(Some(layout.max_content_width()));
         layout.align(None, Alignment::Start, AlignmentOptions::default());

From 1a3ee25e72cabfb678ce780cb5741fd7f1c4eaa8 Mon Sep 17 00:00:00 2001
From: valadaptive <valadaptive@protonmail.com>
Date: Wed, 5 Mar 2025 03:37:51 -0500
Subject: [PATCH 2/4] Use display_scale more in swash_render

---
 examples/swash_render/src/main.rs | 30 +++++++++++++++++++++++++-----
 1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs
index ad4d6312..614c915c 100644
--- a/examples/swash_render/src/main.rs
+++ b/examples/swash_render/src/main.rs
@@ -52,7 +52,7 @@ fn main() {
     let bg_color = Rgba([255, 255, 255, 255]);
 
     // Padding around the output image
-    let padding = 20;
+    let padding = (20f32 * display_scale).round() as u32;
 
     // Create a FontContext, LayoutContext and ScaleContext
     //
@@ -92,11 +92,21 @@ fn main() {
 
         builder.push_text(&text[5..40]);
 
-        builder.push_inline_box(InlineBox::new(0, 0, 50.0, 50.0));
+        builder.push_inline_box(InlineBox::new(
+            0,
+            0,
+            50.0 * display_scale,
+            50.0 * display_scale,
+        ));
 
         builder.push_text(&text[40..50]);
 
-        builder.push_inline_box(InlineBox::new(1, 50, 50.0, 30.0));
+        builder.push_inline_box(InlineBox::new(
+            1,
+            50,
+            50.0 * display_scale,
+            30.0 * display_scale,
+        ));
 
         builder.push_text(&text[50..141]);
 
@@ -137,8 +147,18 @@ fn main() {
         builder.push(underline_style, 141..150);
         builder.push(strikethrough_style, 155..168);
 
-        builder.push_inline_box(InlineBox::new(0, 40, 50.0, 50.0));
-        builder.push_inline_box(InlineBox::new(1, 50, 50.0, 30.0));
+        builder.push_inline_box(InlineBox::new(
+            0,
+            40,
+            50.0 * display_scale,
+            50.0 * display_scale,
+        ));
+        builder.push_inline_box(InlineBox::new(
+            1,
+            50,
+            50.0 * display_scale,
+            30.0 * display_scale,
+        ));
 
         // Build the builder into a Layout
         // let mut layout: Layout<ColorBrush> = builder.build(&text);

From c80d069901b556ff56cc57c94c75c1e870b17ae8 Mon Sep 17 00:00:00 2001
From: valadaptive <valadaptive@protonmail.com>
Date: Thu, 6 Mar 2025 21:51:06 -0500
Subject: [PATCH 3/4] [WIP] Calculate line box bounds for vertical align

This takes one vertical position as the origin, and aligns everything to
that, e.g. if you select text-top, the glyph's ascender will be at the
origin. This works well if every glyph has the same vertical alignment,
but if not, it falls apart (e.g. if one glyph is aligned to text-top,
and the next is aligned to text-bottom, they'll be extremely vertically
separated). What CSS does is align everything using the metrics of the
"parent style".

I didn't get as far as implementing the alignment for actual glyph
positioning, only line height calculation.
---
 parley/src/inline_box.rs         |   4 +-
 parley/src/layout/data.rs        |  32 ++++++
 parley/src/layout/line/greedy.rs | 189 ++++++++++++++++++++++++++-----
 parley/src/layout/line/mod.rs    |   7 ++
 parley/src/layout/mod.rs         |   6 +-
 parley/src/layout/run.rs         |   8 +-
 parley/src/resolve/mod.rs        |   2 +
 parley/src/style/mod.rs          |  34 +++++-
 8 files changed, 245 insertions(+), 37 deletions(-)

diff --git a/parley/src/inline_box.rs b/parley/src/inline_box.rs
index ccdb5508..aa512d97 100644
--- a/parley/src/inline_box.rs
+++ b/parley/src/inline_box.rs
@@ -1,7 +1,7 @@
 // Copyright 2024 the Parley Authors
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
-use crate::{BaselineShift, VerticalAlign};
+use crate::{ResolvedBaselineShift, VerticalAlign};
 
 /// A box to be laid out inline with text
 #[derive(Debug, Clone)]
@@ -19,7 +19,7 @@ pub struct InlineBox {
     /// The baseline along which this item is aligned.
     pub vertical_align: VerticalAlign,
     /// Additional baseline alignment applied afterwards.
-    pub baseline_shift: BaselineShift,
+    pub baseline_shift: ResolvedBaselineShift,
 }
 
 impl InlineBox {
diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs
index 4f80c5ef..f4f07d7e 100644
--- a/parley/src/layout/data.rs
+++ b/parley/src/layout/data.rs
@@ -86,6 +86,36 @@ pub(crate) struct RunData {
     pub(crate) advance: f32,
 }
 
+impl RunData {
+    pub(crate) fn unique_styles<'a, 'b, B: Brush>(&'a self, layout: &'b LayoutData<B>, mut cb: impl FnMut(&'b Style<B>)) {
+        let glyph_start = self.glyph_start;
+        for cluster in &layout.clusters[self.cluster_range.clone()] {
+            if cluster.glyph_len != 0xFF && cluster.has_divergent_styles() {
+                let mut start = glyph_start + cluster.glyph_offset as usize;
+                let end = start + cluster.glyph_len as usize;
+
+                'runs: loop {
+                    let start_glyph = &layout.glyphs[start];
+                    cb(&layout.styles[start_glyph.style_index()]);
+
+                    loop {
+                        start += 1;
+
+                        if start >= end {
+                            break 'runs;
+                        }
+                        if layout.glyphs[start].style_index != start_glyph.style_index {
+                            break;
+                        }
+                    }
+                }
+            } else {
+                cb(&layout.styles[cluster.style_index as usize]);
+            }
+        }
+    }
+}
+
 #[derive(Copy, Clone, Default, PartialEq, Debug)]
 pub enum BreakReason {
     #[default]
@@ -335,6 +365,8 @@ impl<B: Brush> LayoutData<B> {
                 ascent: metrics.ascent,
                 descent: metrics.descent,
                 leading: metrics.leading,
+                x_height: metrics.x_height,
+                font_size,
                 underline_offset: metrics.underline_offset,
                 underline_size: metrics.stroke_size,
                 strikethrough_offset: metrics.strikeout_offset,
diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs
index 2fccdc87..1c13fa76 100644
--- a/parley/src/layout/line/greedy.rs
+++ b/parley/src/layout/line/greedy.rs
@@ -15,6 +15,7 @@ use crate::layout::{
     LineMetrics, Run,
 };
 use crate::style::Brush;
+use crate::{ResolvedBaselineShift, VerticalAlign};
 
 use core::ops::Range;
 
@@ -440,7 +441,7 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                 }
             }
         }
-        let mut y = 0.;
+        let mut y = 0f32;
         let mut prev_line_metrics = None;
         for line in &mut self.lines.lines {
             // Reset metrics for line
@@ -454,12 +455,20 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                 line.text_range = self.layout.data.text_len..self.layout.data.text_len;
             }
             // Compute metrics for the line, but ignore trailing whitespace.
-            let mut have_metrics = false;
             let mut needs_reorder = false;
+            let mut line_relative_alignment_needed = false;
+            // Accumulate the top and bottom bounds of all the positioned items in the line, relative to the baseline (which is the origin)
+            let mut top = f32::INFINITY;
+            let mut ascent = f32::INFINITY;
+            let mut bottom = f32::NEG_INFINITY;
+            let mut descent = f32::NEG_INFINITY;
+            let mut min_line_height = 0f32;
+
             for line_item in self.lines.line_items[line.item_range.clone()]
                 .iter_mut()
                 .rev()
             {
+                //let item_line_height = line_item.compute_line_height(&self.layout.data);
                 match line_item.kind {
                     LayoutItemKind::InlineBox => {
                         let item = &self.layout.data.inline_boxes[line_item.index];
@@ -468,11 +477,28 @@ impl<'a, B: Brush> BreakLines<'a, B> {
 
                         // Default vertical alignment is to align the bottom of boxes with the text baseline.
                         // This is equivalent to the entire height of the box being "ascent"
-                        line.metrics.ascent = line.metrics.ascent.max(item.height);
-                        line.metrics.line_height = line.metrics.line_height.max(item.height);
+                        min_line_height = min_line_height.max(item.height);
+                        let (mut style_top, mut style_bottom) = match item.vertical_align {
+                            VerticalAlign::Baseline | VerticalAlign::TextBottom => {
+                                (-item.height, 0.0)
+                            }
+                            VerticalAlign::Middle => (-item.height * 0.5, item.height * 0.5),
+                            VerticalAlign::TextTop => (0.0, item.height),
+                        };
+                        let offset = match item.baseline_shift {
+                            ResolvedBaselineShift::Absolute(value) => value,
+                            ResolvedBaselineShift::Top
+                            | ResolvedBaselineShift::Center
+                            | ResolvedBaselineShift::Bottom => {
+                                line_relative_alignment_needed = true;
+                                continue;
+                            }
+                        };
 
-                        // Mark us as having seen non-whitespace content on this line
-                        have_metrics = true;
+                        style_top += offset;
+                        style_bottom += offset;
+                        top = top.min(style_top);
+                        bottom = bottom.max(style_bottom);
                     }
                     LayoutItemKind::TextRun => {
                         // Compute the text range for the line
@@ -488,8 +514,10 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                         }
 
                         let run = &self.layout.data.runs[line_item.index];
-                        let line_height = line_item.compute_line_height(&self.layout.data);
-                        line.metrics.line_height = line.metrics.line_height.max(line_height);
+
+                        /*run.unique_styles(&self.layout.data, |style| {
+                            line.metrics.line_height = line.metrics.line_height.max(style.line_height);
+                        });*/
 
                         // Compute the run's advance by summing the advances of its constituent clusters
                         line_item.advance = self.layout.data.clusters
@@ -500,17 +528,101 @@ impl<'a, B: Brush> BreakLines<'a, B> {
 
                         // Ignore trailing whitespace for metrics computation
                         // (we are iterating backwards so trailing whitespace comes first)
+                        let have_metrics = top.is_finite();
                         if !have_metrics && line_item.is_whitespace {
                             continue;
                         }
 
-                        // Compute the run's vertical metrics
-                        line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent);
-                        line.metrics.descent = line.metrics.descent.max(run.metrics.descent);
-                        line.metrics.leading = line.metrics.leading.max(run.metrics.leading);
+                        // Compute the bounds of items whose baseline values are not line-relative
+                        run.unique_styles(&self.layout.data, |style| {
+                            min_line_height = min_line_height.max(style.line_height);
+                            let (mut style_top, mut style_bottom) = match style.vertical_align {
+                                VerticalAlign::Baseline => {
+                                    (-run.metrics.ascent, run.metrics.descent)
+                                }
+                                VerticalAlign::TextBottom => {
+                                    (-run.metrics.ascent - run.metrics.descent, 0.0)
+                                }
+                                VerticalAlign::Middle => {
+                                    let offset = run.metrics.x_height * 0.5;
+                                    (-run.metrics.ascent + offset, run.metrics.descent + offset)
+                                }
+                                VerticalAlign::TextTop => {
+                                    (0.0, run.metrics.ascent + run.metrics.descent)
+                                }
+                            };
+                            let offset = match style.baseline_shift {
+                                ResolvedBaselineShift::Absolute(value) => -value,
+                                ResolvedBaselineShift::Top
+                                | ResolvedBaselineShift::Center
+                                | ResolvedBaselineShift::Bottom => {
+                                    line_relative_alignment_needed = true;
+                                    return;
+                                }
+                            };
+                            style_top += offset;
+                            style_bottom += offset;
+                            top = top.min(style_top);
+                            ascent = ascent.min(style_top);
+                            bottom = bottom.max(style_bottom);
+                            descent = descent.max(style_bottom);
+                        });
+                    }
+                }
+            }
 
-                        // Mark us as having seen non-whitespace content on this line
-                        have_metrics = true;
+            if line_relative_alignment_needed {
+                // Align the line-relative items to the previously-computed bounds
+                let bounding_top = top;
+                let bounding_bottom = bottom;
+
+                for line_item in self.lines.line_items[line.item_range.clone()]
+                    .iter_mut()
+                    .rev()
+                {
+                    match line_item.kind {
+                        LayoutItemKind::TextRun => {
+                            let run = &self.layout.data.runs[line_item.index];
+                            let height = run.metrics.ascent + run.metrics.descent;
+                            run.unique_styles(&self.layout.data, |style| {
+                                match style.baseline_shift {
+                                    ResolvedBaselineShift::Absolute(_) => return,
+                                    ResolvedBaselineShift::Top => {
+                                        bottom = bottom.max(bounding_top + height);
+                                        ascent = ascent.min(bounding_top);
+                                        descent = descent.max(bounding_top + height);
+                                    }
+                                    ResolvedBaselineShift::Center => {
+                                        let center = (bounding_top + bounding_bottom) * 0.5;
+                                        top = top.min(center - (height * 0.5));
+                                        ascent = ascent.min(center - (height * 0.5));
+                                        descent = descent.max(center + (height * 0.5));
+                                    }
+                                    ResolvedBaselineShift::Bottom => {
+                                        top = top.min(bounding_bottom - height);
+                                        descent = descent.max(bounding_bottom);
+                                        ascent = ascent.min(bounding_bottom - height);
+                                    }
+                                }
+                            });
+                        }
+                        LayoutItemKind::InlineBox => {
+                            let item = &self.layout.data.inline_boxes[line_item.index];
+                            match item.baseline_shift {
+                                ResolvedBaselineShift::Absolute(_) => return,
+                                ResolvedBaselineShift::Top => {
+                                    bottom = bottom.max(bounding_top + item.height);
+                                }
+                                ResolvedBaselineShift::Center => {
+                                    let center = (bounding_top + bounding_bottom) * 0.5;
+                                    top = top.min(center - (item.height * 0.5));
+                                    bottom = bottom.max(center + (item.height * 0.5));
+                                }
+                                ResolvedBaselineShift::Bottom => {
+                                    top = top.min(bounding_bottom - item.height);
+                                }
+                            }
+                        }
                     }
                 }
             }
@@ -543,7 +655,30 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                 })
                 .unwrap_or(0.0);
 
-            if !have_metrics {
+            let have_metrics = top.is_finite();
+            if have_metrics {
+                if bottom - top < min_line_height {
+                    let half_leading = (min_line_height - (bottom - top)) * 0.5;
+                    top -= half_leading;
+                    bottom += half_leading;
+                }
+
+                line.metrics.line_height = bottom - top;
+
+                let baseline_offset = -top;
+                line.metrics.ascent = -ascent - baseline_offset;
+                line.metrics.descent = descent - baseline_offset;
+
+                line.metrics.min_coord = y.round();
+                line.metrics.baseline = (y + baseline_offset).round();
+                y += line.metrics.line_height;
+                line.metrics.max_coord = y.round();
+
+                // Round block/vertical axis metrics
+                line.metrics.ascent = line.metrics.ascent.round();
+                line.metrics.descent = line.metrics.descent.round();
+                line.metrics.line_height = line.metrics.line_height.round();
+            } else {
                 // Line consisting entirely of whitespace?
                 if !line.item_range.is_empty() {
                     let line_item = &self.lines.line_items[line.item_range.start];
@@ -586,23 +721,19 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                         line.item_range = run_index..run_index + 1;
                     }
                 }
-            }
 
-            // Round block/vertical axis metrics
-            line.metrics.ascent = line.metrics.ascent.round();
-            line.metrics.descent = line.metrics.descent.round();
-            line.metrics.line_height = line.metrics.line_height.round();
-            line.metrics.leading =
+                line.metrics.leading =
                 line.metrics.line_height - (line.metrics.ascent + line.metrics.descent);
 
-            // Compute
-            let above = (line.metrics.ascent + line.metrics.leading * 0.5).round();
-            let below = (line.metrics.descent + line.metrics.leading * 0.5).round();
-            line.metrics.min_coord = y;
-            line.metrics.baseline = y + above;
-            y = line.metrics.baseline + below;
-            line.metrics.max_coord = y;
-            prev_line_metrics = Some(line.metrics);
+                // Compute
+                let above = (line.metrics.ascent + line.metrics.leading * 0.5).round();
+                let below = (line.metrics.descent + line.metrics.leading * 0.5).round();
+                line.metrics.min_coord = y;
+                line.metrics.baseline = y + above;
+                y = line.metrics.baseline + below;
+                line.metrics.max_coord = y;
+                prev_line_metrics = Some(line.metrics);
+            }
         }
         if self.layout.data.text_len == 0 {
             if let Some(line) = self.lines.line_items.first_mut() {
diff --git a/parley/src/layout/line/mod.rs b/parley/src/layout/line/mod.rs
index 5c7ea96f..ac03be8d 100644
--- a/parley/src/layout/line/mod.rs
+++ b/parley/src/layout/line/mod.rs
@@ -156,6 +156,7 @@ pub struct GlyphRun<'a, B: Brush> {
     glyph_count: usize,
     offset: f32,
     baseline: f32,
+    min_coord: f32,
     advance: f32,
 }
 
@@ -185,6 +186,11 @@ impl<'a, B: Brush> GlyphRun<'a, B> {
         self.advance
     }
 
+    pub fn y_offset(&self) -> f32 {
+        let line_ascent = self.baseline - self.min_coord;
+        self.run.metrics().ascent - line_ascent
+    }
+
     /// Returns an iterator over the glyphs in the run.
     pub fn glyphs(&'a self) -> impl Iterator<Item = Glyph> + 'a + Clone {
         self.run
@@ -266,6 +272,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
                             glyph_count,
                             offset: offset + self.line.data.metrics.offset,
                             baseline: self.line.data.metrics.baseline,
+                            min_coord: self.line.data.metrics.min_coord,
                             advance,
                         }));
                     }
diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs
index 7ab63316..c4985145 100644
--- a/parley/src/layout/mod.rs
+++ b/parley/src/layout/mod.rs
@@ -16,7 +16,7 @@ pub mod editor;
 use self::alignment::align;
 
 use super::style::Brush;
-use crate::{Font, InlineBox};
+use crate::{Font, InlineBox, ResolvedBaselineShift, VerticalAlign};
 #[cfg(feature = "accesskit")]
 use accesskit::{Node, NodeId, Role, TextDirection, TreeUpdate};
 use alignment::unjustify;
@@ -301,6 +301,10 @@ pub struct Style<B: Brush> {
     pub strikethrough: Option<Decoration<B>>,
     /// Absolute line height in layout units (style line height * font size)
     pub(crate) line_height: f32,
+    /// The baseline along which this item is aligned.
+    pub(crate) vertical_align: VerticalAlign,
+    /// Additional baseline alignment applied afterwards.
+    pub(crate) baseline_shift: ResolvedBaselineShift,
 }
 
 /// Underline or strikethrough decoration.
diff --git a/parley/src/layout/run.rs b/parley/src/layout/run.rs
index 601f6634..8fdd6d0f 100644
--- a/parley/src/layout/run.rs
+++ b/parley/src/layout/run.rs
@@ -2,8 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
 use super::{
-    Brush, Cluster, ClusterPath, Font, Layout, LineItemData, NormalizedCoord, Range, Run, RunData,
-    Synthesis,
+    Brush, Cluster, ClusterPath, Font, Layout, LineItemData, NormalizedCoord, Range, Run, RunData, Style, Synthesis
 };
 
 impl<'a, B: Brush> Run<'a, B> {
@@ -210,6 +209,11 @@ pub struct RunMetrics {
     pub descent: f32,
     /// Typographic leading.
     pub leading: f32,
+    /// Typographic x-height.
+    pub x_height: f32,
+    // TODO: this is necessary for subscript/superscript layout, but ideally it could be removed
+    /// The run's font size.
+    pub(crate) font_size: f32,
     /// Offset of the top of underline decoration from the baseline.
     pub underline_offset: f32,
     /// Thickness of the underline decoration.
diff --git a/parley/src/resolve/mod.rs b/parley/src/resolve/mod.rs
index 4436b2e8..c52b0b22 100644
--- a/parley/src/resolve/mod.rs
+++ b/parley/src/resolve/mod.rs
@@ -516,6 +516,8 @@ impl<B: Brush> ResolvedStyle<B> {
             underline: self.underline.as_layout_decoration(&self.brush),
             strikethrough: self.strikethrough.as_layout_decoration(&self.brush),
             line_height: self.line_height * self.font_size,
+            vertical_align: self.vertical_align,
+            baseline_shift: self.baseline_shift.resolve(self.font_size, self.line_height),
         }
     }
 }
diff --git a/parley/src/style/mod.rs b/parley/src/style/mod.rs
index 54ea096f..e6031d10 100644
--- a/parley/src/style/mod.rs
+++ b/parley/src/style/mod.rs
@@ -35,16 +35,44 @@ pub enum VerticalAlign {
 pub enum BaselineShift {
     Absolute(f32),
     Relative(f32),
-    Sub,
-    Super,
     Top,
     Center,
     Bottom,
+    // Sub,
+    // Super,
 }
 
 impl Default for BaselineShift {
     fn default() -> Self {
-        BaselineShift::Absolute(0.0)
+        Self::Absolute(0.0)
+    }
+}
+
+impl BaselineShift {
+    pub(crate) fn resolve(&self, font_size: f32, _line_height: f32) -> ResolvedBaselineShift {
+        match self {
+            Self::Absolute(value) => ResolvedBaselineShift::Absolute(*value),
+            Self::Relative(value) => ResolvedBaselineShift::Absolute(*value * font_size),
+            Self::Top => ResolvedBaselineShift::Top,
+            Self::Center => ResolvedBaselineShift::Center,
+            Self::Bottom => ResolvedBaselineShift::Bottom,
+        }
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum ResolvedBaselineShift {
+    Absolute(f32),
+    Top,
+    Center,
+    Bottom,
+    // Sub,
+    // Super,
+}
+
+impl Default for ResolvedBaselineShift {
+    fn default() -> Self {
+        Self::Absolute(0.0)
     }
 }
 

From 13b6354309e6772ba6202ab39c9f0868084de3b7 Mon Sep 17 00:00:00 2001
From: valadaptive <valadaptive@protonmail.com>
Date: Thu, 6 Mar 2025 21:52:31 -0500
Subject: [PATCH 4/4] Format for 2024 edition

---
 parley/src/layout/data.rs        | 6 +++++-
 parley/src/layout/line/greedy.rs | 2 +-
 parley/src/layout/run.rs         | 3 ++-
 parley/src/resolve/mod.rs        | 4 +++-
 4 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs
index f4f07d7e..b6ce65c3 100644
--- a/parley/src/layout/data.rs
+++ b/parley/src/layout/data.rs
@@ -87,7 +87,11 @@ pub(crate) struct RunData {
 }
 
 impl RunData {
-    pub(crate) fn unique_styles<'a, 'b, B: Brush>(&'a self, layout: &'b LayoutData<B>, mut cb: impl FnMut(&'b Style<B>)) {
+    pub(crate) fn unique_styles<'a, 'b, B: Brush>(
+        &'a self,
+        layout: &'b LayoutData<B>,
+        mut cb: impl FnMut(&'b Style<B>),
+    ) {
         let glyph_start = self.glyph_start;
         for cluster in &layout.clusters[self.cluster_range.clone()] {
             if cluster.glyph_len != 0xFF && cluster.has_divergent_styles() {
diff --git a/parley/src/layout/line/greedy.rs b/parley/src/layout/line/greedy.rs
index 1c13fa76..5def4e83 100644
--- a/parley/src/layout/line/greedy.rs
+++ b/parley/src/layout/line/greedy.rs
@@ -723,7 +723,7 @@ impl<'a, B: Brush> BreakLines<'a, B> {
                 }
 
                 line.metrics.leading =
-                line.metrics.line_height - (line.metrics.ascent + line.metrics.descent);
+                    line.metrics.line_height - (line.metrics.ascent + line.metrics.descent);
 
                 // Compute
                 let above = (line.metrics.ascent + line.metrics.leading * 0.5).round();
diff --git a/parley/src/layout/run.rs b/parley/src/layout/run.rs
index 8fdd6d0f..f806216d 100644
--- a/parley/src/layout/run.rs
+++ b/parley/src/layout/run.rs
@@ -2,7 +2,8 @@
 // SPDX-License-Identifier: Apache-2.0 OR MIT
 
 use super::{
-    Brush, Cluster, ClusterPath, Font, Layout, LineItemData, NormalizedCoord, Range, Run, RunData, Style, Synthesis
+    Brush, Cluster, ClusterPath, Font, Layout, LineItemData, NormalizedCoord, Range, Run, RunData,
+    Style, Synthesis,
 };
 
 impl<'a, B: Brush> Run<'a, B> {
diff --git a/parley/src/resolve/mod.rs b/parley/src/resolve/mod.rs
index c52b0b22..21f56d5e 100644
--- a/parley/src/resolve/mod.rs
+++ b/parley/src/resolve/mod.rs
@@ -517,7 +517,9 @@ impl<B: Brush> ResolvedStyle<B> {
             strikethrough: self.strikethrough.as_layout_decoration(&self.brush),
             line_height: self.line_height * self.font_size,
             vertical_align: self.vertical_align,
-            baseline_shift: self.baseline_shift.resolve(self.font_size, self.line_height),
+            baseline_shift: self
+                .baseline_shift
+                .resolve(self.font_size, self.line_height),
         }
     }
 }