| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- use harfbuzz_sys::{
- freetype::hb_ft_font_create_referenced, hb_buffer_add_utf8, hb_buffer_create,
- hb_buffer_destroy, hb_buffer_get_glyph_infos, hb_buffer_get_glyph_positions,
- hb_buffer_guess_segment_properties, hb_buffer_set_cluster_level, hb_buffer_set_content_type,
- hb_font_destroy, hb_glyph_info_t, hb_glyph_position_t, hb_shape,
- HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES, HB_BUFFER_CONTENT_TYPE_UNICODE,
- };
- use std::{
- collections::HashMap,
- ffi::OsStr,
- os,
- path::PathBuf,
- sync::{Arc, Mutex as SyncMutex, Weak},
- };
- use crate::gfx::Rectangle;
- mod atlas;
- pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
- mod ft;
- use ft::{render_glyph, FreetypeFace, Sprite, SpritePtr};
- mod shape;
- use shape::{set_face_size, shape};
- mod wrap;
- pub use wrap::{glyph_str, wrap};
- // Upscale emoji relative to font size
- pub const EMOJI_SCALE_FACT: f32 = 1.6;
- // How much of the emoji is above baseline?
- pub const EMOJI_PROP_ABOVE_BASELINE: f32 = 0.8;
- #[cfg(target_os = "android")]
- fn custom_font_path() -> PathBuf {
- crate::android::get_external_storage_path().join("font")
- }
- #[cfg(not(target_os = "android"))]
- fn custom_font_path() -> PathBuf {
- dirs::data_local_dir().unwrap().join("darkfi/app/font")
- }
- // From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
- //
- // * An `FT_Face' object can only be safely used from one thread at
- // a time.
- //
- // * An `FT_Library' object can now be used without modification
- // from multiple threads at the same time.
- //
- // * `FT_Face' creation and destruction with the same `FT_Library'
- // object can only be done from one thread at a time.
- //
- // One can use a single `FT_Library' object across threads as long
- // as a mutex lock is used around `FT_New_Face' and `FT_Done_Face'.
- // Any calls to `FT_Load_Glyph' and similar API are safe and do not
- // need the lock to be held as long as the same `FT_Face' is not
- // used from multiple threads at the same time.
- // Harfbuzz is threadsafe.
- // Notes:
- // * All ft init and face creation should happen at startup.
- // * FT faces protected behind a Mutex
- // * Glyph cache. Key is (glyph_id, font_size)
- // * Glyph texture cache: (glyph_id, font_size, color)
- #[derive(Clone)]
- pub struct GlyphPositionIter<'a> {
- font_size: f32,
- window_scale: f32,
- glyphs: &'a Vec<Glyph>,
- current_x: f32,
- current_y: f32,
- i: usize,
- }
- impl<'a> GlyphPositionIter<'a> {
- pub fn new(font_size: f32, window_scale: f32, glyphs: &'a Vec<Glyph>, baseline_y: f32) -> Self {
- let start_y = baseline_y * window_scale;
- Self { font_size, window_scale, glyphs, current_x: 0., current_y: start_y, i: 0 }
- }
- }
- impl<'a> Iterator for GlyphPositionIter<'a> {
- type Item = Rectangle;
- fn next(&mut self) -> Option<Self::Item> {
- assert!(self.i <= self.glyphs.len());
- if self.i == self.glyphs.len() {
- return None
- }
- let glyph = &self.glyphs[self.i];
- let sprite = &glyph.sprite;
- // current_x/y is scaled real coords
- // but the returned rect is unscaled
- let rect = if sprite.has_fixed_sizes {
- // Downscale by height
- let w = (sprite.bmp_width as f32 * EMOJI_SCALE_FACT * self.font_size) /
- sprite.bmp_height as f32;
- let h = EMOJI_SCALE_FACT * self.font_size;
- let x = self.current_x / self.window_scale;
- let y = self.current_y / self.window_scale - (EMOJI_PROP_ABOVE_BASELINE * h);
- self.current_x += w * self.window_scale;
- Rectangle { x, y, w, h }
- } else {
- let (w, h) = (sprite.bmp_width as f32, sprite.bmp_height as f32);
- let off_x = glyph.x_offset as f32 / 64.;
- let off_y = glyph.y_offset as f32 / 64.;
- let x = self.current_x + off_x + sprite.bearing_x;
- let y = self.current_y - off_y - sprite.bearing_y;
- let x_advance = glyph.x_advance;
- let y_advance = glyph.y_advance;
- self.current_x += x_advance;
- self.current_y += y_advance;
- // Downscale back again
- Rectangle { x, y, w, h } / self.window_scale
- };
- self.i += 1;
- Some(rect)
- }
- }
- struct TextShaperInternal {
- font_faces: FtFaces,
- cache: TextShaperCache,
- }
- impl TextShaperInternal {
- #[inline]
- fn faces<'a>(&'a mut self) -> &'a mut Vec<FreetypeFace> {
- &mut self.font_faces.0
- }
- #[inline]
- fn face<'a>(&'a mut self, idx: usize) -> &'a mut FreetypeFace {
- &mut self.font_faces.0[idx]
- }
- }
- pub struct TextShaper {
- intern: SyncMutex<TextShaperInternal>,
- fonts_data: Vec<Vec<u8>>,
- }
- impl TextShaper {
- pub fn new() -> Arc<Self> {
- let ftlib = freetype::Library::init().unwrap();
- let mut fonts_data = vec![];
- if let Ok(read_dir) = std::fs::read_dir(custom_font_path()) {
- for entry in read_dir {
- let Ok(entry) = entry else {
- warn!(target: "text", "Skipping unknown in custom font path");
- continue
- };
- let font_path = entry.path();
- if font_path.is_dir() {
- warn!(target: "text", "Skipping {font_path:?} in custom font path: is directory");
- continue
- }
- let Some(font_ext) = font_path.extension().and_then(OsStr::to_str) else {
- warn!(target: "text", "Skipping {font_path:?} in custom font path: missing file extension");
- continue
- };
- if !["ttf", "otf"].contains(&font_ext) {
- warn!(target: "text", "Skipping {font_path:?} in custom font path: unsupported file extension (supported: ttf, otf)");
- continue
- }
- let font_data: Vec<u8> = match std::fs::read(&font_path) {
- Ok(font_data) => font_data,
- Err(err) => {
- warn!(target: "text", "Unexpected error loading {font_path:?} in custom font path: {err}");
- continue
- }
- };
- info!(target: "text", "Loaded custom font: {font_path:?}");
- fonts_data.push(font_data);
- }
- }
- fonts_data.reserve_exact(fonts_data.len() + 2);
- let mut faces = vec![];
- let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8];
- let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
- faces.push(ft_face);
- for font_data in &fonts_data {
- let face = unsafe { Self::load_font_face(&ftlib, font_data) };
- faces.push(face);
- }
- let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8];
- let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
- faces.push(ft_face);
- Arc::new(Self {
- intern: SyncMutex::new(TextShaperInternal {
- font_faces: FtFaces(faces),
- cache: HashMap::new(),
- }),
- fonts_data,
- })
- }
- /// Beware: recasts font_data as static. Make sure data outlives the face.
- unsafe fn load_font_face(ftlib: &freetype::Library, font_data: &[u8]) -> FreetypeFace {
- let font_data = &*(font_data as *const _);
- let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
- ft_face
- }
- pub fn shape(&self, mut text: String, font_size: f32, window_scale: f32) -> Vec<Glyph> {
- //debug!(target: "text", "shape('{}', {})", text, font_size);
- if text.is_empty() {
- return vec![]
- }
- let text = &text;
- // Freetype faces are not threadsafe
- let mut intern = self.intern.lock().unwrap();
- let size = font_size * window_scale;
- for face in intern.faces() {
- set_face_size(face, size);
- }
- let mut glyphs: Vec<Glyph> = vec![];
- 'next_glyph: for glyph_info in shape(intern.faces(), text) {
- let face_idx = glyph_info.face_idx;
- let face = intern.face(face_idx);
- let glyph_id = glyph_info.id;
- let substr = glyph_info.substr(text).to_string();
- let x_offset = glyph_info.x_offset as f32 / 64.;
- let y_offset = glyph_info.y_offset as f32 / 64.;
- let x_advance = glyph_info.x_advance as f32 / 64.;
- let y_advance = glyph_info.y_advance as f32 / 64.;
- // Check cache
- // If it exists in the cache then skip
- // Relevant info:
- // * glyph_id
- // * font_size (for non-fixed size faces)
- // * face_idx
- let cache_key = CacheKey {
- glyph_id,
- font_size: if face.has_fixed_sizes() {
- FontSize::Fixed
- } else {
- FontSize::from((font_size, window_scale))
- },
- face_idx,
- };
- //debug!(target: "text", "cache_key: {:?}", cache_key);
- 'load_sprite: {
- if let Some(sprite) = intern.cache.get(&cache_key) {
- let Some(sprite) = sprite.upgrade() else {
- break 'load_sprite;
- };
- //debug!(target: "text", "found glyph!");
- let glyph = Glyph {
- glyph_id,
- substr,
- sprite,
- x_offset,
- y_offset,
- x_advance,
- y_advance,
- };
- glyphs.push(glyph);
- continue 'next_glyph;
- }
- }
- let face = intern.face(face_idx);
- let Some(sprite) = render_glyph(&face, glyph_id) else { continue };
- let sprite = Arc::new(sprite);
- intern.cache.insert(cache_key, Arc::downgrade(&sprite));
- let glyph =
- Glyph { glyph_id, substr, sprite, x_offset, y_offset, x_advance, y_advance };
- //debug!(target: "text", "pushing glyph...");
- glyphs.push(glyph);
- }
- glyphs
- }
- }
- #[derive(Eq, Hash, PartialEq, Debug)]
- enum FontSize {
- Fixed,
- Size((u32, u32)),
- }
- impl FontSize {
- /// You can't use f32 in Hash and Eq impls
- fn from(size: (f32, f32)) -> Self {
- let font_size = (size.0 * 1000.).round() as u32;
- let scale = (size.1 * 1000.).round() as u32;
- Self::Size((font_size, scale))
- }
- }
- #[derive(Eq, Hash, PartialEq, Debug)]
- struct CacheKey {
- glyph_id: u32,
- font_size: FontSize,
- face_idx: usize,
- }
- #[derive(Clone)]
- pub struct Glyph {
- pub glyph_id: u32,
- // Substring this glyph corresponds to
- pub substr: String,
- pub sprite: SpritePtr,
- // Normally these are i32, we provide the conversions
- pub x_offset: f32,
- pub y_offset: f32,
- pub x_advance: f32,
- pub y_advance: f32,
- }
- impl std::fmt::Debug for Glyph {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Glyph")
- .field("glyph_id", &self.glyph_id)
- .field("substr", &self.substr)
- .finish()
- }
- }
- struct FtFaces(Vec<FreetypeFace>);
- unsafe impl Send for FtFaces {}
- unsafe impl Sync for FtFaces {}
- pub type TextShaperPtr = Arc<TextShaper>;
- type TextShaperCache = HashMap<CacheKey, Weak<Sprite>>;
|