egui’s default fonts do not contain all CJK glyphs. The following egui and eframe 0.32 examples use Source Han Serif as a fallback.
1. Native and WASM: embed the font
Put SourceHanSerifCN-VF.ttf in assets/fonts/, then register it when creating the app:
use egui::{
FontData, FontFamily,
epaint::text::{FontInsert, FontPriority, InsertFontFamily},
};
fn add_cjk_font(ctx: &egui::Context) {
ctx.add_font(FontInsert::new(
"source han serif",
FontData::from_static(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/fonts/SourceHanSerifCN-VF.ttf"
))),
vec![InsertFontFamily {
family: FontFamily::Proportional,
priority: FontPriority::Lowest,
}],
));
}
impl MyApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
add_cjk_font(&cc.egui_ctx);
Self::default()
}
}
include_bytes! works for both native and WASM, but adds the font to the application binary.
2. Native: system font; WASM: remote font
Use fontdb to read an installed font on native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
fontdb = "0.24"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
fn install_cjk_font(ctx: &egui::Context, font_data: egui::FontData) {
ctx.add_font(egui::epaint::text::FontInsert::new(
"cjk",
font_data,
vec![egui::epaint::text::InsertFontFamily {
family: egui::FontFamily::Proportional,
priority: egui::epaint::text::FontPriority::Lowest,
}],
));
}
#[cfg(not(target_arch = "wasm32"))]
fn add_system_cjk_font(ctx: &egui::Context) {
let mut database = fontdb::Database::new();
database.load_system_fonts();
let id = [
"Noto Sans CJK SC",
"Source Han Sans SC",
"Microsoft YaHei",
"PingFang SC",
]
.iter()
.find_map(|&name| {
let families = [fontdb::Family::Name(name)];
database.query(&fontdb::Query {
families: &families,
..Default::default()
})
})
.expect("no system CJK font found");
let (bytes, face_index) = database
.with_face_data(id, |bytes, index| (bytes.to_vec(), index))
.expect("failed to read system CJK font");
let mut font_data = egui::FontData::from_owned(bytes);
font_data.index = face_index;
install_cjk_font(ctx, font_data);
}
Call add_system_cjk_font(&cc.egui_ctx) from the native app constructor.
For WASM, export a start function that accepts font bytes:
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
#[wasm_bindgen]
pub async fn start(
canvas: HtmlCanvasElement,
font_bytes: Vec<u8>,
) -> Result<(), JsValue> {
eframe::WebRunner::new()
.start(
canvas,
eframe::WebOptions::default(),
Box::new(move |cc| {
install_cjk_font(
&cc.egui_ctx,
egui::FontData::from_owned(font_bytes),
);
Ok(Box::new(MyApp::default()))
}),
)
.await
.map_err(|error| JsValue::from_str(&format!("{error:?}")))
}
Load the remote font in TypeScript and inject it into WASM:
import init, { start } from "./pkg/my_app.js";
const fontUrl = "https://cdn.example.com/SourceHanSerifCN-VF.ttf";
const [, response] = await Promise.all([init(), fetch(fontUrl)]);
if (!response.ok) throw new Error(`font download failed: ${response.status}`);
const canvas = document.querySelector<HTMLCanvasElement>("#egui_canvas")!;
const fontBytes = new Uint8Array(await response.arrayBuffer());
await start(canvas, fontBytes);
In both approaches, FontPriority::Lowest keeps the default font for Latin text and uses the CJK font only as a fallback.