line_straddler/lib.rs
1// SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0
2// This file is a part of `line-straddler`.
3//
4// `line-straddler` is free software: you can redistribute it and/or modify it under the
5// terms of either:
6//
7// * GNU Lesser General Public License as published by the Free Software Foundation, either
8// version 3 of the License, or (at your option) any later version.
9// * Mozilla Public License as published by the Mozilla Foundation, version 2.
10//
11// `line-straddler` is distributed in the hope that it will be useful, but WITHOUT ANY
12// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
13// PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more
14// details.
15//
16// You should have received a copy of the GNU Lesser General Public License and the Mozilla
17// Public License along with `line-straddler`. If not, see <https://www.gnu.org/licenses/>.
18
19//! Figure out where lines should go when underlining/striking through text.
20//!
21//! When you're drawing text, you need to determine where the lines go for text decorations. This crate provides a renderer-agnostic `LineGenerator` that generates `Line` structures for a set of `Glyph`s.
22//!
23//! ## Example
24//!
25//! ```rust
26//! use line_straddler::{LineGenerator, Line, LineType, Glyph, GlyphStyle, Color};
27//!
28//! # fn draw_line(_point_1: (f32, f32), _point_2: (f32, f32), _style: GlyphStyle) {}
29//! // Take some glyphs from, e.g, cosmic-text
30//! // For instance, this is two lines of two glyphs.
31//! let style = GlyphStyle {
32//! boldness: 100,
33//! color: Color::rgba(0, 0, 0, 255),
34//! };
35//! let glyphs = [
36//! Glyph {
37//! line_y: 0.0,
38//! font_size: 4.0,
39//! width: 2.0,
40//! x: 0.0,
41//! style,
42//! },
43//! Glyph {
44//! line_y: 0.0,
45//! font_size: 4.0,
46//! width: 2.0,
47//! x: 3.0,
48//! style,
49//! },
50//! Glyph {
51//! line_y: 5.0,
52//! font_size: 4.0,
53//! width: 2.0,
54//! x: 0.0,
55//! style,
56//! },
57//! Glyph {
58//! line_y: 5.0,
59//! font_size: 4.0,
60//! width: 2.0,
61//! x: 3.0,
62//! style,
63//! },
64//! ];
65//!
66//! // Create a line generator.
67//! let mut alg = LineGenerator::new(LineType::Underline);
68//!
69//! // Generate lines for the glyphs.
70//! let mut lines = Vec::new();
71//! for glyph in glyphs {
72//! lines.extend(alg.add_glyph(glyph));
73//! }
74//! lines.extend(alg.pop_line());
75//!
76//! // Draw all of the lines.
77//! for line in lines {
78//! let point_1 = (line.start_x, line.y);
79//! let point_2 = (line.end_x, line.y);
80//! draw_line(point_1, point_2, line.style);
81//! }
82//! ```
83
84#![forbid(unsafe_code, future_incompatible, rust_2018_idioms)]
85#![cfg_attr(not(feature = "std"), no_std)]
86
87/// A glyph to be rendered.
88///
89/// This corresponds to the [`LayoutGlyph`] type in [`cosmic-text`] and similar types in other text
90/// renderers. Glyphs should be converted to this type before being passed to the line generator.
91///
92/// [`LayoutGlyph`]: https://docs.rs/cosmic-text/latest/cosmic_text/struct.LayoutGlyph.html
93/// [`cosmic-text`]: https://crates.io/crates/cosmic-text
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Glyph {
96 /// The y coordinate of the glyph's line.
97 pub line_y: f32,
98
99 /// The font size of the glyph in pixels.
100 pub font_size: f32,
101
102 /// The width of the glyph's bounding box.
103 pub width: f32,
104
105 /// The X coordinate of the glyph's bounding box.
106 pub x: f32,
107
108 /// The style of the glyph.
109 pub style: GlyphStyle,
110}
111
112/// Glyph styling information.
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct GlyphStyle {
115 /// The weight of the glyph.
116 pub boldness: u16,
117
118 /// The color of the glyph.
119 pub color: Color,
120}
121
122/// 32-bit RGBA color.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
124pub struct Color(u32);
125
126impl Color {
127 /// Create a new color from the given RGBA values.
128 #[inline]
129 pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
130 Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
131 }
132
133 /// Get the red component of the color.
134 #[inline]
135 pub fn red(self) -> u8 {
136 ((self.0 >> 24) & 0xFF) as u8
137 }
138
139 /// Get the green component of the color.
140 #[inline]
141 pub fn green(self) -> u8 {
142 ((self.0 >> 16) & 0xFF) as u8
143 }
144
145 /// Get the blue component of the color.
146 #[inline]
147 pub fn blue(self) -> u8 {
148 ((self.0 >> 8) & 0xFF) as u8
149 }
150
151 /// Get the alpha component of the color.
152 #[inline]
153 pub fn alpha(self) -> u8 {
154 (self.0 & 0xFF) as u8
155 }
156
157 /// Get an array of the components.
158 #[inline]
159 pub fn components(self) -> [u8; 4] {
160 [self.red(), self.green(), self.blue(), self.alpha()]
161 }
162}
163
164/// The horizontal line that needs to be rendered.
165#[derive(Debug, Clone, Copy, PartialEq)]
166#[non_exhaustive]
167pub struct Line {
168 /// The Y coordinate of the line.
169 pub y: f32,
170
171 /// The X coordinate of the line's start.
172 pub start_x: f32,
173
174 /// The X coordinate of the line's end.
175 pub end_x: f32,
176
177 /// The style of the line.
178 pub style: GlyphStyle,
179}
180
181/// What kind of lind are we trying to produce?
182#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
183#[non_exhaustive]
184pub enum LineType {
185 /// This is an overline.
186 Overline,
187
188 /// This is a strike-through.
189 StrikeThrough,
190
191 /// This is an underline.
192 Underline,
193}
194
195impl LineType {
196 /// Get the offset of the line given the font size.
197 fn offset(self, font_size: f32) -> f32 {
198 match self {
199 Self::Overline => 0.0,
200 Self::StrikeThrough => font_size / 2.0,
201 Self::Underline => font_size,
202 }
203 }
204}
205
206/// The generator for lines.
207#[derive(Debug)]
208pub struct LineGenerator {
209 /// The line we are currently creating, if any.
210 ongoing_line: Option<OngoingLine>,
211
212 /// The type of line we are currently creating.
213 line_type: LineType,
214}
215
216impl LineGenerator {
217 /// Create a new, empty line generator.
218 #[inline]
219 pub fn new(ty: LineType) -> Self {
220 Self {
221 ongoing_line: None,
222 line_type: ty,
223 }
224 }
225
226 /// Pop the current line out of the generator.
227 #[inline]
228 pub fn pop_line(&mut self) -> Option<Line> {
229 self.ongoing_line.take().map(Into::into)
230 }
231
232 /// Add a new glyph to the generator.
233 ///
234 /// Returns a new line if one was created.
235 #[inline]
236 pub fn add_glyph(&mut self, glyph: impl Into<Glyph>) -> Option<Line> {
237 self.add_glyph_impl(glyph.into())
238 }
239
240 #[inline]
241 fn add_glyph_impl(&mut self, glyph: Glyph) -> Option<Line> {
242 // See if we need to start a new line.
243 if let Some(line) = self.ongoing_line.as_mut() {
244 if approx_eq(line.last_line_y, glyph.line_y)
245 && line.end_x <= glyph.x
246 && approx_eq(line.font_size, glyph.font_size)
247 && line.style == glyph.style
248 {
249 // Just extend the current line.
250 line.end_x = glyph.x + glyph.width;
251 return None;
252 }
253 }
254
255 // Just start a new line.
256 let mut old_line = self.ongoing_line.replace(OngoingLine {
257 y: glyph.line_y + self.line_type.offset(glyph.font_size),
258 last_line_y: glyph.line_y,
259 start_x: glyph.x,
260 end_x: glyph.x + glyph.width,
261 style: glyph.style,
262 font_size: glyph.font_size,
263 });
264
265 // Make sure the old line ends where the new glyph begins if it's on the same line.
266 if let Some(old_line) = old_line.as_mut() {
267 if approx_eq(old_line.last_line_y, glyph.line_y) {
268 old_line.end_x = glyph.x;
269 }
270 }
271
272 old_line.map(Into::into)
273 }
274}
275
276#[derive(Debug)]
277struct OngoingLine {
278 /// The Y coordinate of the line.
279 y: f32,
280
281 /// The X coordinate of the line's start.
282 start_x: f32,
283
284 /// The current X coordinate of the line's end.
285 end_x: f32,
286
287 /// The style of the line so far.
288 style: GlyphStyle,
289
290 /// The line y of the last glyph we observed.
291 last_line_y: f32,
292
293 /// The font size we last observed.
294 font_size: f32,
295}
296
297impl From<OngoingLine> for Line {
298 fn from(line: OngoingLine) -> Self {
299 Self {
300 y: line.y,
301 start_x: line.start_x,
302 end_x: line.end_x,
303 style: line.style,
304 }
305 }
306}
307
308/// Tell if two floats are approximately equal.
309fn approx_eq(a: f32, b: f32) -> bool {
310 abs(a - b) < EPSILON
311}
312
313macro_rules! float_switch {
314 ($i:ident => [$std:expr] [$libm:expr]) => {{
315 #[cfg(feature = "std")]
316 {
317 $std
318 }
319
320 #[cfg(all(not(feature = "std"), feature = "libm"))]
321 {
322 $libm
323 }
324
325 #[cfg(all(not(feature = "std"), not(feature = "libm")))]
326 {
327 compile_error!("Either the `std` or `libm` feature must be enabled");
328 }
329 }};
330}
331
332/// Absolute value of a float.
333fn abs(a: f32) -> f32 {
334 float_switch!(
335 a => [a.abs()] [libm::fabsf(a)]
336 )
337}
338
339const EPSILON: f32 = 0.001;