Skip to content

Commit 224a06a

Browse files
authored
Gallery Example: Add i18n and dynamic font loading for WASM (#9762)
- Bundle translations for WASM/Android only (not for native builds) - Load Noto Sans CJK fonts dynamically from GitHub at runtime - Implement register_font_from_memory() with Result<FontHandle, RegisterFontError> - Make API automatically available with std feature (no explicit feature flag needed) - Export load_font_from_bytes() for WASM font loading from JavaScript - Change from wasm_bindgen(start) to explicit main() call for better control Fonts are loaded before app initialization to ensure proper CJK text rendering across all major browsers without bundling font files. Native builds load translations from lang/ directory at runtime. WASM/Android builds bundle translations at compile time and detect browser/system language automatically. API is now available when std feature is enabled, since we always have fontique with std. No need for experimental-register-font feature.
1 parent 2de0e57 commit 224a06a

File tree

8 files changed

+203
-10
lines changed

8 files changed

+203
-10
lines changed

api/rs/slint/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,39 @@ pub use i_slint_core::{
233233
string::{SharedString, ToSharedString},
234234
};
235235

236+
/// Register a custom font from byte data at runtime.
237+
///
238+
/// **This is an experimental API.** The API may change in future versions.
239+
///
240+
/// Returns a [`FontHandle`] on success, or a [`RegisterFontError`] on failure.
241+
///
242+
/// This API is available when the `std` feature is enabled.
243+
///
244+
/// # Example
245+
///
246+
/// ```ignore
247+
/// # use slint::*;
248+
/// let font_data = include_bytes!("path/to/font.ttf");
249+
/// match register_font_from_memory(font_data.to_vec()) {
250+
/// Ok(handle) => println!("Registered {} font families", handle.family_ids.len()),
251+
/// Err(e) => eprintln!("Failed to register font: {}", e),
252+
/// }
253+
/// ```
254+
#[cfg(feature = "std")]
255+
pub use i_slint_core::register_font_from_memory;
256+
257+
/// Handle to a registered font that can be used for future operations.
258+
///
259+
/// **This is an experimental API.** The API may change in future versions.
260+
#[cfg(feature = "std")]
261+
pub use i_slint_core::FontHandle;
262+
263+
/// Error type for font registration failures.
264+
///
265+
/// **This is an experimental API.** The API may change in future versions.
266+
#[cfg(feature = "std")]
267+
pub use i_slint_core::RegisterFontError;
268+
236269
pub mod private_unstable_api;
237270

238271
/// Enters the main event loop. This is necessary in order to receive

examples/gallery/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ slint-build = { path = "../../api/rs/build" }
3131
#wasm#
3232
#wasm# [target.'cfg(target_arch = "wasm32")'.dependencies]
3333
#wasm# wasm-bindgen = { version = "0.2" }
34-
#wasm# web-sys = { version = "0.3", features=["console"] }
34+
#wasm# web-sys = { version = "0.3", features=["console", "Window", "Navigator"] }
3535
#wasm# console_error_panic_hook = "0.1.5"
3636

3737
[package.metadata.bundle]

examples/gallery/build.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
// SPDX-License-Identifier: MIT
33

44
fn main() {
5-
slint_build::compile("gallery.slint").unwrap();
5+
let mut config = slint_build::CompilerConfiguration::new();
6+
let target = std::env::var("TARGET").unwrap();
7+
if target.contains("android") || target.contains("wasm32") {
8+
config = config.with_bundled_translations(concat!(env!("CARGO_MANIFEST_DIR"), "/lang/"));
9+
}
10+
slint_build::compile_with_config("gallery.slint", config).unwrap();
611
}

examples/gallery/index.html

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,44 @@ <h1>Slint Gallery</h1>
5151
var galleries = [];
5252
var currentGallery = undefined;
5353

54+
// Get Noto CJK font URL from GitHub for the detected language
55+
function getNotoFontUrl(lang) {
56+
const langCode = lang.split('-')[0].toLowerCase();
57+
58+
// Direct URLs to OTF files from Noto CJK GitHub repository (using raw.githubusercontent.com for CORS)
59+
const fontMap = {
60+
'ja': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf',
61+
// 'zh': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf',
62+
// 'ko': 'https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf',
63+
};
64+
65+
return fontMap[langCode];
66+
}
67+
68+
// Fetch font from GitHub
69+
async function fetchFont(fontUrl) {
70+
const fontResponse = await fetch(fontUrl);
71+
if (!fontResponse.ok) {
72+
throw new Error(`HTTP ${fontResponse.status}: ${fontResponse.statusText}`);
73+
}
74+
return await fontResponse.arrayBuffer();
75+
}
76+
77+
// Load font for the detected language
78+
async function loadFontForLanguage(module, lang) {
79+
const fontUrl = getNotoFontUrl(lang);
80+
81+
if (fontUrl) {
82+
try {
83+
const fontData = await fetchFont(fontUrl);
84+
const uint8Array = new Uint8Array(fontData);
85+
const result = await module.load_font_from_bytes(uint8Array);
86+
} catch (error) {
87+
console.error(`Failed to load font for language ${lang}:`, error);
88+
}
89+
}
90+
}
91+
5492
function initGallery(gallery) {
5593
document.getElementById("spinner").hidden = false;
5694

@@ -67,19 +105,31 @@ <h1>Slint Gallery</h1>
67105
document.getElementById("canvas-parent").appendChild(galleries[gallery]);
68106
document.getElementById("spinner").hidden = true;
69107
} else {
70-
import(gallery).then(module => {
108+
import(gallery).then(async module => {
71109
let canvas = document.createElement("canvas");
72110
canvas.id = "canvas";
73111
canvas.dataset.slintAutoResizeToPreferred = "true";
74112
currentGallery = gallery;
75113
galleries[gallery] = canvas;
76114

77115
document.getElementById("canvas-parent").appendChild(canvas);
78-
module.default().finally(() => {
79-
document.getElementById("canvas").hidden = false;
80-
document.getElementById("spinner").hidden = true;
81-
});
82-
})
116+
117+
// Initialize WASM module first
118+
await module.default();
119+
120+
// Detect browser language and load appropriate font
121+
const browserLang = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage || 'en';
122+
await loadFontForLanguage(module, browserLang);
123+
124+
// Start the application
125+
module.main();
126+
127+
document.getElementById("canvas").hidden = false;
128+
document.getElementById("spinner").hidden = true;
129+
}).catch(error => {
130+
console.error('Failed to initialize gallery:', error);
131+
document.getElementById("spinner").hidden = true;
132+
});
83133
}
84134
}
85135

