1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3pub mod atlas;
10pub mod cache;
11pub mod decoration;
12pub mod editor;
13#[cfg(feature = "emoji")]
18pub mod emoji;
19pub mod fallback;
20pub mod highlight;
21pub mod hyperlink;
22pub mod ime;
23pub mod input;
24pub mod label;
25pub mod layout;
26pub mod rich;
27pub mod selection;
28pub mod truncation;
29
30pub use atlas::{GlyphAtlas, GlyphEntry, GlyphKey};
31pub use editor::{TextArea, WrapMode};
32pub use highlight::{Highlighter, KeywordHighlighter};
33pub use ime::Preedit;
34pub use input::TextInput;
35pub use label::Label;
36
37use oxiui_core::UiError;
38
39pub use oxitext::{ParagraphMetrics, PositionedGlyph, RenderResult};
42
43#[derive(Debug)]
47pub enum TextError {
48 Pipeline(oxitext::OxiTextError),
50 Other(String),
52}
53
54impl std::fmt::Display for TextError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 TextError::Pipeline(e) => write!(f, "text pipeline error: {e}"),
58 TextError::Other(s) => write!(f, "text error: {s}"),
59 }
60 }
61}
62
63impl std::error::Error for TextError {}
64
65impl From<oxitext::OxiTextError> for TextError {
66 fn from(e: oxitext::OxiTextError) -> Self {
67 TextError::Pipeline(e)
68 }
69}
70
71impl From<TextError> for UiError {
72 fn from(e: TextError) -> Self {
73 UiError::Render(e.to_string())
74 }
75}
76
77#[derive(Clone, Debug)]
85pub struct TextStyle {
86 pub font_family: Option<String>,
88 pub font_size: f32,
90 pub bold: bool,
92 pub italic: bool,
94 pub color: [u8; 4],
96 pub letter_spacing: f32,
98 pub line_height: f32,
100 pub max_width: f32,
102}
103
104impl Default for TextStyle {
105 fn default() -> Self {
106 Self::new(16.0)
107 }
108}
109
110impl TextStyle {
111 pub fn new(size: f32) -> Self {
113 Self {
114 font_family: None,
115 font_size: size,
116 bold: false,
117 italic: false,
118 color: [0, 0, 0, 255],
119 letter_spacing: 0.0,
120 line_height: 1.0,
121 max_width: 0.0,
122 }
123 }
124
125 pub fn family(mut self, name: impl Into<String>) -> Self {
127 self.font_family = Some(name.into());
128 self
129 }
130
131 pub fn bold(mut self) -> Self {
133 self.bold = true;
134 self
135 }
136
137 pub fn italic(mut self) -> Self {
139 self.italic = true;
140 self
141 }
142
143 pub fn color(mut self, rgba: [u8; 4]) -> Self {
145 self.color = rgba;
146 self
147 }
148
149 pub fn letter_spacing(mut self, spacing: f32) -> Self {
151 self.letter_spacing = spacing;
152 self
153 }
154
155 pub fn line_height(mut self, height: f32) -> Self {
157 self.line_height = height;
158 self
159 }
160
161 pub fn max_width(mut self, width: f32) -> Self {
163 self.max_width = width;
164 self
165 }
166
167 pub(crate) fn to_upstream(&self) -> oxitext::TextStyle {
169 oxitext::TextStyle {
170 font_size: self.font_size,
171 max_width: self.max_width,
172 flow_direction: oxitext::FlowDirection::Horizontal,
173 alignment: oxitext::TextAlignment::Left,
174 line_spacing: oxitext::LineSpacing::default(),
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq)]
183pub struct GlyphPosition {
184 pub byte_offset: usize,
186 pub x: f32,
188 pub y: f32,
190 pub width: f32,
192 pub height: f32,
194}
195
196#[derive(Debug, Clone)]
200pub struct ShapedText {
201 pub lines: Vec<Vec<GlyphPosition>>,
203 pub total_width: f32,
205 pub total_height: f32,
207}
208
209pub struct TextPipeline {
215 inner: oxitext::Pipeline,
216}
217
218impl TextPipeline {
219 pub fn from_bytes(font_bytes: &[u8]) -> Result<Self, TextError> {
225 Ok(Self {
226 inner: oxitext::Pipeline::from_bytes(font_bytes)?,
227 })
228 }
229
230 pub fn from_system_font(family: &str) -> Result<Self, TextError> {
235 Ok(Self {
236 inner: oxitext::Pipeline::new_with_system_font(family)?,
237 })
238 }
239
240 pub fn set_fallback_fonts(&mut self, fonts: Vec<Vec<u8>>) {
245 self.inner.set_fallback_fonts(fonts);
246 }
247
248 pub fn shape(&mut self, text: &str, style: &TextStyle) -> Result<ShapedText, TextError> {
254 let upstream_style = style.to_upstream();
255 let layout = self.inner.shape_and_layout(text, &upstream_style)?;
256 let line_height = layout.metrics.total_height / layout.metrics.line_count.max(1) as f32;
257
258 let mut shaped_lines: Vec<Vec<GlyphPosition>> = Vec::with_capacity(layout.lines.len());
259 for line in &layout.lines {
260 let ascent = line.metrics.ascent;
261 let descent = line.metrics.descent;
262 let glyph_height = ascent + descent;
263 let top_y = line.metrics.baseline_y - ascent;
264
265 let glyphs: Vec<GlyphPosition> = layout.glyphs[line.glyph_start..line.glyph_end]
266 .iter()
267 .map(|g| GlyphPosition {
268 byte_offset: g.cluster as usize,
269 x: g.pos.0,
270 y: top_y,
271 width: g.advance_x,
272 height: glyph_height,
273 })
274 .collect();
275 shaped_lines.push(glyphs);
276 }
277
278 let _ = line_height; Ok(ShapedText {
282 lines: shaped_lines,
283 total_width: layout.metrics.total_width,
284 total_height: layout.metrics.total_height,
285 })
286 }
287
288 pub fn measure(&mut self, text: &str, style: &TextStyle) -> Result<(f32, f32), TextError> {
295 let upstream_style = style.to_upstream();
296 let metrics = self.inner.measure(text, &upstream_style)?;
297 Ok((metrics.total_width, metrics.total_height))
298 }
299
300 pub fn glyph_positions(
305 &mut self,
306 text: &str,
307 style: &TextStyle,
308 ) -> Result<Vec<GlyphPosition>, TextError> {
309 let shaped = self.shape(text, style)?;
310 Ok(shaped.lines.into_iter().flatten().collect())
311 }
312
313 pub fn render(&mut self, text: &str, style: &TextStyle) -> Result<RenderResult, UiError> {
320 let upstream_style = style.to_upstream();
321 self.inner
322 .render(text, &upstream_style)
323 .map_err(|e| UiError::Render(e.to_string()))
324 }
325}
326
327pub struct LazyTextPipeline {
335 font_bytes: Vec<u8>,
337 inner: std::cell::OnceCell<TextPipeline>,
339}
340
341impl LazyTextPipeline {
342 pub fn new(font_bytes: Vec<u8>) -> Self {
344 Self {
345 font_bytes,
346 inner: std::cell::OnceCell::new(),
347 }
348 }
349
350 pub fn get(&self) -> Result<&TextPipeline, TextError> {
356 if let Some(p) = self.inner.get() {
357 return Ok(p);
358 }
359 let pipeline = TextPipeline::from_bytes(&self.font_bytes)?;
360 let _ = self.inner.set(pipeline);
363 self.inner
364 .get()
365 .ok_or_else(|| TextError::Other("lazy pipeline initialisation failed".into()))
366 }
367}
368
369#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
377 fn from_bytes_result_type() {
378 let result = TextPipeline::from_bytes(&[]);
379 assert!(result.is_err(), "empty bytes must yield Err");
380 }
381
382 #[test]
384 fn text_style_defaults() {
385 let s = TextStyle::new(24.0);
386 assert!((s.font_size - 24.0).abs() < f32::EPSILON);
387 assert!(!s.bold);
388 assert!(!s.italic);
389 assert_eq!(s.color, [0, 0, 0, 255]);
390 }
391
392 #[test]
394 fn text_style_builder_chain() {
395 let s = TextStyle::new(16.0)
396 .bold()
397 .italic()
398 .color([255, 0, 0, 255])
399 .letter_spacing(2.0)
400 .family("Arial");
401 assert!(s.bold);
402 assert!(s.italic);
403 assert_eq!(s.color, [255, 0, 0, 255]);
404 assert_eq!(s.font_family.as_deref(), Some("Arial"));
405 }
406
407 #[test]
409 fn lazy_pipeline_empty_bytes_is_err() {
410 let lazy = LazyTextPipeline::new(vec![]);
411 assert!(lazy.get().is_err(), "empty bytes must yield Err");
412 }
413
414 #[test]
416 fn lazy_pipeline_second_call_still_err() {
417 let lazy = LazyTextPipeline::new(vec![]);
418 let _ = lazy.get();
419 assert!(
420 lazy.get().is_err(),
421 "repeated call with empty bytes must remain Err"
422 );
423 }
424}