Skip to content
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
33 changes: 33 additions & 0 deletions api/rs/slint/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,39 @@ pub use i_slint_core::{
string::{SharedString, ToSharedString},
};

/// Register a custom font from byte data at runtime.
///
/// **This is an experimental API.** The API may change in future versions.
///
/// Returns a [`FontHandle`] on success, or a [`RegisterFontError`] on failure.
///
/// This API is available when the `std` feature is enabled.
///
/// # Example
///
/// ```ignore
/// # use slint::*;
/// let font_data = include_bytes!("path/to/font.ttf");
/// match register_font_from_memory(font_data.to_vec()) {
/// Ok(handle) => println!("Registered {} font families", handle.family_ids.len()),
/// Err(e) => eprintln!("Failed to register font: {}", e),
/// }
/// ```
#[cfg(feature = "std")]
pub use i_slint_core::register_font_from_memory;

/// Handle to a registered font that can be used for future operations.
///
/// **This is an experimental API.** The API may change in future versions.
#[cfg(feature = "std")]
pub use i_slint_core::FontHandle;

/// Error type for font registration failures.
///
/// **This is an experimental API.** The API may change in future versions.
#[cfg(feature = "std")]
pub use i_slint_core::RegisterFontError;

pub mod private_unstable_api;

/// Enters the main event loop. This is necessary in order to receive
Expand Down
2 changes: 1 addition & 1 deletion examples/gallery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ slint-build = { path = "../../api/rs/build" }
#wasm#
#wasm# [target.'cfg(target_arch = "wasm32")'.dependencies]
#wasm# wasm-bindgen = { version = "0.2" }
#wasm# web-sys = { version = "0.3", features=["console"] }
#wasm# web-sys = { version = "0.3", features=["console", "Window", "Navigator"] }
#wasm# console_error_panic_hook = "0.1.5"

[package.metadata.bundle]
Expand Down
7 changes: 6 additions & 1 deletion examples/gallery/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
// SPDX-License-Identifier: MIT

fn main() {
slint_build::compile("gallery.slint").unwrap();
let mut config = slint_build::CompilerConfiguration::new();
let target = std::env::var("TARGET").unwrap();
if target.contains("android") || target.contains("wasm32") {
config = config.with_bundled_translations(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/"));
}
slint_build::compile_with_config("gallery.slint", config).unwrap();
}
62 changes: 56 additions & 6 deletions examples/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,44 @@ <h1>Slint Gallery</h1>
var galleries = [];
var currentGallery = undefined;

// Get Noto CJK font URL from GitHub for the detected language
function getNotoFontUrl(lang) {
const langCode = lang.split('-')[0].toLowerCase();

// Direct URLs to OTF files from Noto CJK GitHub repository (using raw.githubusercontent.com for CORS)
const fontMap = {
'ja': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf',
// 'zh': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf',
// 'ko': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf',
};

return fontMap[langCode];
}

// Fetch font from GitHub
async function fetchFont(fontUrl) {
const fontResponse = await fetch(fontUrl);
if (!fontResponse.ok) {
throw new Error(`HTTP ${fontResponse.status}: ${fontResponse.statusText}`);
}
return await fontResponse.arrayBuffer();
}

// Load font for the detected language
async function loadFontForLanguage(module, lang) {
const fontUrl = getNotoFontUrl(lang);

if (fontUrl) {
try {
const fontData = await fetchFont(fontUrl);
const uint8Array = new Uint8Array(fontData);
const result = await module.load_font_from_bytes(uint8Array);
} catch (error) {
console.error(`Failed to load font for language ${lang}:`, error);
}
}
}

function initGallery(gallery) {
document.getElementById("spinner").hidden = false;

Expand All @@ -67,19 +105,31 @@ <h1>Slint Gallery</h1>
document.getElementById("canvas-parent").appendChild(galleries[gallery]);
document.getElementById("spinner").hidden = true;
} else {
import(gallery).then(module => {
import(gallery).then(async module => {
let canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.dataset.slintAutoResizeToPreferred = "true";
currentGallery = gallery;
galleries[gallery] = canvas;

document.getElementById("canvas-parent").appendChild(canvas);
module.default().finally(() => {
document.getElementById("canvas").hidden = false;
document.getElementById("spinner").hidden = true;
});
})

// Initialize WASM module first
await module.default();

// Detect browser language and load appropriate font
const browserLang = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || 'en';
await loadFontForLanguage(module, browserLang);

// Start the application
module.main();

document.getElementById("canvas").hidden = false;
document.getElementById("spinner").hidden = true;
}).catch(error => {
console.error('Failed to initialize gallery:', error);
document.getElementById("spinner").hidden = true;
});
}
}

Expand Down
12 changes: 11 additions & 1 deletion examples/gallery/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@ use wasm_bindgen::prelude::*;

