orinium_browser/platform/renderer/
text_measurer.rs

1//! Text measurement interface and implementations.
2
3use crate::engine::bridge::text::{
4    TextMeasureError, TextMeasureRequest, TextMeasurer, TextMetrics,
5};
6use crate::engine::layouter::types::TextStyle;
7
8use std::env;
9use std::sync::{Arc, Mutex};
10
11use glyphon::{Attrs, Buffer, Color as GlyphColor, FontSystem, Metrics, Shaping, Style, Weight};
12
13/// Platform-backed text measurer using glyphon / cosmic-text.
14///
15/// This measurer performs real text shaping and line layout,
16/// and is intended for production use.
17pub struct PlatformTextMeasurer {
18    /// Font system used for shaping and metrics
19    font_sys: Mutex<FontSystem>,
20}
21
22impl PlatformTextMeasurer {
23    /// Initialize using system fonts.
24    ///
25    /// TODO:
26    /// - Share font system with PlatformTextRenderer
27    /// - Support font family / fallback selection
28    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
29        let mut maybe_bytes: Option<Vec<u8>> = None;
30
31        if let Ok(p) = env::var("ORINIUM_FONT")
32            && let Ok(b) = std::fs::read(&p)
33        {
34            maybe_bytes = Some(b);
35        }
36
37        if maybe_bytes.is_none() {
38            for p in crate::platform::font::system_font_candidates()? {
39                if let Ok(b) = std::fs::read(p) {
40                    maybe_bytes = Some(b);
41                    break;
42                }
43            }
44        }
45
46        if let Some(bytes) = maybe_bytes {
47            let font_source = Arc::new(bytes);
48            let font = glyphon::fontdb::Source::Binary(font_source);
49            let font_sys = FontSystem::new_with_fonts(vec![font]);
50
51            return Ok(Self {
52                font_sys: Mutex::new(font_sys),
53            });
54        }
55
56        Err("no system font found".into())
57    }
58
59    /// Initialize from raw font bytes.
60    pub fn from_bytes(_id: &str, bytes: Vec<u8>) -> Result<Self, Box<dyn std::error::Error>> {
61        let font_source = Arc::new(bytes);
62        let font = glyphon::fontdb::Source::Binary(font_source);
63        let font_sys = FontSystem::new_with_fonts(vec![font]);
64
65        Ok(Self {
66            font_sys: Mutex::new(font_sys),
67        })
68    }
69}
70
71impl TextMeasurer<TextStyle> for PlatformTextMeasurer {
72    /// Measure text using real shaping and line layout.
73    ///
74    /// Notes:
75    /// - Baseline is currently approximated
76    /// - Decorations and alignment are handled at render time
77    fn measure(
78        &self,
79        req: &TextMeasureRequest<TextStyle>,
80    ) -> Result<TextMetrics, TextMeasureError> {
81        let font_size = req.style.font_size.max(1.0);
82
83        let mut fs = self
84            .font_sys
85            .lock()
86            .map_err(|e| TextMeasureError::Internal(format!("font_sys lock poisoned: {}", e)))?;
87
88        // glyphon metrics: font size + line height
89        let metrics = Metrics::relative(font_size, 1.2);
90        let mut buffer = Buffer::new(&mut fs, metrics);
91
92        // Attributes used only for shaping / layout
93        let attrs = Attrs::new()
94            .metrics(metrics)
95            .color(GlyphColor::rgba(0, 0, 0, 255))
96            .weight(Weight(req.style.font_weight.0))
97            .style(Style::from(req.style.font_style));
98
99        buffer.set_text(&mut fs, &req.text, &attrs, Shaping::Advanced, None);
100
101        let mut max_width: f32 = 0.0;
102        let mut line_count: usize = 0;
103
104        // Iterate over shaped lines
105        for para_i in 0..buffer.lines.len() {
106            if let Some(layout_lines) = buffer.line_layout(&mut fs, para_i) {
107                for line in layout_lines {
108                    max_width = max_width.max(line.w);
109                    line_count += 1;
110                }
111            }
112        }
113
114        if line_count == 0 {
115            // Empty text
116            return Ok(TextMetrics {
117                width: 0.0,
118                height: metrics.line_height,
119                baseline: font_size * 0.8,
120                line_count: 1,
121            });
122        }
123
124        // Apply wrapping constraint
125        if let Some(max_width_limit) = req.max_width {
126            max_width = max_width.min(max_width_limit);
127        }
128
129        let height = metrics.line_height * line_count as f32;
130
131        Ok(TextMetrics {
132            width: max_width,
133            height,
134            baseline: font_size * 0.8, // TODO: precise baseline from font metrics
135            line_count,
136        })
137    }
138}