Skip to content
Closed
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
753 changes: 249 additions & 504 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/gosub_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ r2d2_sqlite = { version = "0.33", optional = true, features = ["bundled"] }
gtk4 = { version = "0.11", optional = true }
cairo-rs = { version = "0.22", optional = true }

skia-safe = { version = "0.87.0", optional = true }
skia-safe = { version = "0.97.2", optional = true }

vello = { version = "0.8.0", optional = true }
wgpu = { version = "29.0.0", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/gosub_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,6 @@ pub fn init_gtk_resources() {
#[cfg(feature = "backend_cairo_pango")]
{
gtk4::init().expect("GTK init failed — on headless systems set GDK_BACKEND=offscreen");
gosub_renderer_cairo::font::pango::init_system_ui_font();
gosub_renderer_cairo::font::pango::init();
}
}
1 change: 1 addition & 0 deletions crates/gosub_fontmanager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ path = "src/bin/vello-test.rs"

[dependencies]
gosub_interface = { version = "0.1.1", registry = "gosub", path = "../gosub_interface", features = [] }
gosub_shared = { version = "0.1.1", registry = "gosub", path = "../gosub_shared" }
parking_lot = "0.12"
colog = "1.4.0"
log = "0.4.29"
Expand Down
2 changes: 2 additions & 0 deletions crates/gosub_fontmanager/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod flatland;
pub mod font_manager;
pub mod parley_system;

pub use font_manager::font_info::FontInfo;
pub use font_manager::manager::FontManager;
pub use parley_system::ParleyFontSystem;
271 changes: 271 additions & 0 deletions crates/gosub_fontmanager/src/parley_system.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
use cow_utils::CowUtils;
use gosub_interface::font::{FontBlob, FontError, FontStyle};
use gosub_interface::font_system::{
FontQuery, FontStretch, FontSystem, FontWeight, ResolvedFont, ShapedGlyph, ShapedRun, ShapedText,
};
use parley::fontique::{Attributes, FontWidth, GenericFamily, QueryFamily, QueryStatus, SourceCache};
use parley::style::{FontStyle as ParleyStyle, FontWeight as ParleyWeight};
use parley::{Alignment, AlignmentOptions, FontContext, LayoutContext, PositionedLayoutItem};
use std::any::Any;

/// A [`FontSystem`] implementation backed by Parley + Fontique.
///
/// Holds a single `FontContext` (fontique collection) and `LayoutContext` so that
/// all callers — the layout engine and every renderer — share the same font data and
/// produce consistent glyph metrics.
///
/// Construct once at application start, wrap in `Arc<Mutex<ParleyFontSystem>>`, and
/// pass the same `Arc` into both the Taffy layouter and the rendering backend.
pub struct ParleyFontSystem {
font_cx: FontContext,
layout_cx: LayoutContext<()>,
source_cache: SourceCache,
}

impl std::fmt::Debug for ParleyFontSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParleyFontSystem").finish_non_exhaustive()
}
}

impl Default for ParleyFontSystem {
fn default() -> Self {
Self::new()
}
}

impl ParleyFontSystem {
/// Create a new font system with system fonts loaded and Roboto registered as
/// the built-in fallback.
pub fn new() -> Self {
let mut font_cx = FontContext::new();

// Register Roboto as a bundled fallback so there is always something to
// render with even on systems that have no fonts installed.
font_cx
.collection
.register_fonts(gosub_shared::ROBOTO_FONT.to_vec().into(), None);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this here should not copy the font. It wants a dyn AsRef<[u8]> and we can just do that with a &'static [u8]


Self {
font_cx,
layout_cx: LayoutContext::new(),
source_cache: SourceCache::new_shared(),
}
}
}

impl ParleyFontSystem {
/// Grants direct access to the underlying Parley font collection.
///
/// Used by `TaffyLayouter` so that the same font collection is shared between
/// the layout engine and rendering, ensuring consistent shaping.
pub fn font_cx_mut(&mut self) -> &mut FontContext {
&mut self.font_cx
}
}

impl FontSystem for ParleyFontSystem {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}

fn register_font(&mut self, data: Vec<u8>, _family_override: Option<&str>) -> Result<(), FontError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I think we should have an additional method where you can register a &'static [u8] font and shared fonts with Arc<[u8]>

// fontique derives the family name from the font's own `name` table;
// custom name overrides are not yet supported here.
self.font_cx.collection.register_fonts(data.into(), None);
Ok(())
}

fn resolve(&mut self, query: &FontQuery<'_>) -> Result<ResolvedFont, FontError> {
let families: Vec<QueryFamily> = query.families.iter().map(|&name| css_family_to_query(name)).collect();

let attrs = Attributes::new(
stretch_to_width(query.stretch),
style_to_fontique(query.style),
weight_to_fontique(query.weight),
);

let mut col_clone = self.font_cx.collection.clone();
let mut q = self.font_cx.collection.query(&mut self.source_cache);
q.set_families(families);
q.set_attributes(attrs);

let mut found: Option<ResolvedFont> = None;
q.matches_with(|cand| {
// Extract the inner Arc from fontique's Blob<u8> without copying bytes.
let (data_arc, _) = cand.blob.clone().into_raw_parts();
let blob = FontBlob::new(data_arc, cand.index);

let (fam_id, _) = cand.family;
let family = col_clone
.family(fam_id)
.map(|f| f.name().to_string())
.unwrap_or_else(|| query.families.first().copied().unwrap_or("sans-serif").to_string());

found = Some(ResolvedFont {
family,
style: query.style,
weight: query.weight,
stretch: query.stretch,
blob,
});

QueryStatus::Stop
});

found.ok_or_else(|| FontError::FontNotFound(query.families.join(", ")))
}