slint::include_modules!();

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn load_font_from_bytes(font_data: &[u8]) -> Result<(), JsValue> {
slint::register_font_from_memory(font_data.to_vec())
.map(|_| ())
.map_err(|e| JsValue::from_str(&format!("Failed to register font: {}", e)))
}

use std::rc::Rc;

use slint::{Model, ModelExt, ModelRc, SharedString, StandardListViewItem, VecModel};

#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub fn main() {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.
#[cfg(all(debug_assertions, target_arch = "wasm32"))]
console_error_panic_hook::set_once();

// For native builds, initialize gettext translations
#[cfg(not(target_arch = "wasm32"))]
slint::init_translations!(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/"));

let app = App::new().unwrap();
Expand Down
82 changes: 82 additions & 0 deletions internal/common/sharedfontique.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,88 @@ impl std::ops::DerefMut for Collection {
}
}

/// Handle to a registered font that can be used for future operations.
#[derive(Debug, Clone)]
pub struct FontHandle {
/// Family IDs of the registered fonts
pub family_ids: Vec<fontique::FamilyId>,
}

/// Error type for font registration failures.
#[derive(Debug, Clone)]
pub enum RegisterFontError {
/// The provided font data was empty
EmptyData,
/// No valid fonts could be extracted from the data
NoFontsFound,
/// The font data could not be parsed
InvalidFontData,
}

impl std::fmt::Display for RegisterFontError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegisterFontError::EmptyData => write!(f, "Font data is empty"),
RegisterFontError::NoFontsFound => {
write!(f, "No valid fonts found in the provided data")
}
RegisterFontError::InvalidFontData => write!(f, "Invalid font data"),
}
}
}

impl std::error::Error for RegisterFontError {}

/// Register a font from byte data dynamically.
///
/// # Arguments
///
/// * `font_data` - Font data as bytes (supports any type that can be converted to a byte slice)
///
/// # Returns
///
/// Returns a `FontHandle` on success, or a `RegisterFontError` on failure.
///
/// # Example
///
/// ```ignore
/// let font_bytes = include_bytes!("my_font.ttf");
/// match register_font_from_memory(font_bytes.to_vec()) {
/// Ok(handle) => println!("Registered {} font families", handle.family_ids.len()),
/// Err(e) => eprintln!("Failed to register font: {}", e),
/// }
/// ```
pub fn register_font_from_memory(
font_data: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<FontHandle, RegisterFontError> {
let data = font_data.as_ref();

if data.is_empty() {
return Err(RegisterFontError::EmptyData);
}

// Convert to owned data for Arc
let owned_data: Vec<u8> = data.to_vec();
let blob = fontique::Blob::new(Arc::new(owned_data));

let mut collection = get_collection();
let fonts = collection.register_fonts(blob, None);

if fonts.is_empty() {
return Err(RegisterFontError::NoFontsFound);
}

let family_ids: Vec<_> = fonts.iter().map(|(family_id, _)| *family_id).collect();

// Set up fallbacks for all scripts
for script in fontique::Script::all_samples().iter().map(|(script, _)| *script) {
collection
.append_fallbacks(fontique::FallbackKey::new(script, None), family_ids.iter().copied());
}

Ok(FontHandle { family_ids })
}

/// Font metrics in design space. Scale with desired pixel size and divided by units_per_em
/// to obtain pixel metrics.
#[derive(Clone)]
Expand Down
3 changes: 2 additions & 1 deletion internal/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ std = [
"chrono/wasmbind",
"chrono/clock",
"dep:sys-locale",
"shared-fontique",
]
# Unsafe feature meaning that there is only one core running and all thread_local are static.
# You can only enable this feature if you are sure that any API of this crate is only called
Expand Down Expand Up @@ -106,7 +107,7 @@ unicode-linebreak = { version = "0.1.5", optional = true }
unicode-script = { version = "0.5.7", optional = true }
integer-sqrt = { version = "0.1.5" }
bytemuck = { workspace = true, optional = true, features = ["derive"] }
sys-locale = { version = "0.3.2", optional = true }
sys-locale = { version = "0.3.2", optional = true, features = ["js"] }
parley = { version = "0.6.0", optional = true }
pulldown-cmark = { version = "0.13.0", optional = true }

Expand Down
12 changes: 12 additions & 0 deletions internal/core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ pub mod window;
#[doc(inline)]
pub use string::SharedString;

/// Register a font from memory.
#[cfg(feature = "std")]
pub use i_slint_common::sharedfontique::register_font_from_memory;

/// Handle to a registered font.
#[cfg(feature = "std")]
pub use i_slint_common::sharedfontique::FontHandle;

/// Error type for font registration failures.
#[cfg(feature = "std")]
pub use i_slint_common::sharedfontique::RegisterFontError;

#[doc(inline)]
pub use sharedvector::SharedVector;

Expand Down