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
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7use i_slint_core::graphics::euclid::num::Zero;
8use i_slint_core::graphics::FontRequest;
9use i_slint_core::items::{TextHorizontalAlignment, TextVerticalAlignment};
10use i_slint_core::lengths::{LogicalLength, ScaleFactor};
11use i_slint_core::{items, Color};
12
13use super::itemrenderer::to_skia_color;
14use super::{PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalSize};
15
16pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.);
17
18#[derive(PartialEq, Eq)]
19enum CustomFontSource {
20 ByData(&'static [u8]),
21 ByPath(std::path::PathBuf),
22}
23
24struct 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
31thread_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
46pub 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
54pub 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
61fn 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
73pub 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
176pub 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
210fn 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
245pub fn register_font_from_memory(data: &'static [u8]) -> Result<(), Box<dyn std::error::Error>> {
246 register_font(source:CustomFontSource::ByData(data))
247}
248
249pub 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
253pub 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

Provided by KDAB

Privacy Policy