1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3pub mod atlas;
10pub mod cache;
11pub mod decoration;
12pub mod editor;
13pub mod fallback;
14pub mod highlight;
15pub mod hyperlink;
16pub mod ime;
17pub mod input;
18pub mod label;
19pub mod layout;
20pub mod rich;
21pub mod selection;
22pub mod truncation;
23
24pub use atlas::{GlyphAtlas, GlyphEntry, GlyphKey};
25pub use editor::{TextArea, WrapMode};
26pub use highlight::{Highlighter, KeywordHighlighter};
27pub use ime::Preedit;
28pub use input::TextInput;
29pub use label::Label;
30
31use oxiui_core::UiError;
32
33pub use oxitext::{ParagraphMetrics, PositionedGlyph, RenderResult};
36
37#[derive(Debug)]
41pub enum TextError {
42 Pipeline(oxitext::OxiTextError),
44 Other(String),
46}
47
48impl std::fmt::Display for TextError {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 TextError::Pipeline(e) => write!(f, "text pipeline error: {e}"),
52 TextError::Other(s) => write!(f, "text error: {s}"),
53 }
54 }
55}
56
57impl std::error::Error for TextError {}
58
59impl From<oxitext::OxiTextError> for TextError {
60 fn from(e: oxitext::OxiTextError) -> Self {
61 TextError::Pipeline(e)
62 }
63}
64
65impl From<TextError> for UiError {
66 fn from(e: TextError) -> Self {
67 UiError::Render(e.to_string())
68 }
69}
70
71#[derive(Clone, Debug)]
79pub struct TextStyle {
80 pub font_family: Option<String>,
82 pub font_size: f32,
84 pub bold: bool,
86 pub italic: bool,
88 pub color: [u8; 4],
90 pub letter_spacing: f32,
92 pub line_height: f32,
94 pub max_width: f32,
96}
97
98impl Default for TextStyle {
99 fn default() -> Self {
100 Self::new(16.0)
101 }
102}
103
104impl TextStyle {
105 pub fn new(size: f32) -> Self {
107 Self {
108 font_family: None,
109 font_size: size,
110 bold: false,
111 italic: false,
112 color: [0, 0, 0, 255],
113 letter_spacing: 0.0,
114 line_height: 1.0,
115 max_width: 0.0,
116 }
117 }
118
119 pub fn family(mut self, name: impl Into<String>) -> Self {
121 self.font_family = Some(name.into());
122 self
123 }
124
125 pub fn bold(mut self) -> Self {
127 self.bold = true;
128 self
129 }
130
131 pub fn italic(mut self) -> Self {
133 self.italic = true;
134 self
135 }
136
137 pub fn color(mut self, rgba: [u8; 4]) -> Self {
139 self.color = rgba;
140 self
141 }
142
143 pub fn letter_spacing(mut self, spacing: f32) -> Self {
145 self.letter_spacing = spacing;
146 self
147 }
148
149 pub fn line_height(mut self, height: f32) -> Self {
151 self.line_height = height;
152 self
153 }
154
155 pub fn max_width(mut self, width: f32) -> Self {
157 self.max_width = width;
158 self
159 }
160
161 pub(crate) fn to_upstream(&self) -> oxitext::TextStyle {
163 oxitext::TextStyle {
164 font_size: self.font_size,
165 max_width: self.max_width,
166 flow_direction: oxitext::FlowDirection::Horizontal,
167 alignment: oxitext::TextAlignment::Left,
168 line_spacing: oxitext::LineSpacing::default(),
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
177pub struct GlyphPosition {
178 pub byte_offset: usize,
180 pub x: f32,
182 pub y: f32,
184 pub width: f32,
186 pub height: f32,
188}
189
190#[derive(Debug, Clone)]
194pub struct ShapedText {
195 pub lines: Vec<Vec<GlyphPosition>>,
197 pub total_width: f32,
199 pub total_height: f32,
201}
202
203pub struct TextPipeline {
209 inner: oxitext::Pipeline,
210}
211
212impl TextPipeline {
213 pub fn from_bytes(font_bytes: &[u8]) -> Result<Self, TextError> {
219 Ok(Self {
220 inner: oxitext::Pipeline::from_bytes(font_bytes)?,
221 })
222 }
223
224 pub fn from_system_font(family: &str) -> Result<Self, TextError> {
229 Ok(Self {
230 inner: oxitext::Pipeline::new_with_system_font(family)?,
231 })
232 }
233
234 pub fn set_fallback_fonts(&mut self, fonts: Vec<Vec<u8>>) {
239 self.inner.set_fallback_fonts(fonts);
240 }
241
242 pub fn shape(&mut self, text: &str, style: &TextStyle) -> Result<ShapedText, TextError> {
248 let upstream_style = style.to_upstream();
249 let layout = self.inner.shape_and_layout(text, &upstream_style)?;
250 let line_height = layout.metrics.total_height / layout.metrics.line_count.max(1) as f32;
251
252 let mut shaped_lines: Vec<Vec<GlyphPosition>> = Vec::with_capacity(layout.lines.len());
253 for line in &layout.lines {
254 let ascent = line.metrics.ascent;
255 let descent = line.metrics.descent;
256 let glyph_height = ascent + descent;
257 let top_y = line.metrics.baseline_y - ascent;
258
259 let glyphs: Vec<GlyphPosition> = layout.glyphs[line.glyph_start..line.glyph_end]
260 .iter()
261 .map(|g| GlyphPosition {
262 byte_offset: g.cluster as usize,
263 x: g.pos.0,
264 y: top_y,
265 width: g.advance_x,
266 height: glyph_height,
267 })
268 .collect();
269 shaped_lines.push(glyphs);
270 }
271
272 let _ = line_height; Ok(ShapedText {
276 lines: shaped_lines,
277 total_width: layout.metrics.total_width,
278 total_height: layout.metrics.total_height,
279 })
280 }
281
282 pub fn measure(&mut self, text: &str, style: &TextStyle) -> Result<(f32, f32), TextError> {
289 let upstream_style = style.to_upstream();
290 let metrics = self.inner.measure(text, &upstream_style)?;
291 Ok((metrics.total_width, metrics.total_height))
292 }
293
294 pub fn glyph_positions(
299 &mut self,
300 text: &str,
301 style: &TextStyle,
302 ) -> Result<Vec<GlyphPosition>, TextError> {
303 let shaped = self.shape(text, style)?;
304 Ok(shaped.lines.into_iter().flatten().collect())
305 }
306
307 pub fn render(&mut self, text: &str, style: &TextStyle) -> Result<RenderResult, UiError> {
314 let upstream_style = style.to_upstream();
315 self.inner
316 .render(text, &upstream_style)
317 .map_err(|e| UiError::Render(e.to_string()))
318 }
319}
320
321pub struct LazyTextPipeline {
329 font_bytes: Vec<u8>,
331 inner: std::cell::OnceCell<TextPipeline>,
333}
334
335impl LazyTextPipeline {
336 pub fn new(font_bytes: Vec<u8>) -> Self {
338 Self {
339 font_bytes,
340 inner: std::cell::OnceCell::new(),
341 }
342 }
343
344 pub fn get(&self) -> Result<&TextPipeline, TextError> {
350 if let Some(p) = self.inner.get() {
351 return Ok(p);
352 }
353 let pipeline = TextPipeline::from_bytes(&self.font_bytes)?;
354 let _ = self.inner.set(pipeline);
357 self.inner
358 .get()
359 .ok_or_else(|| TextError::Other("lazy pipeline initialisation failed".into()))
360 }
361}
362
363#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
371 fn from_bytes_result_type() {
372 let result = TextPipeline::from_bytes(&[]);
373 assert!(result.is_err(), "empty bytes must yield Err");
374 }
375
376 #[test]
378 fn text_style_defaults() {
379 let s = TextStyle::new(24.0);
380 assert!((s.font_size - 24.0).abs() < f32::EPSILON);
381 assert!(!s.bold);
382 assert!(!s.italic);
383 assert_eq!(s.color, [0, 0, 0, 255]);
384 }
385
386 #[test]
388 fn text_style_builder_chain() {
389 let s = TextStyle::new(16.0)
390 .bold()
391 .italic()
392 .color([255, 0, 0, 255])
393 .letter_spacing(2.0)
394 .family("Arial");
395 assert!(s.bold);
396 assert!(s.italic);
397 assert_eq!(s.color, [255, 0, 0, 255]);
398 assert_eq!(s.font_family.as_deref(), Some("Arial"));
399 }
400
401 #[test]
403 fn lazy_pipeline_empty_bytes_is_err() {
404 let lazy = LazyTextPipeline::new(vec![]);
405 assert!(lazy.get().is_err(), "empty bytes must yield Err");
406 }
407
408 #[test]
410 fn lazy_pipeline_second_call_still_err() {
411 let lazy = LazyTextPipeline::new(vec![]);
412 let _ = lazy.get();
413 assert!(
414 lazy.get().is_err(),
415 "repeated call with empty bytes must remain Err"
416 );
417 }
418}