use crate::text::style::{
LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, TextStyle,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LineBox {
pub height: f32,
pub baseline: f32,
pub trim_top: f32,
pub trim_bottom: f32,
}
impl LineBox {
pub fn untrimmed(height: f32, baseline: f32) -> Self {
Self {
height,
baseline,
trim_top: 0.0,
trim_bottom: 0.0,
}
}
pub fn block_height(self, lines: usize) -> f32 {
(self.height * lines.max(1) as f32 - self.trim_top - self.trim_bottom).max(1.0)
}
pub fn first_baseline(self) -> f32 {
self.baseline - self.trim_top
}
pub fn line_top(self, index: usize) -> f32 {
index as f32 * self.height - self.trim_top
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FontExtent {
pub ascent: f32,
pub descent: f32,
pub line_gap: f32,
}
impl FontExtent {
pub fn new(ascent: f32, descent: f32, line_gap: f32) -> Self {
Self {
ascent,
descent,
line_gap,
}
}
pub fn natural(self) -> f32 {
self.ascent + self.descent
}
}
pub fn line_box(style: &TextStyle, extent: FontExtent, asked: f32, grid: f32) -> LineBox {
let grid = if grid.is_finite() && grid > 0.0 {
grid
} else {
1.0
};
let asked = if style.paragraph_style.line_height.is_unspecified() {
f32::NAN
} else {
asked
};
let padding = font_padding(style, extent);
let line_height_style = match style.paragraph_style.line_height_style {
Some(line_height_style) => line_height_style,
None if padding > 0.0 || font_padding_asked(style) => LineHeightStyle {
alignment: LineHeightAlignment::Proportional,
trim: LineHeightTrim::None,
mode: LineHeightMode::Fixed,
},
None => LineHeightStyle::default(),
};
aosp_line_box(line_height_style, extent, asked, padding, grid)
}
fn font_padding_asked(style: &TextStyle) -> bool {
style
.paragraph_style
.platform_style
.and_then(|platform| platform.include_font_padding)
.unwrap_or(false)
}
fn font_padding(style: &TextStyle, extent: FontExtent) -> f32 {
if font_padding_asked(style) && extent.line_gap.is_finite() && extent.line_gap > 0.0 {
extent.line_gap
} else {
0.0
}
}
fn aosp_line_box(
style: LineHeightStyle,
extent: FontExtent,
asked: f32,
padding: f32,
grid: f32,
) -> LineBox {
let up = |value: f32| (value * grid).ceil() / grid;
let down = |value: f32| (value * grid).floor() / grid;
let round = |value: f32| ((value * grid) + 0.5).floor() / grid;
let ascent = -round(-extent.ascent.max(0.0));
let descent = round(extent.descent.max(0.0));
let above_padding = down(padding * 0.5);
let below_padding = padding - above_padding;
let natural = up(ascent + descent + padding);
let asked = if asked.is_finite() {
up(asked)
} else {
natural
};
let height = match style.mode {
LineHeightMode::Fixed => asked.max(1.0),
LineHeightMode::Minimum => asked.max(natural).max(1.0),
LineHeightMode::Tight => natural.max(1.0),
};
let leading = height - (ascent + descent + padding);
let (mut above, mut below) = match style.alignment {
LineHeightAlignment::Top => (0.0, leading),
LineHeightAlignment::Bottom => (leading, 0.0),
LineHeightAlignment::Center => {
let below = up(leading * 0.5);
(leading - below, below)
}
LineHeightAlignment::Proportional => {
let total = ascent + descent;
if total > 0.0 {
let above = leading * (ascent / total);
(above, leading - above)
} else {
(leading * 0.5, leading * 0.5)
}
}
};
above += above_padding;
below += below_padding;
let (trim_above, trim_below) = match style.trim {
LineHeightTrim::None => (false, false),
LineHeightTrim::FirstLineTop => (true, false),
LineHeightTrim::LastLineBottom => (false, true),
LineHeightTrim::Both => (true, true),
};
LineBox {
height: height.max(1.0),
baseline: above + ascent,
trim_top: if trim_above { above } else { 0.0 },
trim_bottom: if trim_below { below } else { 0.0 },
}
}
#[cfg(test)]
#[path = "tests/line_box_tests.rs"]
mod tests;