examples/gallery/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,27 @@ use wasm_bindgen::prelude::*;
88

99
slint::include_modules!();
1010

11+
#[cfg(target_arch = "wasm32")]
12+
#[wasm_bindgen]
13+
pub fn load_font_from_bytes(font_data: &[u8]) -> Result<(), JsValue> {
14+
slint::register_font_from_memory(font_data.to_vec())
15+
.map(|_| ())
16+
.map_err(|e| JsValue::from_str(&format!("Failed to register font: {}", e)))
17+
}
18+
1119
use std::rc::Rc;
1220

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

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

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

2434
let app = App::new().unwrap();

internal/common/sharedfontique.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,88 @@ impl std::ops::DerefMut for Collection {
131131
}
132132
}
133133

134+
/// Handle to a registered font that can be used for future operations.
135+
#[derive(Debug, Clone)]
136+
pub struct FontHandle {
137+
/// Family IDs of the registered fonts
138+
pub family_ids: Vec<fontique::FamilyId>,
139+
}
140+
141+
/// Error type for font registration failures.
142+
#[derive(Debug, Clone)]
143+
pub enum RegisterFontError {
144+
/// The provided font data was empty
145+
EmptyData,
146+
/// No valid fonts could be extracted from the data
147+
NoFontsFound,
148+
/// The font data could not be parsed
149+
InvalidFontData,
150+
}
151+
152+
impl std::fmt::Display for RegisterFontError {
153+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154+
match self {
155+
RegisterFontError::EmptyData => write!(f, "Font data is empty"),
156+
RegisterFontError::NoFontsFound => {
157+
write!(f, "No valid fonts found in the provided data")
158+
}
159+
RegisterFontError::InvalidFontData => write!(f, "Invalid font data"),
160+
}
161+
}
162+
}
163+
164+
impl std::error::Error for RegisterFontError {}
165+
166+
/// Register a font from byte data dynamically.
167+
///
168+
/// # Arguments
169+
///
170+
/// * `font_data` - Font data as bytes (supports any type that can be converted to a byte slice)
171+
///
172+
/// # Returns
173+
///
174+
/// Returns a `FontHandle` on success, or a `RegisterFontError` on failure.
175+
///
176+
/// # Example
177+
///
178+
/// ```ignore
179+
/// let font_bytes = include_bytes!("my_font.ttf");
180+
/// match register_font_from_memory(font_bytes.to_vec()) {
181+
/// Ok(handle) => println!("Registered {} font families", handle.family_ids.len()),
182+
/// Err(e) => eprintln!("Failed to register font: {}", e),
183+
/// }
184+
/// ```
185+
pub fn register_font_from_memory(
186+
font_data: impl AsRef<[u8]> + Send + Sync + 'static,
187+
) -> Result<FontHandle, RegisterFontError> {
188+
let data = font_data.as_ref();
189+
190+
if data.is_empty() {
191+
return Err(RegisterFontError::EmptyData);
192+
}
193+
194+
// Convert to owned data for Arc
195+
let owned_data: Vec<u8> = data.to_vec();
196+
let blob = fontique::Blob::new(Arc::new(owned_data));
197+
198+
let mut collection = get_collection();
199+
let fonts = collection.register_fonts(blob, None);
200+
201+
if fonts.is_empty() {
202+
return Err(RegisterFontError::NoFontsFound);
203+
}
204+
205+
let family_ids: Vec<_> = fonts.iter().map(|(family_id, _)| *family_id).collect();
206+
207+
// Set up fallbacks for all scripts
208+
for script in fontique::Script::all_samples().iter().map(|(script, _)| *script) {
209+
collection
210+
.append_fallbacks(fontique::FallbackKey::new(script, None), family_ids.iter().copied());
211+
}
212+
213+
Ok(FontHandle { family_ids })
214+
}
215+
134216
/// Font metrics in design space. Scale with desired pixel size and divided by units_per_em
135217
/// to obtain pixel metrics.
136218
#[derive(Clone)]

