From 94a0db125ecd25970b076ecaf91ab1b016d7ba6f Mon Sep 17 00:00:00 2001 From: jkds Date: Mon, 5 Jan 2026 18:14:43 +0100 Subject: [PATCH] =?UTF-8?q?app:=20delete=20freetype=20=F0=9F=8E=89?= =?UTF-8?q?=F0=9F=8E=89=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/app/Cargo.lock | 121 -------- bin/app/Cargo.toml | 12 - bin/app/src/app/mod.rs | 5 +- bin/app/src/app/schema/settings.rs | 12 +- bin/app/src/main.rs | 8 +- bin/app/src/text/atlas.rs | 224 --------------- bin/app/src/text/ft.rs | 143 ---------- bin/app/src/text/mod.rs | 369 ------------------------ bin/app/src/text/old_atlas.rs | 132 --------- bin/app/src/text/shape.rs | 434 ----------------------------- bin/app/src/text/wrap.rs | 295 -------------------- 11 files changed, 8 insertions(+), 1747 deletions(-) delete mode 100644 bin/app/src/text/atlas.rs delete mode 100644 bin/app/src/text/ft.rs delete mode 100644 bin/app/src/text/mod.rs delete mode 100644 bin/app/src/text/old_atlas.rs delete mode 100644 bin/app/src/text/shape.rs delete mode 100644 bin/app/src/text/wrap.rs diff --git a/bin/app/Cargo.lock b/bin/app/Cargo.lock index 6f0bf112f..f95d2abe3 100644 --- a/bin/app/Cargo.lock +++ b/bin/app/Cargo.lock @@ -1251,58 +1251,12 @@ dependencies = [ "futures", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "core-text" -version = "20.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" -dependencies = [ - "core-foundation", - "core-graphics", - "foreign-types", - "libc", -] - [[package]] name = "core2" version = "0.4.0" @@ -1562,11 +1516,9 @@ dependencies = [ "easy-parallel", "file-rotate", "fluent", - "freetype-rs", "fud", "futures", "glam", - "harfbuzz-sys", "image", "indoc", "miniquad", @@ -2450,33 +2402,6 @@ dependencies = [ "yeslogic-fontconfig-sys", ] -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.112", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2486,26 +2411,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "freetype-rs" -version = "0.37.0" -source = "git+https://github.com/narodnik/freetype-rs#bad2e69dc20d35f33eaee8cd26a2dbf588026ea8" -dependencies = [ - "bitflags 2.10.0", - "freetype-sys", - "libc", -] - -[[package]] -name = "freetype-sys" -version = "0.22.1" -source = "git+https://github.com/narodnik/freetype-sys2#ed8af21a697670e06fc5f072c5b2276ebe7f76ec" -dependencies = [ - "cc", - "libz-sys", - "pkg-config", -] - [[package]] name = "fs-mistrust" version = "0.13.1" @@ -2890,20 +2795,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "harfbuzz-sys" -version = "0.6.1" -source = "git+https://github.com/narodnik/rust-harfbuzz2#450d37dad132906335ec760cd51463a1ede359ee" -dependencies = [ - "cc", - "core-graphics", - "core-text", - "foreign-types", - "freetype-sys", - "pkg-config", - "windows 0.58.0", -] - [[package]] name = "harfrust" version = "0.3.2" @@ -3580,18 +3471,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linebender_resource_handle" version = "0.1.1" diff --git a/bin/app/Cargo.toml b/bin/app/Cargo.toml index b9d4db911..04939508b 100644 --- a/bin/app/Cargo.toml +++ b/bin/app/Cargo.toml @@ -11,12 +11,6 @@ repository = "https://codeberg.org/darkrenaissance/darkfi" [dependencies] miniquad = { git = "https://github.com/not-fl3/miniquad" } -# Currently latest version links to freetype-sys 0.19 but we use 0.21 -#harfbuzz-sys = "0.6.1" -#harfbuzz-sys = { git = "https://github.com/servo/rust-harfbuzz", features = ["bundled"] } -harfbuzz-sys = { git = "https://github.com/narodnik/rust-harfbuzz2", features = ["bundled"] } -freetype-rs = { version = "0.37.0", features = ["bundled"] } - image = "0.25.9" qoi = "0.4.1" @@ -84,12 +78,6 @@ schema-app = [] schema-test = [] [patch.crates-io] -# We can remove these patches. But unfortunately harfbuzz-sys is still linking -# the old freetype libs so we need to fix that first. -# Fucking servo rust-harfbuzz -freetype-rs = { git = "https://github.com/narodnik/freetype-rs" } -freetype-sys = { git = "https://github.com/narodnik/freetype-sys2" } - halo2_proofs = { git="https://github.com/parazyd/halo2", branch="v032" } halo2_gadgets = { git="https://github.com/parazyd/halo2", branch="v032" } # This patch didn't work for me diff --git a/bin/app/src/app/mod.rs b/bin/app/src/app/mod.rs index 7bcca61a8..e09185213 100644 --- a/bin/app/src/app/mod.rs +++ b/bin/app/src/app/mod.rs @@ -31,7 +31,6 @@ use crate::{ plugin::PluginSettings, prop::{Property, PropertyAtomicGuard, PropertySubType, PropertyType, PropertyValue, Role}, scene::{Pimpl, SceneNode, SceneNodePtr, SceneNodeType}, - text::TextShaperPtr, ui::{chatview, Window}, util::i18n::I18nBabelFish, ExecutorPtr, @@ -59,7 +58,6 @@ pub type AppPtr = Arc; pub struct App { pub sg_root: SceneNodePtr, pub render_api: RenderApi, - pub text_shaper: TextShaperPtr, pub tasks: SyncMutex>>, pub ex: ExecutorPtr, } @@ -68,10 +66,9 @@ impl App { pub fn new( sg_root: SceneNodePtr, render_api: RenderApi, - text_shaper: TextShaperPtr, ex: ExecutorPtr, ) -> Arc { - Arc::new(Self { sg_root, ex, render_api, text_shaper, tasks: SyncMutex::new(vec![]) }) + Arc::new(Self { sg_root, ex, render_api, tasks: SyncMutex::new(vec![]) }) } /// Does not require miniquad to be init. Created the scene graph tree / schema and all diff --git a/bin/app/src/app/schema/settings.rs b/bin/app/src/app/schema/settings.rs index 491373f0c..d4b90735f 100644 --- a/bin/app/src/app/schema/settings.rs +++ b/bin/app/src/app/schema/settings.rs @@ -26,6 +26,7 @@ use crate::{ scene::{SceneNodePtr, Slot}, shape, ui::{Button, EditBox, Layer, ShapeVertex, Text, VectorArt, VectorShape}, + util::i18n::I18nBabelFish, ExecutorPtr, }; @@ -134,7 +135,7 @@ impl Setting { } } -pub async fn make(app: &App, window: SceneNodePtr, _ex: ExecutorPtr) { +pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) { let mut cc = Compiler::new(); cc.add_const_f32("BORDER_RIGHT_SCALE", BORDER_RIGHT_SCALE); cc.add_const_f32("SEARCH_PADDING_X", SEARCH_PADDING_X); @@ -281,8 +282,7 @@ pub async fn make(app: &App, window: SceneNodePtr, _ex: ExecutorPtr) { me, window_scale.clone(), app.render_api.clone(), - app.text_shaper.clone(), - app.ex.clone(), + i18n_fish.clone(), ) }) .await; @@ -383,8 +383,7 @@ pub async fn make(app: &App, window: SceneNodePtr, _ex: ExecutorPtr) { me, window_scale.clone(), app.render_api.clone(), - app.text_shaper.clone(), - app.ex.clone(), + i18n_fish.clone(), ) }) .await; @@ -410,8 +409,7 @@ pub async fn make(app: &App, window: SceneNodePtr, _ex: ExecutorPtr) { me, window_scale.clone(), app.render_api.clone(), - app.text_shaper.clone(), - app.ex.clone(), + i18n_fish.clone(), ) }) .await; diff --git a/bin/app/src/main.rs b/bin/app/src/main.rs index c7d316554..34e4962f2 100644 --- a/bin/app/src/main.rs +++ b/bin/app/src/main.rs @@ -45,7 +45,6 @@ mod pubsub; //mod ringbuf; mod scene; mod shape; -mod text; mod text2; mod ui; mod util; @@ -55,7 +54,6 @@ use crate::{ gfx::EpochIndex, prop::{Property, PropertySubType, PropertyType}, scene::{CallArgType, SceneNode, SceneNodeType}, - text::TextShaper, util::AsyncRuntime, }; #[cfg(feature = "enable-netdebug")] @@ -152,9 +150,7 @@ impl God { let render_api = gfx::RenderApi::new(method_send); let event_pub = gfx::GraphicsEventPublisher::new(); - let text_shaper = TextShaper::new(); - - let app = App::new(sg_root.clone(), render_api.clone(), text_shaper, fg_ex.clone()); + let app = App::new(sg_root.clone(), render_api.clone(), fg_ex.clone()); let app2 = app.clone(); let cv_app_is_setup = Arc::new(CondVar::new()); @@ -482,7 +478,7 @@ fn main() { GOD.get_or_init(God::new); - // Reuse render_api, event_pub and text_shaper + // Reuse render_api and event_pub // No need for setup(), just wait for gfx start then call .start() // ZMQ, darkirc stay running diff --git a/bin/app/src/text/atlas.rs b/bin/app/src/text/atlas.rs deleted file mode 100644 index 9b29aa47d..000000000 --- a/bin/app/src/text/atlas.rs +++ /dev/null @@ -1,224 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 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 . - */ - -use crate::gfx::{DebugTag, ManagedTexturePtr, Rectangle, RenderApi}; -use miniquad::TextureFormat; - -use super::{ - ft::{Sprite, SpritePtr}, - Glyph, -}; - -/// Prevents render artifacts from aliasing. -/// Even with aliasing turned off, some bleed still appears possibly -/// due to UV coord calcs. Adding a gap perfectly fixes this. -const ATLAS_GAP: usize = 2; - -/// Convenience wrapper fn. Use if rendering a single line of glyphs. -pub fn make_texture_atlas( - render_api: &RenderApi, - tag: DebugTag, - glyphs: &Vec, -) -> RenderedAtlas { - let mut atlas = Atlas::new(render_api, tag); - atlas.push(&glyphs); - atlas.make() -} - -/// Responsible for aggregating glyphs, and then producing a single software -/// blitted texture usable in a single draw call. -/// This makes OpenGL batch precomputation of meshes efficient. -/// -/// ```rust -/// let mut atlas = Atlas::new(&render_api); -/// atlas.push(&glyphs); // repeat as needed for shaped lines -/// let atlas = atlas.make().unwrap(); -/// let uv = atlas.fetch_uv(glyph_id).unwrap(); -/// let atlas_texture_id = atlas.texture_id; -/// ``` -#[derive(Clone)] -pub struct Atlas<'a> { - glyph_ids: Vec, - sprites: Vec, - // LHS x pos of glyph - x_pos: Vec, - - width: usize, - height: usize, - - render_api: &'a RenderApi, - tag: DebugTag, -} - -impl<'a> Atlas<'a> { - pub fn new(render_api: &'a RenderApi, tag: DebugTag) -> Self { - Self { - glyph_ids: vec![], - sprites: vec![], - x_pos: vec![], - width: ATLAS_GAP, - // Not really important to set a value here since it will - // get overwritten. - // FYI glyphs have a gap on all sides (top and bottom here). - height: 2 * ATLAS_GAP, - render_api, - tag, - } - } - - fn push_glyph(&mut self, glyph: &Glyph) { - if self.glyph_ids.contains(&glyph.glyph_id) { - return - } - - self.glyph_ids.push(glyph.glyph_id); - self.sprites.push(glyph.sprite.clone()); - - let sprite = &glyph.sprite; - self.x_pos.push(self.width); - - // Gap on the top and bottom - let height = ATLAS_GAP + sprite.bmp_height + ATLAS_GAP; - self.height = std::cmp::max(height, self.height); - - // Gap between glyphs and on both sides - self.width += sprite.bmp_width + ATLAS_GAP; - } - - /// Push a line of shaped text represented as `Vec` - /// to this atlas. - pub fn push(&mut self, glyphs: &Vec) { - for glyph in glyphs { - self.push_glyph(glyph); - } - } - - fn render(&self) -> Vec { - let mut atlas = vec![0; 4 * self.width * self.height]; - // For drawing debug lines we want a single white pixel. - // This is very useful to have in our texture for debugging. - atlas[0] = 255; - atlas[1] = 255; - atlas[2] = 255; - atlas[3] = 255; - - let y = ATLAS_GAP; - // Copy all the sprites to our atlas. - // They should have ATLAS_GAP spacing on all sides to avoid bleeding. - for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) { - copy_image(sprite, *x, y, &mut atlas, self.width); - } - - atlas - } - - fn compute_uvs(&self) -> Vec { - // UV coords are in the range [0, 1] - let mut uvs = vec![]; - - let (self_w, self_h) = (self.width as f32, self.height as f32); - let y = ATLAS_GAP as f32; - - for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) { - let x = *x as f32; - let sprite_w = sprite.bmp_width as f32; - let sprite_h = sprite.bmp_height as f32; - - let uv = Rectangle { - x: x / self_w, - y: y / self_h, - w: sprite_w / self_w, - h: sprite_h / self_h, - }; - uvs.push(uv); - } - - uvs - } - - /// Invalidate this atlas and produce the finalized result. - /// Each glyph is given a sub-rect within the texture, accessible by calling - /// `rendered_atlas.fetch_uv(my_glyph_id)`. - /// The texture ID is a struct member: `rendered_atlas.texture_id`. - pub fn make(self) -> RenderedAtlas { - //if self.glyph_ids.is_empty() { - // return Err(Error::AtlasIsEmpty) - //} - - assert_eq!(self.glyph_ids.len(), self.sprites.len()); - assert_eq!(self.glyph_ids.len(), self.x_pos.len()); - - let atlas = self.render(); - let texture = self.render_api.new_texture( - self.width as u16, - self.height as u16, - atlas, - TextureFormat::RGBA8, - self.tag, - ); - - let uv_rects = self.compute_uvs(); - let glyph_ids = self.glyph_ids; - - RenderedAtlas { glyph_ids, uv_rects, texture } - } -} - -/// Copy a sprite to (x, y) position within the atlas texture. -/// Both image formats are RGBA flat vecs. -fn copy_image(sprite: &Sprite, x: usize, y: usize, atlas: &mut Vec, atlas_width: usize) { - for i in 0..sprite.bmp_height { - for j in 0..sprite.bmp_width { - let src_y = i * sprite.bmp_width; - let off_src = 4 * (src_y + j); - - let dest_y = (y + i) * atlas_width; - let off_dest = 4 * (dest_y + j + x); - - atlas[off_dest] = sprite.bmp[off_src]; - atlas[off_dest + 1] = sprite.bmp[off_src + 1]; - atlas[off_dest + 2] = sprite.bmp[off_src + 2]; - atlas[off_dest + 3] = sprite.bmp[off_src + 3]; - } - } -} - -/// Final result computed from `Atlas::make()`. -#[derive(Clone)] -pub struct RenderedAtlas { - glyph_ids: Vec, - /// UV rectangle within the texture. - uv_rects: Vec, - /// Allocated atlas texture. - pub texture: ManagedTexturePtr, -} - -impl RenderedAtlas { - /// Get UV coords for a glyph within the rendered atlas. - pub fn fetch_uv(&self, glyph_id: u32) -> Option<&Rectangle> { - let glyphs_len = self.glyph_ids.len(); - assert_eq!(glyphs_len, self.uv_rects.len()); - - for i in 0..glyphs_len { - if self.glyph_ids[i] == glyph_id { - return Some(&self.uv_rects[i]) - } - } - None - } -} diff --git a/bin/app/src/text/ft.rs b/bin/app/src/text/ft.rs deleted file mode 100644 index 5601a5826..000000000 --- a/bin/app/src/text/ft.rs +++ /dev/null @@ -1,143 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 Dyne.org foundation - * - * This program is free sofreetypeware: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Sofreetypeware 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 . - */ - -use freetype::face::LoadFlag as FtLoadFlag; -use std::sync::Arc; - -pub type FreetypeFace = freetype::Face<&'static [u8]>; - -pub type SpritePtr = Arc; - -pub struct Sprite { - pub bmp: Vec, - pub bmp_width: usize, - pub bmp_height: usize, - - pub bearing_x: f32, - pub bearing_y: f32, - pub has_fixed_sizes: bool, - pub has_color: bool, -} - -fn load_ft_glyph<'a>( - face: &'a FreetypeFace, - glyph_id: u32, - flags: FtLoadFlag, -) -> Option<&'a freetype::GlyphSlot> { - //debug!("load_glyph {} flags={flags:?}", glyph_id); - if let Err(err) = face.load_glyph(glyph_id, flags) { - error!(target: "text", "error loading glyph {glyph_id}: {err}"); - return None - } - //debug!("load_glyph {} [done]", glyph_id); - - // https://gist.github.com/jokertarot/7583938?permalink_comment_id=3327566#gistcomment-3327566 - - let glyph = face.glyph(); - glyph.render_glyph(freetype::RenderMode::Normal).ok()?; - Some(glyph) -} - -pub fn render_glyph(face: &FreetypeFace, glyph_id: u32) -> Option { - // If color is available then attempt to load it. - // Otherwise fallback to black and white. - let glyph = if face.has_color() { - match load_ft_glyph(face, glyph_id, FtLoadFlag::DEFAULT | FtLoadFlag::COLOR) { - Some(glyph) => glyph, - None => load_ft_glyph(face, glyph_id, FtLoadFlag::DEFAULT)?, - } - } else { - load_ft_glyph(face, glyph_id, FtLoadFlag::DEFAULT)? - }; - - let bmp = glyph.bitmap(); - let buffer = bmp.buffer(); - let bmp_width = bmp.width() as usize; - let bmp_height = bmp.rows() as usize; - let bearing_x = glyph.bitmap_left() as f32; - let bearing_y = glyph.bitmap_top() as f32; - let has_fixed_sizes = face.has_fixed_sizes(); - - let pixel_mode = bmp.pixel_mode().unwrap(); - let bmp = match pixel_mode { - freetype::bitmap::PixelMode::Bgra => { - let mut tdata = vec![]; - tdata.resize(4 * bmp_width * bmp_height, 0); - // Convert from BGRA to RGBA - for i in 0..bmp_width * bmp_height { - let idx = i * 4; - let b = buffer[idx]; - let g = buffer[idx + 1]; - let r = buffer[idx + 2]; - let a = buffer[idx + 3]; - tdata[idx] = r; - tdata[idx + 1] = g; - tdata[idx + 2] = b; - tdata[idx + 3] = a; - } - tdata - } - freetype::bitmap::PixelMode::Gray => { - // Convert from greyscale to RGBA8 - let tdata: Vec<_> = - buffer.iter().flat_map(|coverage| vec![255, 255, 255, *coverage]).collect(); - tdata - } - freetype::bitmap::PixelMode::Mono => { - // Convert from mono to RGBA8 - let tdata: Vec<_> = - buffer.iter().flat_map(|coverage| vec![255, 255, 255, *coverage]).collect(); - tdata - } - _ => panic!("unsupport pixel mode: {:?}", pixel_mode), - }; - - Some(Sprite { - bmp, - bmp_width, - bmp_height, - bearing_x, - bearing_y, - has_fixed_sizes, - has_color: face.has_color(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn render_simple() { - let ftlib = freetype::Library::init().unwrap(); - let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8]; - let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - - // glyph 11 in IBM plex mono regular is 'h' - let glyph = render_glyph(&face, 11).unwrap(); - } - - #[test] - fn render_custom_glyph() { - let ftlib = freetype::Library::init().unwrap(); - let font_data = include_bytes!("../../darkirc-emoji-svg.ttf") as &[u8]; - let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - - let glyph = render_glyph(&face, 4).unwrap(); - } -} diff --git a/bin/app/src/text/mod.rs b/bin/app/src/text/mod.rs deleted file mode 100644 index 12aa9cfe0..000000000 --- a/bin/app/src/text/mod.rs +++ /dev/null @@ -1,369 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 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 . - */ - -use std::{ - collections::HashMap, - ffi::OsStr, - 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::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, - current_x: f32, - current_y: f32, - i: usize, -} - -impl<'a> GlyphPositionIter<'a> { - pub fn new(font_size: f32, window_scale: f32, glyphs: &'a Vec, 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 { - 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 { - &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, - _fonts_data: Vec>, -} - -impl TextShaper { - pub fn new() -> Arc { - 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 = 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: 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, text: String, font_size: f32, window_scale: f32) -> Vec { - //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 = 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); - -unsafe impl Send for FtFaces {} -unsafe impl Sync for FtFaces {} - -pub type TextShaperPtr = Arc; - -type TextShaperCache = HashMap>; diff --git a/bin/app/src/text/old_atlas.rs b/bin/app/src/text/old_atlas.rs deleted file mode 100644 index 12ab91ea1..000000000 --- a/bin/app/src/text/old_atlas.rs +++ /dev/null @@ -1,132 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 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 . - */ - -use miniquad::{TextureFormat, TextureId}; - -use crate::{ - error::Result, - gfx::{Rectangle, RenderApi}, - util::ansi_texture, -}; - -use super::{Glyph, Sprite}; - -pub struct RenderedAtlas { - pub uv_rects: Vec, - pub texture_id: GfxTextureId, -} - -const ATLAS_GAP: usize = 2; - -pub async fn make_texture_atlas( - render_api: &RenderApi, - font_size: f32, - glyphs: &Vec, -) -> Result { - // First compute total size of the atlas - let mut total_width = ATLAS_GAP; - let mut total_height = ATLAS_GAP; - - // Glyph IDs already rendered so we don't do it twice - let mut rendered = vec![]; - - for (idx, glyph) in glyphs.iter().enumerate() { - let sprite = &glyph.sprite; - assert_eq!(sprite.bmp.len(), 4 * sprite.bmp_width * sprite.bmp_height); - - // Already done this one so skip - if rendered.contains(&glyph.glyph_id) { - continue - } - rendered.push(glyph.glyph_id); - - total_width += sprite.bmp_width + ATLAS_GAP; - total_height = std::cmp::max(total_height, sprite.bmp_height); - } - total_width += ATLAS_GAP; - total_height += 2 * ATLAS_GAP; - - // Allocate the big texture now - let mut atlas_bmp = vec![0; 4 * total_width * total_height]; - // For debug lines we want a single white pixel. - atlas_bmp[0] = 255; - atlas_bmp[1] = 255; - atlas_bmp[2] = 255; - atlas_bmp[3] = 255; - - // Calculate dimensions of final product first - let mut current_x = ATLAS_GAP; - let mut rendered_glyphs: Vec = vec![]; - let mut uv_rects: Vec = vec![]; - - for (idx, glyph) in glyphs.iter().enumerate() { - let sprite = &glyph.sprite; - - // Did we already rendered this glyph? - // If so just copy the UV rect from before. - let mut uv_rect = None; - for (rendered_glyph_id, rendered_uv_rect) in rendered_glyphs.iter().zip(uv_rects.iter()) { - if *rendered_glyph_id == glyph.glyph_id { - uv_rect = Some(rendered_uv_rect.clone()); - } - } - - let uv_rect = match uv_rect { - Some(uv_rect) => uv_rect, - // Allocating a new glyph sprite in the atlas - None => { - copy_image(sprite, &mut atlas_bmp, total_width, current_x); - - // Compute UV coords - let uv_rect = Rectangle { - x: (ATLAS_GAP + current_x) as f32 / total_width as f32, - y: ATLAS_GAP as f32 / total_height as f32, - w: sprite.bmp_width as f32 / total_width as f32, - h: sprite.bmp_height as f32 / total_height as f32, - }; - - current_x += sprite.bmp_width + ATLAS_GAP; - - uv_rect - } - }; - - rendered_glyphs.push(glyph.glyph_id); - uv_rects.push(uv_rect); - } - - // Finally allocate the texture - let texture_id = render_api - .new_texture(total_width as u16, total_height as u16, atlas_bmp, TextureFormat::RGBA8) - .await?; - - Ok(RenderedAtlas { uv_rects, texture_id }) -} - -fn copy_image(sprite: &Sprite, atlas_bmp: &mut Vec, total_width: usize, current_x: usize) { - for i in 0..sprite.bmp_height { - for j in 0..sprite.bmp_width { - let off_dest = 4 * ((i + ATLAS_GAP) * total_width + j + current_x + ATLAS_GAP); - let off_src = 4 * (i * sprite.bmp_width + j); - atlas_bmp[off_dest] = sprite.bmp[off_src]; - atlas_bmp[off_dest + 1] = sprite.bmp[off_src + 1]; - atlas_bmp[off_dest + 2] = sprite.bmp[off_src + 2]; - atlas_bmp[off_dest + 3] = sprite.bmp[off_src + 3]; - } - } -} diff --git a/bin/app/src/text/shape.rs b/bin/app/src/text/shape.rs deleted file mode 100644 index 4e0641830..000000000 --- a/bin/app/src/text/shape.rs +++ /dev/null @@ -1,434 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 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 . - */ - -use freetype as ft; -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_buffer_t, hb_font_destroy, hb_font_t, hb_glyph_info_t, hb_glyph_position_t, hb_shape, - HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES, HB_BUFFER_CONTENT_TYPE_UNICODE, -}; -use std::{iter::Peekable, os, vec::IntoIter}; - -type FreetypeFace = ft::Face<&'static [u8]>; - -struct HarfBuzzInfo<'a> { - info: &'a hb_glyph_info_t, - pos: &'a hb_glyph_position_t, -} - -struct HarfBuzzIter<'a> { - hb_font: *mut hb_font_t, - buf: *mut hb_buffer_t, - infos_iter: std::slice::Iter<'a, hb_glyph_info_t>, - pos_iter: std::slice::Iter<'a, hb_glyph_position_t>, -} - -impl<'a> Iterator for HarfBuzzIter<'a> { - type Item = HarfBuzzInfo<'a>; - fn next(&mut self) -> Option { - let info = self.infos_iter.next()?; - let pos = self.pos_iter.next()?; - Some(HarfBuzzInfo { info, pos }) - } -} - -impl<'a> Drop for HarfBuzzIter<'a> { - fn drop(&mut self) { - unsafe { - hb_buffer_destroy(self.buf); - hb_font_destroy(self.hb_font); - } - } -} - -pub(super) fn set_face_size(face: &mut FreetypeFace, size: f32) { - if face.has_fixed_sizes() { - //debug!(target: "text", "fixed sizes"); - // emojis required a fixed size - //face.set_char_size(109 * 64, 0, 72, 72).unwrap(); - face.select_size(0).unwrap(); - } else { - //debug!(target: "text", "set char size"); - face.set_char_size(size as isize * 64, 0, 96, 96).unwrap(); - } -} - -fn harfbuzz_shape<'a>(face: &mut FreetypeFace, text: &str) -> HarfBuzzIter<'a> { - let utf8_ptr = text.as_ptr() as *const _; - // https://harfbuzz.github.io/a-simple-shaping-example.html - let (hb_font, buf, glyph_infos, glyph_pos) = unsafe { - let ft_face_ptr: freetype::freetype_sys::FT_Face = face.raw_mut(); - let hb_font = hb_ft_font_create_referenced(ft_face_ptr); - let buf = hb_buffer_create(); - hb_buffer_set_content_type(buf, HB_BUFFER_CONTENT_TYPE_UNICODE); - hb_buffer_set_cluster_level(buf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES); - hb_buffer_add_utf8( - buf, - utf8_ptr, - text.len() as os::raw::c_int, - 0 as os::raw::c_uint, - text.len() as os::raw::c_int, - ); - hb_buffer_guess_segment_properties(buf); - hb_shape(hb_font, buf, std::ptr::null(), 0 as os::raw::c_uint); - - let mut length: u32 = 0; - let glyph_infos = hb_buffer_get_glyph_infos(buf, &mut length as *mut u32); - let glyph_infos: &[hb_glyph_info_t] = - std::slice::from_raw_parts(glyph_infos as *const _, length as usize); - - let glyph_pos = hb_buffer_get_glyph_positions(buf, &mut length as *mut u32); - let glyph_pos: &[hb_glyph_position_t] = - std::slice::from_raw_parts(glyph_pos as *const _, length as usize); - - (hb_font, buf, glyph_infos, glyph_pos) - }; - - let infos_iter = glyph_infos.iter(); - let pos_iter = glyph_pos.iter(); - - HarfBuzzIter { hb_font, buf, infos_iter, pos_iter } -} - -pub(super) struct GlyphInfo { - pub face_idx: usize, - pub id: u32, - pub cluster_start: usize, - pub cluster_end: usize, - pub x_offset: i32, - pub y_offset: i32, - pub x_advance: i32, - pub y_advance: i32, -} - -impl GlyphInfo { - pub fn substr<'a>(&self, text: &'a str) -> &'a str { - // RTL - let start = std::cmp::min(self.cluster_start, self.cluster_end); - let end = std::cmp::max(self.cluster_start, self.cluster_end); - &text[start..end] - } -} - -struct ShapedGlyphs { - glyphs: Vec, -} - -impl ShapedGlyphs { - fn new(glyphs: Vec) -> Self { - Self { glyphs } - } - - fn has_zero(&self) -> bool { - self.glyphs.iter().any(|g| g.id == 0) - } - - fn fill_zeros(&mut self, fallback: Vec) { - let mut primary_iter = std::mem::take(&mut self.glyphs).into_iter().peekable(); - let mut fallback_iter = fallback.into_iter().peekable(); - assert!(self.glyphs.is_empty()); - - while let Some(primary_glyph) = primary_iter.next() { - if primary_glyph.id != 0 { - Self::consume(&mut fallback_iter, primary_glyph.cluster_start); - self.glyphs.push(primary_glyph); - continue - } - - let mut fallbacks = Self::consume(&mut fallback_iter, primary_glyph.cluster_start); - - let Some(last_fallback) = fallbacks.last() else { continue }; - let cluster_end = last_fallback.cluster_end; - - self.glyphs.append(&mut fallbacks); - - Self::drop_replaced(&mut primary_iter, cluster_end); - } - } - - fn consume(iter: &mut Peekable>, cluster_bound: usize) -> Vec { - let mut consumed = vec![]; - while let Some(glyph) = iter.peek() { - if glyph.cluster_start > cluster_bound { - break - } - - let glyph = iter.next().unwrap(); - consumed.push(glyph); - } - consumed - } - fn drop_replaced(iter: &mut Peekable>, cluster_end: usize) { - while let Some(glyph) = iter.peek() { - if glyph.cluster_start >= cluster_end { - break - } - let _ = iter.next(); - } - } -} - -/* -fn print_glyphs(ctx: &str, glyphs: &Vec, indent: usize) { - let ws = " ".repeat(2 * indent); - println!("{ws}{} ------------------", ctx); - for (i, glyph) in glyphs.iter().enumerate() { - println!( - "{ws}{i}: {}/{} [{}, {}]", - glyph.face_idx, glyph.id, glyph.cluster_start, glyph.cluster_end - ); - } - println!("{ws}---------------------"); -} -*/ - -fn face_shape(face: &mut FreetypeFace, text: &str, face_idx: usize) -> Vec { - let mut glyphs: Vec = vec![]; - for (i, hbinf) in harfbuzz_shape(face, text).enumerate() { - let glyph_id = hbinf.info.codepoint as u32; - // Index within this substr - let cluster = hbinf.info.cluster as usize; - //println!(" {i}: glyph_id = {glyph_id}, cluster = {cluster}"); - - if i != 0 { - glyphs.last_mut().unwrap().cluster_end = cluster; - } - - glyphs.push(GlyphInfo { - face_idx, - id: glyph_id, - cluster_start: cluster, - cluster_end: 0, - x_offset: hbinf.pos.x_offset, - y_offset: hbinf.pos.y_offset, - x_advance: hbinf.pos.x_advance, - y_advance: hbinf.pos.y_advance, - }); - } - if let Some(last) = glyphs.last_mut() { - last.cluster_end = text.len(); - } - glyphs -} - -/// Shape text using fallback fonts. We shape it using the primary font, then go down through -/// the list of fallbacks. For every zero we encounter, take the remaining text on that line -/// and try to shape it. Then replace that glyph + any others in the cluster with the new one. -/// [More info](https://zachbayl.in/blog/font_fallback_revery/) -pub(super) fn shape(faces: &mut Vec, text: &str) -> Vec { - let glyphs = face_shape(&mut faces[0], text, 0); - let mut shaped = ShapedGlyphs::new(glyphs); - - // Go down successively in our fallbacks - for face_idx in 1..faces.len() { - if !shaped.has_zero() { - break - } - - let glyphs = face_shape(&mut faces[face_idx], text, face_idx); - shaped.fill_zeros(glyphs); - } - shaped.glyphs -} - -#[cfg(test)] -mod tests { - use super::*; - - fn load_faces() -> Vec { - let ftlib = freetype::Library::init().unwrap(); - - let mut faces = vec![]; - let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8]; - let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - faces.push(face); - - let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8]; - let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - faces.push(face); - - //let font_data = include_bytes!("../noto-serif-cjk-jp-regular.otf") as &[u8]; - //let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - //faces.push(face); - - faces - } - - #[test] - fn simple_shape_test() { - let mut faces = load_faces(); - let text = "\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}"; - let glyphs = shape(&mut faces, text); - - assert_eq!(glyphs.len(), 1); - assert_eq!(glyphs[0].face_idx, 1); - assert_eq!(glyphs[0].id, 1895); - assert_eq!(glyphs[0].cluster_start, 0); - assert_eq!(glyphs[0].cluster_end, 16); - } - - #[test] - fn simple_double_shape_test() { - let mut faces = load_faces(); - let text = - "\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}"; - let glyphs = shape(&mut faces, text); - - assert_eq!(glyphs.len(), 2); - assert_eq!(glyphs[0].face_idx, 1); - assert_eq!(glyphs[0].id, 1895); - assert_eq!(glyphs[0].cluster_start, 0); - assert_eq!(glyphs[0].cluster_end, 16); - assert_eq!(glyphs[1].face_idx, 1); - assert_eq!(glyphs[1].id, 1895); - assert_eq!(glyphs[1].cluster_start, 16); - assert_eq!(glyphs[1].cluster_end, 32); - } - - #[test] - fn mixed_shape_test() { - //let text = "日本語"; - //let text = "hel 日本語\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f} ally"; - - let mut faces = load_faces(); - let text = "hel \u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f} 123 X\u{01f44d}\u{01f3fe}X br"; - let glyphs = shape(&mut faces, text); - - assert_eq!(glyphs[0].face_idx, 0); - assert_eq!(glyphs[0].id, 11); - assert_eq!(glyphs[0].cluster_start, 0); - assert_eq!(glyphs[0].cluster_end, 1); - assert_eq!(glyphs[1].face_idx, 0); - assert_eq!(glyphs[1].id, 6); - assert_eq!(glyphs[1].cluster_start, 1); - assert_eq!(glyphs[1].cluster_end, 2); - assert_eq!(glyphs[1].cluster_start, glyphs[0].cluster_end); - assert_eq!(glyphs[2].face_idx, 0); - assert_eq!(glyphs[2].id, 15); - assert_eq!(glyphs[2].cluster_start, 2); - assert_eq!(glyphs[2].cluster_end, 3); - assert_eq!(glyphs[2].cluster_start, glyphs[1].cluster_end); - assert_eq!(glyphs[3].face_idx, 0); - assert_eq!(glyphs[3].id, 1099); - assert_eq!(glyphs[3].cluster_start, 3); - assert_eq!(glyphs[3].cluster_end, 4); - assert_eq!(glyphs[3].cluster_start, glyphs[2].cluster_end); - assert_eq!(glyphs[4].face_idx, 1); - assert_eq!(glyphs[4].id, 1895); - assert_eq!(glyphs[4].cluster_start, 4); - assert_eq!(glyphs[4].cluster_end, 20); - assert_eq!(glyphs[4].cluster_start, glyphs[3].cluster_end); - assert_eq!(glyphs[5].face_idx, 0); - assert_eq!(glyphs[5].id, 1099); - assert_eq!(glyphs[5].cluster_start, 20); - assert_eq!(glyphs[5].cluster_end, 21); - assert_eq!(glyphs[5].cluster_start, glyphs[4].cluster_end); - assert_eq!(glyphs[6].face_idx, 0); - assert_eq!(glyphs[6].id, 59); - assert_eq!(glyphs[6].cluster_start, 21); - assert_eq!(glyphs[6].cluster_end, 22); - assert_eq!(glyphs[6].cluster_start, glyphs[5].cluster_end); - assert_eq!(glyphs[7].face_idx, 0); - assert_eq!(glyphs[7].id, 60); - assert_eq!(glyphs[7].cluster_start, 22); - assert_eq!(glyphs[7].cluster_end, 23); - assert_eq!(glyphs[7].cluster_start, glyphs[6].cluster_end); - assert_eq!(glyphs[8].face_idx, 0); - assert_eq!(glyphs[8].id, 61); - assert_eq!(glyphs[8].cluster_start, 23); - assert_eq!(glyphs[8].cluster_end, 24); - assert_eq!(glyphs[8].cluster_start, glyphs[7].cluster_end); - assert_eq!(glyphs[9].face_idx, 0); - assert_eq!(glyphs[9].id, 1099); - assert_eq!(glyphs[9].cluster_start, 24); - assert_eq!(glyphs[9].cluster_end, 25); - assert_eq!(glyphs[9].cluster_start, glyphs[8].cluster_end); - assert_eq!(glyphs[10].face_idx, 0); - assert_eq!(glyphs[10].id, 53); - assert_eq!(glyphs[10].cluster_start, 25); - assert_eq!(glyphs[10].cluster_end, 26); - assert_eq!(glyphs[10].cluster_start, glyphs[9].cluster_end); - assert_eq!(glyphs[11].face_idx, 1); - assert_eq!(glyphs[11].id, 1955); - assert_eq!(glyphs[11].cluster_start, 26); - assert_eq!(glyphs[11].cluster_end, 34); - assert_eq!(glyphs[11].cluster_start, glyphs[10].cluster_end); - assert_eq!(glyphs[12].face_idx, 0); - assert_eq!(glyphs[12].id, 53); - assert_eq!(glyphs[12].cluster_start, 34); - assert_eq!(glyphs[12].cluster_end, 35); - assert_eq!(glyphs[12].cluster_start, glyphs[11].cluster_end); - assert_eq!(glyphs[13].face_idx, 0); - assert_eq!(glyphs[13].id, 1099); - assert_eq!(glyphs[13].cluster_start, 35); - assert_eq!(glyphs[13].cluster_end, 36); - assert_eq!(glyphs[13].cluster_start, glyphs[12].cluster_end); - assert_eq!(glyphs[14].face_idx, 0); - assert_eq!(glyphs[14].id, 3); - assert_eq!(glyphs[14].cluster_start, 36); - assert_eq!(glyphs[14].cluster_end, 37); - assert_eq!(glyphs[14].cluster_start, glyphs[13].cluster_end); - assert_eq!(glyphs[15].face_idx, 0); - assert_eq!(glyphs[15].id, 21); - assert_eq!(glyphs[15].cluster_start, 37); - assert_eq!(glyphs[15].cluster_end, 38); - assert_eq!(glyphs[15].cluster_start, glyphs[14].cluster_end); - } - - #[test] - fn hb_shape_custom_emoji() { - let ftlib = ft::Library::init().unwrap(); - - let font_data = include_bytes!("../../darkirc-emoji-svg.ttf") as &[u8]; - let mut face = ftlib.new_memory_face2(font_data, 0).unwrap(); - - let text = "\u{f0001}"; - - for (i, hbinf) in harfbuzz_shape(&mut face, text).enumerate() { - let glyph_id = hbinf.info.codepoint as u32; - // Index within this substr - let cluster = hbinf.info.cluster as usize; - //println!(" {i}: glyph_id = {glyph_id}, cluster = {cluster}"); - } - } - - #[test] - fn custom_emoji() { - let ftlib = ft::Library::init().unwrap(); - - let font_data = include_bytes!("../../darkirc-emoji-svg.ttf") as &[u8]; - let face = ftlib.new_memory_face2(font_data, 0).unwrap(); - - let mut faces = vec![face]; - let text = "\u{f0001}"; - let glyphs = shape(&mut faces, text); - //print_glyphs("", &glyphs); - } - - /* - #[test] - fn weird_stuff() { - let mut faces = load_faces(); - //let text = "( \u{361}° \u{35c}ʖ \u{361}°)"; - let text = "\u{35c}ʖ \u{361}a"; - let glyphs = shape(&mut faces, text); - } - */ -} diff --git a/bin/app/src/text/wrap.rs b/bin/app/src/text/wrap.rs deleted file mode 100644 index c446d54c8..000000000 --- a/bin/app/src/text/wrap.rs +++ /dev/null @@ -1,295 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2026 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 . - */ - -use super::{Glyph, GlyphPositionIter}; - -#[derive(Debug, PartialEq)] -#[repr(u8)] -enum TokenType { - Null, - Word, - Whitespace, -} - -pub struct Token { - token_type: TokenType, - lhs: f32, - rhs: f32, - glyphs: Vec, -} - -impl Token { - #[allow(dead_code)] - fn as_str(&self) -> String { - glyph_str(&self.glyphs) - } -} - -impl std::fmt::Debug for Token { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.token_type { - TokenType::Null => write!(f, "Token::Null")?, - TokenType::Word => write!(f, "Token::Word")?, - TokenType::Whitespace => write!(f, "Token::Whitespace")?, - } - write!(f, "({})", self.as_str()) - } -} - -/// Get the string represented by a vec of glyphs. Useful for debugging. -pub fn glyph_str(glyphs: &[Glyph]) -> String { - glyphs.iter().map(|g| g.substr.as_str()).collect::>().join("") -} - -fn tokenize(font_size: f32, window_scale: f32, glyphs: &Vec) -> Vec { - let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, glyphs, 0.); - - let mut tokens = vec![]; - let mut token_glyphs = vec![]; - let mut lhs = -1.; - let mut rhs = 0.; - - let mut token_type = TokenType::Null; - - for (pos, glyph) in glyph_pos_iter.zip(glyphs.iter()) { - let new_type = if glyph.substr.chars().all(char::is_whitespace) { - TokenType::Whitespace - } else { - TokenType::Word - }; - - // This is the initial token so lets begin - // Just assume the token_type - if token_type == TokenType::Null { - assert!(token_glyphs.is_empty()); - token_type = new_type; - } else if new_type != token_type { - // We just changed from one token type to another - assert!(!token_glyphs.is_empty()); - - // We have a non-empty word to push - let token = Token { token_type, lhs, rhs, glyphs: std::mem::take(&mut token_glyphs) }; - tokens.push(token); - - // Reset ruler - lhs = -1.; - //rhs = 0.; - // take() blanked token_glyphs above - - token_type = new_type; - } - - // LHS is uninitialized so this is the first glyph in the word - if lhs < 0. { - lhs = pos.x; - } - - // RHS should always be the max - rhs = pos.x + pos.w; - - // Update word - token_glyphs.push(glyph.clone()); - } - - if !token_glyphs.is_empty() { - let token = Token { token_type, lhs, rhs, glyphs: std::mem::take(&mut token_glyphs) }; - tokens.push(token); - } - - tokens -} - -/// Given a series of words, apply wrapping. -/// Whitespace is completely perserved. -fn apply_wrap(line_width: f32, mut tokens: Vec) -> Vec> { - //debug!(target: "text::wrap", "apply_wrap({line_width}, {tokens:?})"); - - let mut lines = vec![]; - let mut line = vec![]; - let mut start = 0.; - - let mut tokens_iter = tokens.iter_mut().peekable(); - while let Some(token) = tokens_iter.next() { - assert!(token.token_type != TokenType::Null); - - // Does this token cross over the end of the line? - if token.rhs > start + line_width { - // Whitespace tokens that cause wrapping are prepended to the current line before - // making the line break. - if token.token_type == TokenType::Whitespace { - line.append(&mut token.glyphs); - } - - // Start a new line - let line = std::mem::take(&mut line); - //debug!(target: "text::apply_wrap", "adding line: {}", glyph_str(&line)); - // This can happen if this token is very long and crosses the line boundary - if !line.is_empty() { - lines.push(line); - } - - // Move to the next token if this is whitespace - if token.token_type == TokenType::Whitespace { - // Load LHS from next token in loop - if let Some(next_token) = tokens_iter.peek() { - start = next_token.lhs; - } - } else { - start = token.lhs; - } - } - - line.append(&mut token.glyphs); - } - - // Handle the remainders - if !line.is_empty() { - let line = std::mem::take(&mut line); - //debug!(target: "text::apply_wrap", "adding rem line: {}", glyph_str(&line)); - lines.push(line); - } - - lines -} - -/// Splits any Word token that exceeds the line width. -/// So Word("aaaaaaaaaaaaaaa") => [Word("aaaaaaaa"), Word("aaaaaaa")]. -pub fn restrict_word_len( - font_size: f32, - window_scale: f32, - raw_tokens: Vec, - line_width: f32, -) -> Vec { - let mut tokens = vec![]; - for token in raw_tokens { - match token.token_type { - TokenType::Word => { - assert!(!token.glyphs.is_empty()); - let token_width = token.rhs - token.lhs; - // No change required. This is the usual code path - if token_width < line_width { - tokens.push(token); - continue - } - } - _ => { - tokens.push(token); - continue - } - } - - // OK we have encountered a Word that is very long. Lets split it up - // into multiple Words each under line_width. - - let glyphs2 = token.glyphs.clone(); - let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs2, 0.); - let mut token_glyphs = vec![]; - let mut lhs = -1.; - let mut rhs = 0.; - - // Just loop through each glyph. When the running total exceeds line_width - // then push the buffer, and start again. - // Very basic stuff. - for (pos, glyph) in glyph_pos_iter.zip(token.glyphs.into_iter()) { - if lhs < 0. { - lhs = pos.x; - } - rhs = pos.x + pos.w; - - token_glyphs.push(glyph); - - let curr_width = rhs - lhs; - // Line width exceeded. Do our thing. - if curr_width > line_width { - let token = Token { - token_type: TokenType::Word, - lhs, - rhs, - glyphs: std::mem::take(&mut token_glyphs), - }; - tokens.push(token); - lhs = -1.; - } - } - - // Take care of any remainders left over. - if !token_glyphs.is_empty() { - let token = Token { - token_type: TokenType::Word, - lhs, - rhs, - glyphs: std::mem::take(&mut token_glyphs), - }; - tokens.push(token); - } - } - tokens -} - -pub fn wrap( - line_width: f32, - font_size: f32, - window_scale: f32, - glyphs: &Vec, -) -> Vec> { - let tokens = tokenize(font_size, window_scale, glyphs); - - //debug!(target: "text::wrap", "tokenized words {:?}", - // words.iter().map(|w| w.as_str()).collect::>()); - - let tokens = restrict_word_len(font_size, window_scale, tokens, line_width); - - let lines = apply_wrap(line_width, tokens); - - //if lines.len() > 1 { - // debug!(target: "text::wrap", "wrapped line: {}", glyph_str(glyphs)); - // for line in &lines { - // debug!(target: "text::wrap", "-> {}", glyph_str(line)); - // } - //} - - lines -} - -#[cfg(test)] -mod tests { - use super::{super::*, *}; - - #[test] - fn wrap_simple() { - let shaper = TextShaper::new(); - let glyphs = shaper.shape("hello world 123".to_string(), 32., 1.); - - let wrapped = wrap(200., 32., 1., &glyphs); - assert_eq!(wrapped.len(), 3); - assert_eq!(glyph_str(&wrapped[0]), "hello "); - assert_eq!(glyph_str(&wrapped[1]), "world "); - assert_eq!(glyph_str(&wrapped[2]), "123"); - } - - #[test] - fn wrap_long() { - let shaper = TextShaper::new(); - let glyphs = shaper.shape("aaaaaaaaaaaaaaa".to_string(), 32., 1.); - - let wrapped = wrap(200., 32., 1., &glyphs); - assert_eq!(wrapped.len(), 2); - assert_eq!(glyph_str(&wrapped[0]), "aaaaaaaa"); - assert_eq!(glyph_str(&wrapped[1]), "aaaaaaa"); - } -}