use std::fmt;
use cssparser::{Parser, match_ignore_ascii_case};
use parley::LineMetrics;
use crate::{
layout::style::{ToCss, tw::TailwindPropertyParser, *},
rendering::Sizing,
};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum VerticalAlignKeyword {
#[default]
Baseline,
Top,
Middle,
Bottom,
TextTop,
TextBottom,
Sub,
Super,
}
declare_enum_from_css_impl!(
VerticalAlignKeyword,
"baseline" => VerticalAlignKeyword::Baseline,
"top" => VerticalAlignKeyword::Top,
"middle" => VerticalAlignKeyword::Middle,
"bottom" => VerticalAlignKeyword::Bottom,
"text-top" => VerticalAlignKeyword::TextTop,
"text-bottom" => VerticalAlignKeyword::TextBottom,
"sub" => VerticalAlignKeyword::Sub,
"super" => VerticalAlignKeyword::Super
);
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum VerticalAlign {
Keyword(VerticalAlignKeyword),
Length(Length),
}
impl ToCss for VerticalAlign {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Self::Keyword(kw) => kw.to_css(dest),
Self::Length(l) => l.to_css(dest),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ResolvedVerticalAlign {
Keyword(VerticalAlignKeyword),
BaselineShift {
px: f32,
line_height_relative: f32,
},
}
impl Default for VerticalAlign {
fn default() -> Self {
Self::Keyword(VerticalAlignKeyword::default())
}
}
impl Default for ResolvedVerticalAlign {
fn default() -> Self {
Self::Keyword(VerticalAlignKeyword::default())
}
}
impl<'i> FromCss<'i> for VerticalAlign {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
if let Ok(keyword) = input.try_parse(VerticalAlignKeyword::from_css) {
return Ok(Self::Keyword(keyword));
}
Ok(Self::Length(Length::from_css(input)?))
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Keyword("baseline"),
CssToken::Keyword("top"),
CssToken::Keyword("middle"),
CssToken::Keyword("bottom"),
CssToken::Keyword("text-top"),
CssToken::Keyword("text-bottom"),
CssToken::Keyword("sub"),
CssToken::Keyword("super"),
CssToken::Syntax(CssSyntaxKind::Length),
];
}
impl VerticalAlign {
pub(crate) fn resolve(
self,
sizing: &Sizing,
font_size: f32,
line_height: LineHeight,
) -> ResolvedVerticalAlign {
match self {
Self::Keyword(keyword) => ResolvedVerticalAlign::Keyword(keyword),
Self::Length(length) => {
if line_height == LineHeight::Normal {
if let Length::Percentage(value) = length {
return ResolvedVerticalAlign::BaselineShift {
px: 0.0,
line_height_relative: value / 100.0,
};
}
if let Length::Calc(formula) = length {
let linear = formula.resolve(sizing);
let (px, percent) = linear.components();
return ResolvedVerticalAlign::BaselineShift {
px,
line_height_relative: percent,
};
}
}
let shift = match line_height {
LineHeight::Normal => length.to_px(sizing, 0.0),
LineHeight::Unitless(value) => length.to_px(sizing, value * font_size),
LineHeight::Length(value) => length.to_px(sizing, value.to_px(sizing, font_size)),
};
ResolvedVerticalAlign::BaselineShift {
px: shift,
line_height_relative: 0.0,
}
}
}
}
}
impl MakeComputed for VerticalAlign {
fn make_computed(&mut self, sizing: &Sizing) {
if let Self::Length(length) = self {
length.make_computed(sizing);
}
}
}
impl ResolvedVerticalAlign {
pub(crate) fn apply(
self,
y: &mut f32,
metrics: &LineMetrics,
box_height: f32,
baseline_offset: Option<f32>,
parent_x_height: Option<f32>,
parent_text_metrics: Option<(f32, f32)>,
) {
let baseline_offset = baseline_offset.unwrap_or(box_height).clamp(0.0, box_height);
let baseline_top = metrics.baseline - baseline_offset;
let (parent_text_ascent, parent_text_descent) =
parent_text_metrics.unwrap_or((metrics.ascent, metrics.descent));
match self {
ResolvedVerticalAlign::Keyword(keyword) => match keyword {
VerticalAlignKeyword::Baseline => *y = baseline_top,
VerticalAlignKeyword::Top => {
debug_assert!(metrics.block_min_coord.is_finite());
*y = metrics.block_min_coord;
}
VerticalAlignKeyword::Middle => {
let x_height = parent_x_height.unwrap_or(metrics.ascent * 0.5);
*y = metrics.baseline - (x_height * 0.5) - (box_height / 2.0);
}
VerticalAlignKeyword::Bottom => {
debug_assert!(metrics.block_max_coord.is_finite());
*y = metrics.block_max_coord - box_height;
}
VerticalAlignKeyword::TextTop => *y = metrics.baseline - parent_text_ascent,
VerticalAlignKeyword::TextBottom => {
*y = metrics.baseline + parent_text_descent - box_height
}
VerticalAlignKeyword::Sub => *y = baseline_top + (metrics.descent * 0.2),
VerticalAlignKeyword::Super => *y = baseline_top - metrics.ascent + (metrics.ascent * 0.4),
},
ResolvedVerticalAlign::BaselineShift {
px,
line_height_relative,
} => {
let line_height_component =
(metrics.ascent - metrics.descent + metrics.leading) * line_height_relative;
*y = baseline_top - (px + line_height_component);
}
}
}
}
impl TailwindPropertyParser for VerticalAlign {
fn parse_tw(token: &str) -> Option<Self> {
match_ignore_ascii_case! {token,
"baseline" => Some(Self::Keyword(VerticalAlignKeyword::Baseline)),
"top" => Some(Self::Keyword(VerticalAlignKeyword::Top)),
"middle" => Some(Self::Keyword(VerticalAlignKeyword::Middle)),
"bottom" => Some(Self::Keyword(VerticalAlignKeyword::Bottom)),
"text-top" => Some(Self::Keyword(VerticalAlignKeyword::TextTop)),
"text-bottom" => Some(Self::Keyword(VerticalAlignKeyword::TextBottom)),
"sub" => Some(Self::Keyword(VerticalAlignKeyword::Sub)),
"super" => Some(Self::Keyword(VerticalAlignKeyword::Super)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use taffy::Size;
use crate::layout::Viewport;
use super::*;
fn sizing() -> Sizing {
Sizing {
viewport: Viewport {
size: (200, 100).into(),
font_size: 16.0,
device_pixel_ratio: 2.0,
},
container_size: Size::NONE,
font_size: 10.0,
root_font_size: None,
line_height: 0.0,
root_line_height: None,
calc_arena: Rc::new(CalcArena::default()),
}
}
fn line_metrics() -> LineMetrics {
LineMetrics {
ascent: 9.0,
descent: 3.0,
leading: 0.0,
line_height: 14.0,
baseline: 20.0,
offset: 0.0,
advance: 100.0,
trailing_whitespace: 0.0,
inline_min_coord: 0.0,
inline_max_coord: 100.0,
block_min_coord: 10.0,
block_max_coord: 24.0,
}
}
#[test]
fn parse_keywords_and_length_percentage() {
assert_eq!(
VerticalAlign::from_str("baseline"),
Ok(VerticalAlign::Keyword(VerticalAlignKeyword::Baseline))
);
assert_eq!(
VerticalAlign::from_str("10px"),
Ok(VerticalAlign::Length(Length::Px(10.0)))
);
assert_eq!(
VerticalAlign::from_str("25%"),
Ok(VerticalAlign::Length(Length::Percentage(25.0)))
);
assert_eq!(
VerticalAlign::from_str("-0.5em"),
Ok(VerticalAlign::Length(Length::Em(-0.5)))
);
}
#[test]
fn resolve_length_to_baseline_shift_px() {
let resolved =
VerticalAlign::Length(Length::Px(8.0)).resolve(&sizing(), 12.0, LineHeight::Unitless(1.5));
assert_eq!(
resolved,
ResolvedVerticalAlign::BaselineShift {
px: 16.0,
line_height_relative: 0.0
}
);
}
#[test]
fn resolve_percentage_uses_line_height_basis() {
let unitless = VerticalAlign::Length(Length::Percentage(50.0)).resolve(
&sizing(),
12.0,
LineHeight::Unitless(2.0),
);
assert_eq!(
unitless,
ResolvedVerticalAlign::BaselineShift {
px: 12.0,
line_height_relative: 0.0
}
);
let fixed = VerticalAlign::Length(Length::Percentage(50.0)).resolve(
&sizing(),
12.0,
LineHeight::Length(Length::Px(20.0)),
);
assert_eq!(
fixed,
ResolvedVerticalAlign::BaselineShift {
px: 20.0,
line_height_relative: 0.0
}
);
let normal =
VerticalAlign::Length(Length::Percentage(50.0)).resolve(&sizing(), 12.0, LineHeight::Normal);
assert_eq!(
normal,
ResolvedVerticalAlign::BaselineShift {
px: 0.0,
line_height_relative: 0.5
}
);
}
#[test]
fn apply_baseline_shift_raises_and_lowers() {
let metrics = line_metrics();
let baseline = metrics.baseline - 4.0;
let mut y = 0.0;
ResolvedVerticalAlign::BaselineShift {
px: 5.0,
line_height_relative: 0.0,
}
.apply(&mut y, &metrics, 4.0, None, None, None);
assert_eq!(y, baseline - 5.0);
ResolvedVerticalAlign::BaselineShift {
px: -5.0,
line_height_relative: 0.0,
}
.apply(&mut y, &metrics, 4.0, None, None, None);
assert_eq!(y, baseline + 5.0);
}
#[test]
fn apply_keyword_top_uses_block_min_coord() {
let mut y = 0.0;
let metrics = line_metrics();
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Top)
.apply(&mut y, &metrics, 8.0, None, None, None);
assert_eq!(y, metrics.block_min_coord);
}
#[test]
fn apply_keyword_bottom_uses_block_max_coord() {
let mut y = 0.0;
let metrics = line_metrics();
let box_height = 8.0;
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Bottom)
.apply(&mut y, &metrics, box_height, None, None, None);
assert_eq!(y, metrics.block_max_coord - box_height);
}
#[test]
fn apply_metrics_relative_shift_uses_line_metrics_formula() {
let metrics = line_metrics();
let baseline = metrics.baseline - 4.0;
let mut y = 0.0;
ResolvedVerticalAlign::BaselineShift {
px: 0.0,
line_height_relative: 0.5,
}
.apply(&mut y, &metrics, 4.0, None, None, None);
assert_eq!(
y,
baseline - ((metrics.ascent - metrics.descent + metrics.leading) * 0.5)
);
}
#[test]
fn resolve_normal_calc_percentage_uses_metrics_relative_px() {
let Ok(length) = Length::from_str("calc(50% + 4px)") else {
return;
};
let resolved = VerticalAlign::Length(length).resolve(&sizing(), 12.0, LineHeight::Normal);
assert_eq!(
resolved,
ResolvedVerticalAlign::BaselineShift {
px: 8.0,
line_height_relative: 0.5
}
);
}
#[test]
fn apply_metrics_relative_px_shift_uses_line_metrics_formula_plus_px() {
let metrics = line_metrics();
let baseline = metrics.baseline - 4.0;
let mut y = 0.0;
ResolvedVerticalAlign::BaselineShift {
px: 3.0,
line_height_relative: 0.5,
}
.apply(&mut y, &metrics, 4.0, None, None, None);
assert_eq!(
y,
baseline - ((metrics.ascent - metrics.descent + metrics.leading) * 0.5 + 3.0)
);
}
#[test]
fn keyword_apply_matches_previous_baseline_behavior() {
let metrics = line_metrics();
let mut y = 0.0;
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Baseline)
.apply(&mut y, &metrics, 4.0, None, None, None);
assert_eq!(y, metrics.baseline - 4.0);
}
#[test]
fn keyword_apply_uses_inline_box_baseline_when_available() {
let metrics = line_metrics();
let mut y = 0.0;
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Baseline).apply(
&mut y,
&metrics,
20.0,
Some(12.0),
None,
None,
);
assert_eq!(y, metrics.baseline - 12.0);
}
#[test]
fn sub_and_super_use_inline_box_baseline_when_available() {
let metrics = line_metrics();
let mut sub_y = 0.0;
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Sub).apply(
&mut sub_y,
&metrics,
20.0,
Some(12.0),
None,
None,
);
assert_eq!(sub_y, (metrics.baseline - 12.0) + (metrics.descent * 0.2));
let mut super_y = 0.0;
ResolvedVerticalAlign::Keyword(VerticalAlignKeyword::Super).apply(
&mut super_y,
&metrics,
20.0,
Some(12.0),
None,
None,
);
assert_eq!(
super_y,
(metrics.baseline - 12.0) - metrics.ascent + (metrics.ascent * 0.4)
);
}
}