internal/core/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ std = [
4343
"chrono/clock",
4444
"dep:sys-locale",
4545
"dep:webbrowser",
46+
"shared-fontique",
4647
]
4748
# Unsafe feature meaning that there is only one core running and all thread_local are static.
4849
# You can only enable this feature if you are sure that any API of this crate is only called
@@ -112,7 +113,7 @@ unicode-script = { version = "0.5.7", optional = true }
112113
integer-sqrt = { version = "0.1.5" }
113114
bytemuck = { workspace = true, optional = true, features = ["derive"] }
114115
zeno = { version = "0.3.3", optional = true, default-features = false, features = ["eval"] }
115-
sys-locale = { version = "0.3.2", optional = true }
116+
sys-locale = { version = "0.3.2", optional = true, features = ["js"] }
116117
parley = { version = "0.6.0", optional = true }
117118
pulldown-cmark = { version = "0.13.0", optional = true }
118119

internal/core/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ pub mod window;
6262
#[doc(inline)]
6363
pub use string::SharedString;
6464

65+
/// Register a font from memory.
66+
#[cfg(feature = "std")]
67+
pub use i_slint_common::sharedfontique::register_font_from_memory;
68+
69+
/// Handle to a registered font.
70+
#[cfg(feature = "std")]
71+
pub use i_slint_common::sharedfontique::FontHandle;
72+
73+
/// Error type for font registration failures.
74+
#[cfg(feature = "std")]
75+
pub use i_slint_common::sharedfontique::RegisterFontError;
76+
6577
#[doc(inline)]
6678
pub use sharedvector::SharedVector;
6779

0 commit comments

Comments
 (0)