orinium_browser/platform/renderer/
text_measurer.rs1use std::sync::Arc;
2
3use crate::engine::bridge::text::{
4 GlyphCluster, MeasuredFragment, TextMeasureError, TextMeasureRequest, TextMeasurer,
5};
6use crate::engine::layouter::types::{FontStyle, LineHeight};
7use crate::platform::renderer::text::global_font;
8use crate::platform::renderer::text::text_renderer::*;
9use crate::platform::renderer::text_cache::TextShapeCache;
10use crate::{perf_scope, profile_log};
11
12use orinium_text::TextStyle as OriTextStyle;
13use orinium_text::{
14 BidiMode, Color as OriColor, FontStyle as OriFontStyle, FontWeight as OriFontWeight,
15 TextLayouter,
16};
17
18fn quantize_font_size(px: f32) -> f32 {
19 (px * 64.0).round() / 64.0
20}
21
22pub struct PlatformTextMeasurer {
23 cache: TextShapeCache,
24}
25
26impl PlatformTextMeasurer {
27 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
28 if global_font::global_font_system_ready() {
29 Ok(Self {
30 cache: TextShapeCache::new(),
31 })
32 } else {
33 Err("no system font found".into())
34 }
35 }
36
37 pub fn from_bytes(_id: &str, _bytes: Vec<u8>) -> Result<Self, Box<dyn std::error::Error>> {
38 Ok(Self {
39 cache: TextShapeCache::new(),
40 })
41 }
42}
43
44impl TextMeasurer for PlatformTextMeasurer {
45 fn measure(&self, req: &TextMeasureRequest) -> Result<Vec<MeasuredFragment>, TextMeasureError> {
46 perf_scope!(total);
47
48 let style = req.attribute.style.clone();
49 let flow_style = req.attribute.flow_style;
50
51 let font_size = quantize_font_size(flow_style.font_size.max(1.0));
52
53 let line_height_ratio = match flow_style.line_height {
54 LineHeight::Normal => 1.2,
55 LineHeight::Number(n) => n,
56 LineHeight::Px(px) => px / font_size,
57 };
58
59 let font_families = build_family_list(&style.font_families);
60
61 let ori_style = OriTextStyle {
62 font_size,
63 color: OriColor(style.color.0, style.color.1, style.color.2, style.color.3),
64 font_weight: OriFontWeight(style.font_weight.0),
65 font_style: match style.font_style {
66 FontStyle::Normal => OriFontStyle::Normal,
67 FontStyle::Italic => OriFontStyle::Italic,
68 FontStyle::Oblique => OriFontStyle::Oblique,
69 },
70 line_height: line_height_ratio,
71 bidi_mode: BidiMode::Auto,
72 font_families,
73 exact_fonts: Vec::new(),
74 variant: orinium_text::FontVariant::Normal,
75 };
76
77 let mut layouter = TextLayouter::new();
78
79 perf_scope!(shape);
80 let shaped = if let Some(shaped) = self.cache.get(&req.text, &ori_style) {
81 shaped
82 } else {
83 let shaped = Arc::new(global_font::with_global_font_system(|fs| {
84 layouter.shape_text(fs, &req.text, &ori_style)
85 }));
86
87 self.cache
88 .insert(&req.text, &ori_style, Arc::clone(&shaped));
89 shaped
90 };
91 #[cfg(any(feature = "profile", debug_assertions))]
92 let shape_time = shape.elapsed();
93
94 let line_ranges: Vec<(usize, usize)> = req
95 .text
96 .split('\n')
97 .scan(0usize, |offset, line| {
98 let start = *offset;
99 *offset += line.len() + 1;
100 let end = start + line.len();
101 Some((start, end))
102 })
103 .collect();
104
105 perf_scope!(layout_pass);
106 let layout = global_font::with_global_font_system(|fs| {
107 layouter.layout_lines(fs, &shaped, &line_ranges, &ori_style)
108 });
109 #[cfg(any(feature = "profile", debug_assertions))]
110 let layout_time = layout_pass.elapsed();
111
112 let fragments: Vec<MeasuredFragment> = layout
113 .lines
114 .iter()
115 .enumerate()
116 .map(|(i, line)| {
117 let line_text = line_ranges[i];
118 MeasuredFragment {
119 text: req.text[line_text.0..line_text.1].to_string(),
120 width: line.width,
121 height: line.height,
122 }
123 })
124 .collect();
125
126 profile_log!(
127 target: "TextMeasurer",
128 log::Level::Info,
129 "measure: text={:?} len={} font_size={} shape={:?} layout={:?} total={:?} fragments={} cache={}",
130 crate::profile::text_preview(&req.text),
131 req.text.len(),
132 font_size,
133 shape_time,
134 layout_time,
135 total.elapsed(),
136 fragments.len(),
137 self.cache.stats(),
138 );
139 Ok(fragments)
140 }
141
142 fn measure_shaped(
143 &self,
144 req: &TextMeasureRequest,
145 ) -> Result<Vec<GlyphCluster>, TextMeasureError> {
146 perf_scope!(total);
147
148 let style = req.attribute.style.clone();
149 let flow_style = req.attribute.flow_style;
150
151 let font_size = quantize_font_size(flow_style.font_size.max(1.0));
152
153 let line_height_ratio = match flow_style.line_height {
154 LineHeight::Normal => 1.2,
155 LineHeight::Number(n) => n,
156 LineHeight::Px(px) => px / font_size,
157 };
158
159 let font_families = build_family_list(&style.font_families);
160
161 let ori_style = OriTextStyle {
162 font_size,
163 color: OriColor(style.color.0, style.color.1, style.color.2, style.color.3),
164 font_weight: OriFontWeight(style.font_weight.0),
165 font_style: match style.font_style {
166 FontStyle::Normal => OriFontStyle::Normal,
167 FontStyle::Italic => OriFontStyle::Italic,
168 FontStyle::Oblique => OriFontStyle::Oblique,
169 },
170 line_height: line_height_ratio,
171 bidi_mode: BidiMode::Auto,
172 font_families,
173 exact_fonts: Vec::new(),
174 variant: orinium_text::FontVariant::Normal,
175 };
176
177 let mut layouter = TextLayouter::new();
178
179 let shaped = if let Some(shaped) = self.cache.get(&req.text, &ori_style) {
180 shaped
181 } else {
182 let shaped = Arc::new(global_font::with_global_font_system(|fs| {
183 layouter.shape_text(fs, &req.text, &ori_style)
184 }));
185
186 self.cache
187 .insert(&req.text, &ori_style, Arc::clone(&shaped));
188 shaped
189 };
190
191 let clusters: Vec<GlyphCluster> = shaped
192 .fragments
193 .iter()
194 .map(|f| GlyphCluster {
195 byte_offset: f.cluster,
196 width: f.width,
197 break_allowed: f.break_after,
198 })
199 .collect();
200
201 profile_log!(
202 target: "TextMeasurer",
203 log::Level::Info,
204 "measure_shaped: text={:?} len={} font_size={} total={:?} clusters={} cache={}",
205 crate::profile::text_preview(&req.text),
206 req.text.len(),
207 font_size,
208 total.elapsed(),
209 clusters.len(),
210 self.cache.stats(),
211 );
212
213 Ok(clusters)
214 }
215}