mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-08 22:28:12 -05:00
app: delete freetype 🎉🎉🎉
This commit is contained in:
121
bin/app/Cargo.lock
generated
121
bin/app/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>>;
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user