app: delete freetype 🎉🎉🎉

This commit is contained in:
jkds
2026-01-05 18:14:43 +01:00
parent cc53053ef2
commit 94a0db125e
11 changed files with 8 additions and 1747 deletions

121
bin/app/Cargo.lock generated
View File

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

View File

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

View File

@@ -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<App>;
pub struct App {
pub sg_root: SceneNodePtr,
pub render_api: RenderApi,
pub text_shaper: TextShaperPtr,
pub tasks: SyncMutex<Vec<Task<()>>>,
pub ex: ExecutorPtr,
}
@@ -68,10 +66,9 @@ impl App {
pub fn new(
sg_root: SceneNodePtr,
render_api: RenderApi,
text_shaper: TextShaperPtr,
ex: ExecutorPtr,
) -> Arc<Self> {
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

View File

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

View File

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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Glyph>,
) -> 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<u32>,
sprites: Vec<SpritePtr>,
// LHS x pos of glyph
x_pos: Vec<usize>,
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<Glyph>`
/// to this atlas.
pub fn push(&mut self, glyphs: &Vec<Glyph>) {
for glyph in glyphs {
self.push_glyph(glyph);
}
}
fn render(&self) -> Vec<u8> {
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<Rectangle> {
// 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<u8>, 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<u32>,
/// UV rectangle within the texture.
uv_rects: Vec<Rectangle>,
/// 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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
use freetype::face::LoadFlag as FtLoadFlag;
use std::sync::Arc;
pub type FreetypeFace = freetype::Face<&'static [u8]>;
pub type SpritePtr = Arc<Sprite>;
pub struct Sprite {
pub bmp: Vec<u8>,
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<Sprite> {
// 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();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<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: 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<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>>;

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Rectangle>,
pub texture_id: GfxTextureId,
}
const ATLAS_GAP: usize = 2;
pub async fn make_texture_atlas(
render_api: &RenderApi,
font_size: f32,
glyphs: &Vec<Glyph>,
) -> Result<RenderedAtlas> {
// 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<u32> = vec![];
let mut uv_rects: Vec<Rectangle> = 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<u8>, 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];
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Self::Item> {
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<GlyphInfo>,
}
impl ShapedGlyphs {
fn new(glyphs: Vec<GlyphInfo>) -> Self {
Self { glyphs }
}
fn has_zero(&self) -> bool {
self.glyphs.iter().any(|g| g.id == 0)
}
fn fill_zeros(&mut self, fallback: Vec<GlyphInfo>) {
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<IntoIter<GlyphInfo>>, cluster_bound: usize) -> Vec<GlyphInfo> {
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<IntoIter<GlyphInfo>>, 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<GlyphInfo>, 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<GlyphInfo> {
let mut glyphs: Vec<GlyphInfo> = 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<FreetypeFace>, text: &str) -> Vec<GlyphInfo> {
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<FreetypeFace> {
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);
}
*/
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Glyph>,
}
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::<Vec<_>>().join("")
}
fn tokenize(font_size: f32, window_scale: f32, glyphs: &Vec<Glyph>) -> Vec<Token> {
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<Token>) -> Vec<Vec<Glyph>> {
//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<Token>,
line_width: f32,
) -> Vec<Token> {
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<Glyph>,
) -> Vec<Vec<Glyph>> {
let tokens = tokenize(font_size, window_scale, glyphs);
//debug!(target: "text::wrap", "tokenized words {:?}",
// words.iter().map(|w| w.as_str()).collect::<Vec<_>>());
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");
}
}