use std::cell::Cell;
use cranpose_ui_graphics::whole;
use crate::style::{
LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, ParagraphStyle, 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 {
paragraph_line_box(&style.paragraph_style, extent, asked, grid)
}
pub fn paragraph_line_box(
style: &ParagraphStyle,
extent: FontExtent,
asked: f32,
grid: f32,
) -> LineBox {
let inputs = LineBoxInputs::of(style, extent, asked, pixel_grid(grid));
inputs.pixels().in_units(inputs.grid)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct LinePixels {
pub(crate) height: f32,
pub(crate) baseline: f32,
pub(crate) trim_top: f32,
pub(crate) trim_bottom: f32,
}
impl LinePixels {
fn in_units(self, grid: f32) -> LineBox {
LineBox {
height: self.height / grid,
baseline: self.baseline / grid,
trim_top: self.trim_top / grid,
trim_bottom: self.trim_bottom / grid,
}
}
}
fn pixel_grid(grid: f32) -> f32 {
if grid.is_finite() && grid > 0.0 {
grid
} else {
1.0
}
}
#[derive(Clone, Copy)]
pub(crate) struct LineBoxInputs {
style: LineHeightStyle,
extent: FontExtent,
asked: f32,
padding: f32,
grid: f32,
}
impl LineBoxInputs {
pub(crate) fn of(style: &ParagraphStyle, extent: FontExtent, asked: f32, grid: f32) -> Self {
let asked = if style.line_height.is_unspecified() {
f32::NAN
} else {
asked
};
let padding = font_padding(style, extent);
let line_height_style = match 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(),
};
Self {
style: line_height_style,
extent,
asked,
padding,
grid,
}
}
#[inline(never)]
fn pixels(self) -> LinePixels {
aosp_line_pixels(self.style, self.extent, self.asked, self.padding, self.grid)
}
fn same(&self, other: &Self) -> bool {
let bits = |inputs: &Self| {
[
inputs.extent.ascent,
inputs.extent.descent,
inputs.extent.line_gap,
inputs.asked,
inputs.padding,
inputs.grid,
]
.map(f32::to_bits)
};
self.style == other.style && bits(self) == bits(other)
}
}
#[derive(Default)]
pub(crate) struct LastLineBox(Cell<Option<(LineBoxInputs, LinePixels)>>);
impl LastLineBox {
pub(crate) fn of(&self, inputs: LineBoxInputs) -> LinePixels {
if let Some((kept, pixels)) = self.0.get()
&& kept.same(&inputs)
{
return pixels;
}
let pixels = inputs.pixels();
self.0.set(Some((inputs, pixels)));
pixels
}
}
fn font_padding_asked(style: &ParagraphStyle) -> bool {
style
.platform_style
.and_then(|platform| platform.include_font_padding)
.unwrap_or(false)
}
fn font_padding(style: &ParagraphStyle, 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_pixels(
style: LineHeightStyle,
extent: FontExtent,
asked: f32,
padding: f32,
grid: f32,
) -> LinePixels {
let ascent = -whole::floor(0.5 - extent.ascent.max(0.0) * grid);
let descent = whole::floor(extent.descent.max(0.0) * grid + 0.5);
let padding = whole::ceil(padding * grid);
let above_padding = whole::floor(padding * 0.5);
let font = ascent + descent;
let natural = font + padding;
let asked = asked * grid;
let asked = if asked.is_finite() {
whole::ceil(asked)
} else {
natural
};
let height = match style.mode {
LineHeightMode::Fixed | LineHeightMode::Tight => asked,
LineHeightMode::Minimum => asked.max(natural),
}
.max(1.0);
let leading = height - natural;
let top_ratio = match style.alignment {
LineHeightAlignment::Top => 0.0,
LineHeightAlignment::Center => 0.5,
LineHeightAlignment::Bottom => 1.0,
LineHeightAlignment::Proportional if font > 0.0 => ascent / font,
LineHeightAlignment::Proportional => 0.5,
};
let below_share = if leading > 0.0 {
1.0 - top_ratio
} else {
top_ratio
};
let below = whole::ceil(leading * below_share);
let above = leading - below + above_padding;
let below = below + padding - above_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),
};
let tight = style.mode == LineHeightMode::Tight && leading < 0.0;
let given_back = |trimmed: bool, share: f32| if trimmed != tight { share } else { 0.0 };
LinePixels {
height,
baseline: above + ascent,
trim_top: given_back(trim_above, above),
trim_bottom: given_back(trim_below, below),
}
}
#[cfg(test)]
#[path = "tests/line_box_tests.rs"]
mod tests;