| 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 | |