fn shape(&mut self, text: &str, font: &ResolvedFont, size: f32, max_width: Option<f32>) -> ShapedText {
if text.is_empty() {
return ShapedText::empty();
}

let mut builder = self.layout_cx.ranged_builder(&mut self.font_cx, text, 1.0, false);
builder.push_default(parley::StyleProperty::FontSize(size));
builder.push_default(parley::StyleProperty::FontFamily(parley::FontFamily::Source(
font.family.as_str().into(),
)));
builder.push_default(parley::StyleProperty::FontWeight(ParleyWeight::new(
font.weight.0 as f32,
)));
builder.push_default(parley::StyleProperty::FontStyle(style_to_parley(font.style)));
builder.push_default(parley::StyleProperty::Brush(()));

let mut layout = builder.build(text);
layout.break_all_lines(Some(max_width.unwrap_or(f32::INFINITY)));
layout.align(Alignment::Start, AlignmentOptions::default());

let mut runs: Vec<ShapedRun> = Vec::new();
let mut pen_y = 0.0f32;
let mut total_width = 0.0f32;
let mut first_ascent = 0.0f32;
let mut last_line_height = 0.0f32;
let mut first_line = true;

for line in layout.lines() {
let lm = line.metrics();
if first_line {
first_ascent = lm.ascent;
first_line = false;
}
last_line_height = lm.line_height;
let baseline = lm.ascent;

for item in line.items() {
if let PositionedLayoutItem::GlyphRun(run) = item {
total_width = total_width.max(run.offset() + run.advance());

let run_x = run.offset();
let mut pen_x = 0.0f32;

let glyphs: Vec<ShapedGlyph> = run
.glyphs()
.map(|g| {
let x = run_x + pen_x + g.x;
let y = pen_y + baseline + g.y;
pen_x += g.advance;
ShapedGlyph { id: g.id, x, y }
})
.collect();

if !glyphs.is_empty() {
runs.push(ShapedRun {
font: font.clone(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should not clone the font here. Every run is made by that one font anyways.

Also, I see problems here. We might have one inline context with more than one font aswell as other elements, and this seems to not account for that.

font_size: size,
glyphs,
});
}
}
}

pen_y += lm.line_height;
}

ShapedText {
runs,
width: total_width,
height: pen_y,
line_height: last_line_height,
ascent: first_ascent,
}
}

fn measure(&mut self, text: &str, font: &ResolvedFont, size: f32, max_width: Option<f32>) -> (f32, f32) {
if text.is_empty() {
return (0.0, 0.0);
}

let mut builder = self.layout_cx.ranged_builder(&mut self.font_cx, text, 1.0, false);
builder.push_default(parley::StyleProperty::FontSize(size));
builder.push_default(parley::StyleProperty::FontFamily(parley::FontFamily::Source(
font.family.as_str().into(),
)));
builder.push_default(parley::StyleProperty::FontWeight(ParleyWeight::new(
font.weight.0 as f32,
)));
builder.push_default(parley::StyleProperty::FontStyle(style_to_parley(font.style)));
builder.push_default(parley::StyleProperty::Brush(()));

let mut layout = builder.build(text);
layout.break_all_lines(Some(max_width.unwrap_or(f32::INFINITY)));

let mut width = 0.0f32;
let mut height = 0.0f32;
for line in layout.lines() {
let lm = line.metrics();
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(run) = item {
width = width.max(run.offset() + run.advance());
}
}
height += lm.line_height;
}

(width, height)
}
}

// ---------------------------------------------------------------------------
// Conversion helpers
// ---------------------------------------------------------------------------

fn css_family_to_query(name: &str) -> QueryFamily<'_> {
match name.cow_to_lowercase().as_ref() {
"sans-serif" => GenericFamily::SansSerif.into(),
"serif" => GenericFamily::Serif.into(),
"monospace" | "monospaced" => GenericFamily::Monospace.into(),
"cursive" => GenericFamily::Cursive.into(),
"fantasy" => GenericFamily::Fantasy.into(),
"system-ui" => GenericFamily::SystemUi.into(),
"ui-sans-serif" => GenericFamily::UiSansSerif.into(),
"ui-serif" => GenericFamily::UiSerif.into(),
"ui-monospace" => GenericFamily::UiMonospace.into(),
"ui-rounded" => GenericFamily::UiRounded.into(),
_ => QueryFamily::Named(name),
}
}

fn weight_to_fontique(w: FontWeight) -> parley::fontique::FontWeight {
parley::fontique::FontWeight::new(w.0 as f32)
}

fn stretch_to_width(s: FontStretch) -> FontWidth {
FontWidth::from_ratio(s.0)
}

fn style_to_fontique(s: FontStyle) -> parley::fontique::FontStyle {
match s {
FontStyle::Normal => parley::fontique::FontStyle::Normal,
FontStyle::Italic => parley::fontique::FontStyle::Italic,
FontStyle::Oblique => parley::fontique::FontStyle::Oblique(None),
}
}

fn style_to_parley(s: FontStyle) -> ParleyStyle {
match s {
FontStyle::Normal => ParleyStyle::Normal,
FontStyle::Italic => ParleyStyle::Italic,
FontStyle::Oblique => ParleyStyle::Oblique(None),
}
}
Loading
Loading