1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 |
3 | |
4 | use std::cell::RefCell; |
5 | use std::collections::HashMap; |
6 | |
7 | use i_slint_core::graphics::euclid::num::Zero; |
8 | use i_slint_core::graphics::FontRequest; |
9 | use i_slint_core::items::{TextHorizontalAlignment, TextVerticalAlignment}; |
10 | use i_slint_core::lengths::{LogicalLength, ScaleFactor}; |
11 | use i_slint_core::{items, Color}; |
12 | |
13 | use super::itemrenderer::to_skia_color; |
14 | use super::{PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalSize}; |
15 | |
16 | pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.); |
17 | |
18 | #[derive (PartialEq, Eq)] |
19 | enum CustomFontSource { |
20 | ByData(&'static [u8]), |
21 | ByPath(std::path::PathBuf), |
22 | } |
23 | |
24 | struct FontCache { |
25 | font_collection: RefCell<skia_safe::textlayout::FontCollection>, |
26 | font_mgr: skia_safe::FontMgr, |
27 | type_face_font_provider: RefCell<skia_safe::textlayout::TypefaceFontProvider>, |
28 | custom_fonts: RefCell<HashMap<String, CustomFontSource>>, |
29 | } |
30 | |
31 | thread_local! { |
32 | static FONT_CACHE: FontCache = { |
33 | let font_mgr = skia_safe::FontMgr::new(); |
34 | let type_face_font_provider = skia_safe::textlayout::TypefaceFontProvider::new(); |
35 | let mut font_collection = skia_safe::textlayout::FontCollection::new(); |
36 | // FontCollection first looks up in the dynamic font manager and then the asset font manager. If the |
37 | // family is empty, the default font manager will match against the system default. We want that behavior, |
38 | // and only if the family is not present in the system, then we want to fall back to the assert font manager |
39 | // to pick up the custom font. |
40 | font_collection.set_asset_font_manager(Some(type_face_font_provider.clone().into())); |
41 | font_collection.set_dynamic_font_manager(font_mgr.clone()); |
42 | FontCache { font_collection: RefCell::new(font_collection), font_mgr, type_face_font_provider: RefCell::new(type_face_font_provider), custom_fonts: Default::default() } |
43 | } |
44 | } |
45 | |
46 | pub fn default_font(scale_factor: f32) -> Option<skia_safe::Font> { |
47 | FONT_CACHE.with(|font_cache: &FontCache| { |
48 | font_cache.font_mgr.legacy_make_typeface(family_name:None, style:skia_safe::FontStyle::default()).map( |
49 | |type_face: RCHandle| skia_safe::Font::new(typeface:type_face, DEFAULT_FONT_SIZE.get() * scale_factor), |
50 | ) |
51 | }) |
52 | } |
53 | |
54 | pub struct Selection { |
55 | pub range: std::ops::Range<usize>, |
56 | pub background: Option<Color>, |
57 | pub foreground: Option<Color>, |
58 | pub underline: bool, |
59 | } |
60 | |
61 | fn font_style_for_request(font_request: &FontRequest) -> skia_safe::FontStyle { |
62 | skia_safe::FontStyle::new( |
63 | weight:font_request.weight.map_or(skia_safe::font_style::Weight::NORMAL, |w| w.into()), |
64 | width:skia_safe::font_style::Width::NORMAL, |
65 | slant:if font_request.italic { |
66 | skia_safe::font_style::Slant::Italic |
67 | } else { |
68 | skia_safe::font_style::Slant::Upright |
69 | }, |
70 | ) |
71 | } |
72 | |
73 | pub fn create_layout( |
74 | font_request: FontRequest, |
75 | scale_factor: ScaleFactor, |
76 | text: &str, |
77 | text_style: Option<skia_safe::textlayout::TextStyle>, |
78 | max_width: Option<PhysicalLength>, |
79 | max_height: PhysicalLength, |
80 | h_align: items::TextHorizontalAlignment, |
81 | v_align: TextVerticalAlignment, |
82 | wrap: items::TextWrap, |
83 | overflow: items::TextOverflow, |
84 | selection: Option<&Selection>, |
85 | ) -> (skia_safe::textlayout::Paragraph, PhysicalPoint) { |
86 | let mut text_style = text_style.unwrap_or_default(); |
87 | |
88 | if let Some(family_name) = font_request.family.as_ref() { |
89 | text_style.set_font_families(&[family_name.as_str()]); |
90 | } |
91 | |
92 | let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE) * scale_factor; |
93 | |
94 | if let Some(letter_spacing) = font_request.letter_spacing { |
95 | text_style.set_letter_spacing((letter_spacing * scale_factor).get()); |
96 | } |
97 | text_style.set_font_size(pixel_size.get()); |
98 | text_style.set_font_style(font_style_for_request(&font_request)); |
99 | |
100 | let mut style = skia_safe::textlayout::ParagraphStyle::new(); |
101 | |
102 | if overflow == items::TextOverflow::Elide { |
103 | style.set_ellipsis("…" ); |
104 | if wrap != items::TextWrap::NoWrap { |
105 | let metrics = text_style.font_metrics(); |
106 | let line_height = metrics.descent - metrics.ascent + metrics.leading; |
107 | style.set_max_lines((max_height.get() / line_height).floor() as usize); |
108 | } |
109 | } |
110 | |
111 | style.set_text_align(match h_align { |
112 | items::TextHorizontalAlignment::Left => skia_safe::textlayout::TextAlign::Left, |
113 | items::TextHorizontalAlignment::Center => skia_safe::textlayout::TextAlign::Center, |
114 | items::TextHorizontalAlignment::Right => skia_safe::textlayout::TextAlign::Right, |
115 | }); |
116 | |
117 | style.set_text_style(&text_style); |
118 | |
119 | let mut builder = FONT_CACHE.with(|font_cache| { |
120 | skia_safe::textlayout::ParagraphBuilder::new( |
121 | &style, |
122 | font_cache.font_collection.borrow().clone(), |
123 | ) |
124 | }); |
125 | |
126 | if let Some(selection) = selection { |
127 | let before_selection = &text[..selection.range.start]; |
128 | builder.add_text(before_selection); |
129 | |
130 | let mut selection_style = text_style.clone(); |
131 | |
132 | if let Some(selection_background) = selection.background { |
133 | let mut selection_background_paint = skia_safe::Paint::default(); |
134 | selection_background_paint.set_color(to_skia_color(&selection_background)); |
135 | selection_style.set_background_paint(&selection_background_paint); |
136 | } |
137 | |
138 | if let Some(selection_foreground) = selection.foreground { |
139 | let mut selection_foreground_paint = skia_safe::Paint::default(); |
140 | selection_foreground_paint.set_color(to_skia_color(&selection_foreground)); |
141 | selection_style.set_foreground_paint(&selection_foreground_paint); |
142 | } |
143 | |
144 | if selection.underline { |
145 | let mut decoration = skia_safe::textlayout::Decoration::default(); |
146 | decoration.ty = skia_safe::textlayout::TextDecoration::UNDERLINE; |
147 | decoration.color = text_style.foreground().color(); |
148 | selection_style.set_decoration(&decoration); |
149 | } |
150 | |
151 | builder.push_style(&selection_style); |
152 | let selected_text = &text[selection.range.clone()]; |
153 | builder.add_text(selected_text); |
154 | builder.pop(); |
155 | |
156 | let after_selection = &text[selection.range.end..]; |
157 | builder.add_text(after_selection); |
158 | } else { |
159 | builder.add_text(text); |
160 | } |
161 | |
162 | let mut paragraph = builder.build(); |
163 | paragraph.layout(max_width.map_or(f32::MAX, |physical_width| physical_width.get())); |
164 | |
165 | let layout_height = PhysicalLength::new(paragraph.height()); |
166 | |
167 | let layout_top_y = match v_align { |
168 | i_slint_core::items::TextVerticalAlignment::Top => PhysicalLength::zero(), |
169 | i_slint_core::items::TextVerticalAlignment::Center => (max_height - layout_height) / 2., |
170 | i_slint_core::items::TextVerticalAlignment::Bottom => max_height - layout_height, |
171 | }; |
172 | |
173 | (paragraph, PhysicalPoint::from_lengths(Default::default(), layout_top_y)) |
174 | } |
175 | |
176 | pub fn font_metrics( |
177 | font_request: i_slint_core::graphics::FontRequest, |
178 | scale_factor: ScaleFactor, |
179 | ) -> i_slint_core::items::FontMetrics { |
180 | let (layout, _) = create_layout( |
181 | font_request, |
182 | scale_factor, |
183 | " " , |
184 | None, |
185 | None, |
186 | PhysicalLength::new(f32::MAX), |
187 | Default::default(), |
188 | Default::default(), |
189 | Default::default(), |
190 | Default::default(), |
191 | None, |
192 | ); |
193 | |
194 | let fonts = layout.get_fonts(); |
195 | |
196 | let Some(font_info) = fonts.first() else { |
197 | return Default::default(); |
198 | }; |
199 | |
200 | let metrics = font_info.font.metrics().1; |
201 | |
202 | i_slint_core::items::FontMetrics { |
203 | ascent: -metrics.ascent / scale_factor.get(), |
204 | descent: -metrics.descent / scale_factor.get(), |
205 | x_height: metrics.x_height / scale_factor.get(), |
206 | cap_height: metrics.cap_height / scale_factor.get(), |
207 | } |
208 | } |
209 | |
210 | fn register_font(source: CustomFontSource) -> Result<(), Box<dyn std::error::Error>> { |
211 | FONT_CACHE.with(|font_cache| { |
212 | if font_cache |
213 | .custom_fonts |
214 | .borrow() |
215 | .values() |
216 | .position(|registered_font| *registered_font == source) |
217 | .is_some() |
218 | { |
219 | return Ok(()); |
220 | } |
221 | |
222 | let data: std::borrow::Cow<[u8]> = match &source { |
223 | CustomFontSource::ByData(data) => std::borrow::Cow::Borrowed(data), |
224 | CustomFontSource::ByPath(path) => std::borrow::Cow::Owned(std::fs::read(path)?), |
225 | }; |
226 | |
227 | let type_face = |
228 | font_cache.font_mgr.new_from_data(data.as_ref(), None).ok_or_else(|| { |
229 | std::io::Error::new( |
230 | std::io::ErrorKind::Other, |
231 | "error parsing TrueType font" .to_string(), |
232 | ) |
233 | })?; |
234 | |
235 | drop(data); |
236 | |
237 | let family_name = type_face.family_name(); |
238 | let no_alias: Option<&str> = None; |
239 | font_cache.type_face_font_provider.borrow_mut().register_typeface(type_face, no_alias); |
240 | font_cache.custom_fonts.borrow_mut().insert(family_name, source); |
241 | Ok(()) |
242 | }) |
243 | } |
244 | |
245 | pub fn register_font_from_memory(data: &'static [u8]) -> Result<(), Box<dyn std::error::Error>> { |
246 | register_font(source:CustomFontSource::ByData(data)) |
247 | } |
248 | |
249 | pub fn register_font_from_path(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> { |
250 | register_font(source:CustomFontSource::ByPath(path.into())) |
251 | } |
252 | |
253 | pub fn cursor_rect( |
254 | string: &str, |
255 | cursor_pos: usize, |
256 | layout: skia_safe::textlayout::Paragraph, |
257 | cursor_width: PhysicalLength, |
258 | h_align: TextHorizontalAlignment, |
259 | ) -> PhysicalRect { |
260 | if string.is_empty() { |
261 | let x = match h_align { |
262 | TextHorizontalAlignment::Left => PhysicalLength::default(), |
263 | TextHorizontalAlignment::Center => PhysicalLength::new(layout.max_width() / 2.), |
264 | TextHorizontalAlignment::Right => PhysicalLength::new(layout.max_width()), |
265 | }; |
266 | return PhysicalRect::new( |
267 | PhysicalPoint::from_lengths(x, PhysicalLength::default()), |
268 | PhysicalSize::from_lengths(cursor_width, PhysicalLength::new(layout.height())), |
269 | ); |
270 | } |
271 | |
272 | // This is needed in case of the cursor is moving to the end of the text (#7203). |
273 | let cursor_pos = cursor_pos.min(string.len()); |
274 | // Not doing this check may cause crashing with non-ASCII text. |
275 | if !string.is_char_boundary(cursor_pos) { |
276 | return Default::default(); |
277 | } |
278 | |
279 | // SkParagraph::getRectsForRange() does not report the text box of a trailing newline |
280 | // correctly. Use the last line's metrics to get the correct coordinates (#3590). |
281 | if cursor_pos == string.len() |
282 | && string.ends_with(|ch| ch == ' \n' || ch == ' \u{2028}' || ch == ' \u{2029}' ) |
283 | { |
284 | if let Some(metrics) = layout.get_line_metrics_at(layout.line_number() - 1) { |
285 | return PhysicalRect::new( |
286 | PhysicalPoint::new( |
287 | (metrics.left + metrics.width) as f32, |
288 | (metrics.baseline - metrics.ascent) as f32, |
289 | ), |
290 | PhysicalSize::from_lengths( |
291 | cursor_width, |
292 | PhysicalLength::new(metrics.height as f32), |
293 | ), |
294 | ); |
295 | } |
296 | } |
297 | |
298 | // The cursor is visually between characters, but the logical cursor_pos refers to the |
299 | // index in the string that is the start of a glyph cluster. The cursor is to be drawn |
300 | // at the left edge of that glyph cluster. |
301 | // When the cursor is at the end of the text, there's no glyph cluster to the right. |
302 | // Instead we pick the previous glyph cluster and select the right edge of it. |
303 | |
304 | let select_glyph_box_edge_x = if cursor_pos == string.len() { |
305 | |rect: &skia_safe::Rect| rect.right |
306 | } else { |
307 | |rect: &skia_safe::Rect| rect.left |
308 | }; |
309 | |
310 | let mut grapheme_cursor = |
311 | unicode_segmentation::GraphemeCursor::new(cursor_pos, string.len(), true); |
312 | let adjacent_grapheme_byte_range = if cursor_pos == string.len() { |
313 | let prev_grapheme = match grapheme_cursor.prev_boundary(string, 0) { |
314 | Ok(byte_offset) => byte_offset.unwrap_or(0), |
315 | Err(_) => return Default::default(), |
316 | }; |
317 | |
318 | prev_grapheme..cursor_pos |
319 | } else { |
320 | let next_grapheme = match grapheme_cursor.next_boundary(string, 0) { |
321 | Ok(byte_offset) => byte_offset.unwrap_or_else(|| string.len()), |
322 | Err(_) => return Default::default(), |
323 | }; |
324 | |
325 | cursor_pos..next_grapheme |
326 | }; |
327 | |
328 | let adjacent_grapheme_utf16_start = |
329 | string[..adjacent_grapheme_byte_range.start].chars().map(char::len_utf16).sum(); |
330 | let adjacent_grapheme_utf16_next: usize = |
331 | string[adjacent_grapheme_byte_range].chars().map(char::len_utf16).sum(); |
332 | |
333 | let boxes = layout.get_rects_for_range( |
334 | adjacent_grapheme_utf16_start..adjacent_grapheme_utf16_start + adjacent_grapheme_utf16_next, |
335 | skia_safe::textlayout::RectHeightStyle::Max, |
336 | skia_safe::textlayout::RectWidthStyle::Max, |
337 | ); |
338 | boxes |
339 | .into_iter() |
340 | .next() |
341 | .map(|textbox| { |
342 | let x = select_glyph_box_edge_x(&textbox.rect); |
343 | PhysicalRect::new( |
344 | PhysicalPoint::new(x, textbox.rect.y()), |
345 | PhysicalSize::from_lengths( |
346 | cursor_width, |
347 | PhysicalLength::new(textbox.rect.height()), |
348 | ), |
349 | ) |
350 | }) |
351 | .unwrap_or_default() |
352 | } |
353 | |