cranpose_ui/text/
line_box.rs1use crate::text::style::{
28 LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, TextStyle,
29};
30
31#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct LineBox {
36 pub height: f32,
39 pub baseline: f32,
41 pub trim_top: f32,
43 pub trim_bottom: f32,
45}
46
47impl LineBox {
48 pub fn untrimmed(height: f32, baseline: f32) -> Self {
50 Self {
51 height,
52 baseline,
53 trim_top: 0.0,
54 trim_bottom: 0.0,
55 }
56 }
57
58 pub fn block_height(self, lines: usize) -> f32 {
61 (self.height * lines.max(1) as f32 - self.trim_top - self.trim_bottom).max(1.0)
62 }
63
64 pub fn first_baseline(self) -> f32 {
66 self.baseline - self.trim_top
67 }
68
69 pub fn line_top(self, index: usize) -> f32 {
71 index as f32 * self.height - self.trim_top
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct FontExtent {
82 pub ascent: f32,
83 pub descent: f32,
84 pub line_gap: f32,
86}
87
88impl FontExtent {
89 pub fn new(ascent: f32, descent: f32, line_gap: f32) -> Self {
90 Self {
91 ascent,
92 descent,
93 line_gap,
94 }
95 }
96
97 pub fn natural(self) -> f32 {
99 self.ascent + self.descent
100 }
101}
102
103pub fn line_box(style: &TextStyle, extent: FontExtent, asked: f32, grid: f32) -> LineBox {
114 let grid = if grid.is_finite() && grid > 0.0 {
115 grid
116 } else {
117 1.0
118 };
119 let asked = if style.paragraph_style.line_height.is_unspecified() {
120 f32::NAN
121 } else {
122 asked
123 };
124 let padding = font_padding(style, extent);
125 let line_height_style = match style.paragraph_style.line_height_style {
126 Some(line_height_style) => line_height_style,
127 None if padding > 0.0 || font_padding_asked(style) => LineHeightStyle {
128 alignment: LineHeightAlignment::Proportional,
129 trim: LineHeightTrim::None,
130 mode: LineHeightMode::Fixed,
131 },
132 None => LineHeightStyle::default(),
133 };
134 aosp_line_box(line_height_style, extent, asked, padding, grid)
135}
136
137fn font_padding_asked(style: &TextStyle) -> bool {
138 style
139 .paragraph_style
140 .platform_style
141 .and_then(|platform| platform.include_font_padding)
142 .unwrap_or(false)
143}
144
145fn font_padding(style: &TextStyle, extent: FontExtent) -> f32 {
146 if font_padding_asked(style) && extent.line_gap.is_finite() && extent.line_gap > 0.0 {
147 extent.line_gap
148 } else {
149 0.0
150 }
151}
152
153fn aosp_line_box(
154 style: LineHeightStyle,
155 extent: FontExtent,
156 asked: f32,
157 padding: f32,
158 grid: f32,
159) -> LineBox {
160 let up = |value: f32| (value * grid).ceil() / grid;
161 let down = |value: f32| (value * grid).floor() / grid;
162 let round = |value: f32| ((value * grid) + 0.5).floor() / grid;
163 let ascent = -round(-extent.ascent.max(0.0));
164 let descent = round(extent.descent.max(0.0));
165 let above_padding = down(padding * 0.5);
166 let below_padding = padding - above_padding;
167 let natural = up(ascent + descent + padding);
168 let asked = if asked.is_finite() {
169 up(asked)
170 } else {
171 natural
172 };
173
174 let height = match style.mode {
175 LineHeightMode::Fixed => asked.max(1.0),
176 LineHeightMode::Minimum => asked.max(natural).max(1.0),
177 LineHeightMode::Tight => natural.max(1.0),
178 };
179
180 let leading = height - (ascent + descent + padding);
181 let (mut above, mut below) = match style.alignment {
182 LineHeightAlignment::Top => (0.0, leading),
183 LineHeightAlignment::Bottom => (leading, 0.0),
184 LineHeightAlignment::Center => {
185 let below = up(leading * 0.5);
186 (leading - below, below)
187 }
188 LineHeightAlignment::Proportional => {
189 let total = ascent + descent;
190 if total > 0.0 {
191 let above = leading * (ascent / total);
192 (above, leading - above)
193 } else {
194 (leading * 0.5, leading * 0.5)
195 }
196 }
197 };
198 above += above_padding;
199 below += below_padding;
200
201 let (trim_above, trim_below) = match style.trim {
202 LineHeightTrim::None => (false, false),
203 LineHeightTrim::FirstLineTop => (true, false),
204 LineHeightTrim::LastLineBottom => (false, true),
205 LineHeightTrim::Both => (true, true),
206 };
207
208 LineBox {
209 height: height.max(1.0),
210 baseline: above + ascent,
211 trim_top: if trim_above { above } else { 0.0 },
212 trim_bottom: if trim_below { below } else { 0.0 },
213 }
214}
215
216#[cfg(test)]
217#[path = "tests/line_box_tests.rs"]
218mod tests;