Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial attempt to setup a font system #754

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/gosub_cairo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ license = "MIT"
gosub_shared = { version = "0.1.1", registry = "gosub", path = "../gosub_shared" }
gosub_interface = { version = "0.1.2", registry = "gosub", path = "../gosub_interface", features = [] }
gosub_svg = { version = "0.1.1", registry = "gosub", path = "../gosub_svg", features = ["resvg"] }
gosub_renderer = { version = "0.1.1", registry = "gosub", path = "../gosub_renderer" }
image = "0.25.2"
smallvec = "1.13.2"
anyhow = "1.0.89"
Expand All @@ -24,3 +25,5 @@ uuid = { version = "1.11.0", features = ["v4"] }
freetype-rs = "0.37.0"
parley = "0.2.0"
once_cell = "1.20.2"
pango = "0.20.7"
pangocairo = "0.20.7"
1 change: 1 addition & 0 deletions crates/gosub_cairo/src/elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pub(crate) mod color;
pub(crate) mod gradient;
pub(crate) mod image;
pub(crate) mod rect;
// pub(crate) mod text;
pub(crate) mod text;
pub(crate) mod transform;
2 changes: 2 additions & 0 deletions crates/gosub_cairo/src/elements/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ impl GsBrush {
}

pub fn render(obj: &GsBrush, cr: &cairo::Context) {
// info!(target: "cairo", "GsBrush::render");

match &obj {
GsBrush::Solid(c) => {
cr.set_source_rgba(c.r, c.g, c.b, c.a);
Expand Down
2 changes: 2 additions & 0 deletions crates/gosub_cairo/src/elements/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ impl GsRect {
}

pub(crate) fn render(obj: &RenderRect<CairoBackend>, cr: &cairo::Context) {
// info!(target: "cairo", "GsRect::render");

let x = obj.rect.x;
let y = obj.rect.y;
let width = obj.rect.width;
Expand Down
229 changes: 18 additions & 211 deletions crates/gosub_cairo/src/elements/text.rs
Original file line number Diff line number Diff line change
@@ -1,233 +1,40 @@
use crate::CairoBackend;
use gosub_interface::layout::{Decoration, TextLayout};
use gosub_interface::render_backend::{RenderText, Text as TText};
use gosub_shared::font::{Glyph, GlyphID};
use gosub_shared::geo::FP;
use peniko::Font;
use skrifa::instance::NormalizedCoord;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use gosub_interface::render_backend::RenderText;

use crate::elements::brush::GsBrush;
use crate::elements::color::GsColor;
use freetype::{Face, Library};
use gosub_shared::ROBOTO_FONT;
use kurbo::Stroke;
use log::info;
use once_cell::sync::Lazy;
use parley::fontique::{FamilyId, SourceKind};
use parley::{FontContext, GenericFamily};

/// Font manager that keeps track of fonts and faces
struct GosubFontContext {
/// Freetype library. Should be kept alive as long as any face is alive.
_library: Library,
/// Font context for parley to find fonts
font_ctx: FontContext,
/// Cache of any loaded font faces
face_cache: HashMap<String, Face>,
/// Default font face to use when a font cannot be found
default_face: Face,
}

impl GosubFontContext {
/// Finds the face for the given family name, or returns the default face if no font is found.
fn find_face_family(&mut self, family: &str) -> &mut Face {
info!("Finding face for family: {}", family);

// See if we already got the face in cache
if self.face_cache.contains_key(family) {
info!("Face found in cache");
return self.face_cache.get_mut(family).expect("Face not found in cache");
}

// Parse the family name into a GenericFamily enum
let gf = GenericFamily::parse(family).unwrap_or(GenericFamily::SansSerif);

// Find all the fonts for this family
let fids: Vec<FamilyId> = self.font_ctx.collection.generic_families(gf).collect();
if fids.is_empty() {
info!("No family found for family: {}", family);
return &mut self.default_face;
}

// We only use the first font in the family
match self.font_ctx.collection.family(fids[0]) {
Some(f) => {
info!("Face found for family: {:?}", f.fonts());

// This first font can have multiple fonts (e.g. regular, bold, italic, etc.)
for font in f.fonts() {
match &font.source().kind {
SourceKind::Memory(blob) => {
info!("Loading font face from memory");
let rc = Rc::new(blob.data().to_vec());

let face = self._library.new_memory_face(rc, 0).expect("Failed to load font face");
self.face_cache.insert(family.to_string(), face);
}
SourceKind::Path(path) => {
info!("Loading font face from path {}", path.to_str().expect("path to string"));

let face = self
._library
.new_face(path.to_str().expect("path to string"), 0)
.expect("Failed to load font face");
self.face_cache.insert(family.to_string(), face);
}
}
}
}
None => {
info!("No face found for family: {}", family);
}
}

&mut self.default_face
}
}

thread_local! {
/// We use a thread-local lazy static to ensure the font context is initialized once per thread
/// and is dropped when the thread exits. We need this because the FreeType library cannot be dropped
/// while any faces are still alive, so all is managed within this struct.
static LIB_FONT_FACE: Lazy<RefCell<GosubFontContext>> = Lazy::new(|| {
let lib = Library::init().expect("Failed to initialize FreeType");
let rc = Rc::new(ROBOTO_FONT.to_vec());
let default_face = lib.new_memory_face(rc, 0).expect("Failed to load font face");

// The FontContext struct holds the lib, ensuring it lives as long as all loaded faces
RefCell::new(GosubFontContext {
_library: lib,
font_ctx: FontContext::new(),
face_cache: HashMap::new(),
default_face,
})
});
}
use pango::Layout;
use gosub_renderer::font::Font as GsRenderFont;

#[allow(unused)]
#[derive(Clone, Debug)]
pub struct GsText {
// List of glyphs we need to show
glyphs: Vec<Glyph>,
// Actual utf-8 text (we don't have this yet)
/// Actual utf-8 text
text: String,
// Font we need to display (we need to have more info, like font familty, weight, etc.)
font: Font,
// Font size
fs: FP,
// List of coordinates for each glyph (?)
coords: Vec<NormalizedCoord>,
// Text decoration (strike-through, underline, etc.)
decoration: Decoration,
}

impl TText for GsText {
type Font = Font;

fn new<TL: TextLayout>(layout: &TL) -> Self
where
TL::Font: Into<Font>,
{
let font = layout.font().clone().into();
let fs = layout.font_size();

let glyphs = layout
.glyphs()
.iter()
.map(|g| Glyph {
id: g.id as GlyphID,
x: g.x,
y: g.y,
})
.collect();
let coords = layout.coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect();

Self {
glyphs,
text: String::new(),
font,
fs,
coords,
decoration: layout.decorations().clone(),
}
}
/// Font in which we need to display the text
font: GsRenderFont,
/// Position of the text (top-left corner)
tl_pos: [f64; 2],
}

impl GsText {
pub(crate) fn render(obj: &RenderText<CairoBackend>, cr: &cairo::Context) {
// let brush = &render.brush;
// let style: StyleRef = Fill::NonZero.into();
//
// let transform = render.transform.map(|t| t).unwrap_or(Transform::IDENTITY);
// let brush_transform = render.brush_transform.map(|t| t);
info!(target: "cairo", "GsText::render");

let base_x = obj.rect.x;
let base_y = obj.rect.y;
cr.move_to(base_x, base_y);
let pango_ctx = pangocairo::functions::create_context(cr);
let layout = Layout::new(&pango_ctx);

let font_desc = &obj.font.get_font_description();
layout.set_font_description(Some(&font_desc));
layout.set_text(&obj.text);

// Setup brush for rendering text
GsBrush::render(&obj.brush, cr);

// This should be moved to the GosubFontContext::get_cairo_font_face(family: &str) method)
let font_face = unsafe {
LIB_FONT_FACE.with(|ctx_ref| {
let mut ctx = ctx_ref.borrow_mut();

let ft_face = ctx.find_face_family("sans-serif");
let ft_face_ptr = ft_face.raw_mut() as *mut _ as *mut std::ffi::c_void;
let ff = cairo::ffi::cairo_ft_font_face_create_for_ft_face(ft_face_ptr, 0);
cairo::FontFace::from_raw_full(ff)
})
};
cr.set_font_face(&font_face);

cr.set_font_size(obj.text.fs.into());

// Convert glyphs that are in parley / taffy format to cairo glyphs. Also make sure we
// offset the glyphs by the base_x and base_y.
let mut cairo_glyphs = vec![];
for glyph in &obj.text.glyphs {
let cairo_glyph = cairo::Glyph::new(glyph.id as u64, base_x + glyph.x as f64, base_y + glyph.y as f64);
cairo_glyphs.push(cairo_glyph);
}

_ = cr.show_glyphs(&cairo_glyphs);

// Set decoration (underline, overline, line-through)
{
let decoration = &obj.text.decoration;
let _stroke = Stroke::new(decoration.width as f64);

let c = decoration.color;
let brush = GsBrush::solid(GsColor::rgba32(c.0, c.1, c.2, 1.0));
GsBrush::render(&brush, cr);

let offset = decoration.x_offset as f64;
if decoration.underline {
let y = base_y + decoration.underline_offset as f64;

cr.move_to(base_x + offset, y);
cr.line_to(base_x + obj.rect.width, y);
_ = cr.stroke();
}
if decoration.overline {
let y = base_y - obj.rect.height;

cr.move_to(base_x + offset, y);
cr.line_to(base_x + obj.rect.width, y);
_ = cr.stroke();
}

if decoration.line_through {
let y = base_y - obj.rect.height / 2.0;

cr.move_to(base_x + offset, y);
cr.line_to(base_x + obj.rect.width, y);
_ = cr.stroke();
}
}
cr.move_to(obj.rect.x.into(), obj.rect.y.into());
cr.set_source_rgb(0.0, 0.0, 1.0);
pangocairo::functions::show_layout(cr, &layout);
}
}
35 changes: 35 additions & 0 deletions crates/gosub_cairo/src/font_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use pango::prelude::{FontFamilyExt, FontMapExt};

/// This manager keeps track of all loaded fonts and provides a way to check if a font family is available.
pub struct FontManager {
font_map: pango::FontMap,
}

impl FontManager {
pub fn new() -> Self {
let font_map = pangocairo::FontMap::new();
Self {
font_map,
}
}

pub fn has_font_family(&self, family: &str) -> bool {
self.font_map.list_families().iter().any(|f| f.name() == family)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_font_manager() {
let font_manager = FontManager::new();
assert!(font_manager.has_font_family("Sans"));
assert!(font_manager.has_font_family("Serif"));
assert!(font_manager.has_font_family("Monospace"));

assert!(!font_manager.has_font_family("Comic Sans"));
assert!(!font_manager.has_font_family("NOTAVAILBLE"));
}
}
Loading
Loading