Skip to main content

egui_system_fonts/
lib.rs

1//! System font helpers for `egui`.
2//!
3//! This crate resolves platform-installed font families and applies them to an `egui::Context`.
4//!
5//! # Quick start
6//!
7//! Replace `egui`'s default fonts using the current system locale:
8//!
9//! ```no_run
10//! # use egui_system_fonts::{set_auto, FontStyle};
11//! # fn demo(ctx: &egui::Context) {
12//! set_auto(ctx, FontStyle::Sans);
13//! # }
14//! ```
15//!
16//! Add system fonts as fallback only (keeps existing font priority):
17//!
18//! ```no_run
19//! # use egui_system_fonts::{extend_auto, FontStyle};
20//! # fn demo(ctx: &egui::Context) {
21//! let mut defs = egui::FontDefinitions::default();
22//! extend_auto(ctx, &mut defs, FontStyle::Sans);
23//! # }
24//! ```
25//!
26use egui::{FontData, FontDefinitions, FontFamily};
27use std::collections::BTreeMap;
28use system_fonts::FoundFontSource;
29pub use system_fonts::{FontPreset, FontRegion, FontStyle};
30
31/// Replaces `egui` font definitions with system fonts detected from the current system locale.
32///
33/// This overwrites the default `egui` fonts. If no matching fonts are found, the context is left unchanged
34/// and an empty list is returned.
35///
36/// # Examples
37///
38/// ```no_run
39/// # use egui_system_fonts::{set_auto, FontStyle};
40/// # fn demo(ctx: &egui::Context) {
41/// set_auto(ctx, FontStyle::Sans);
42/// # }
43/// ```
44pub fn set_auto(ctx: &egui::Context, style: FontStyle) -> Vec<String> {
45    let (locale, region, fonts) = system_fonts::find_for_system_locale(style);
46    log::info!(
47        "Detected locale: {:?}, region: {:?}, style: {:?}, candidates: {}",
48        locale,
49        region,
50        style,
51        fonts.len()
52    );
53    set_found_fonts(ctx, fonts)
54}
55
56/// Replaces `egui` font definitions with system fonts for the given region.
57///
58/// This overwrites the default `egui` fonts. If no matching fonts are found, the context is left unchanged
59/// and an empty list is returned.
60///
61/// # Examples
62///
63/// ```no_run
64/// # use egui_system_fonts::{set_with_region, FontRegion, FontStyle};
65/// # fn demo(ctx: &egui::Context) {
66/// set_with_region(ctx, FontRegion::Korean, FontStyle::Sans);
67/// # }
68/// ```
69pub fn set_with_region(ctx: &egui::Context, region: FontRegion, style: FontStyle) -> Vec<String> {
70    let presets = system_fonts::presets_for_region(region);
71    set_with_presets(ctx, presets, style)
72}
73
74/// Replaces `egui` font definitions with system fonts resolved from the given presets.
75///
76/// Presets are evaluated in priority order. If no matching fonts are found, the context is left unchanged
77/// and an empty list is returned.
78///
79/// # Examples
80///
81/// ```no_run
82/// # use egui_system_fonts::{set_with_presets, FontPreset, FontStyle};
83/// # fn demo(ctx: &egui::Context) {
84/// let presets = [FontPreset::Korean, FontPreset::Latin];
85/// set_with_presets(ctx, presets, FontStyle::Sans);
86/// # }
87/// ```
88pub fn set_with_presets<I>(ctx: &egui::Context, presets: I, style: FontStyle) -> Vec<String>
89where
90    I: IntoIterator<Item = FontPreset>,
91{
92    let fonts = system_fonts::find_from_presets(presets, style);
93    set_found_fonts(ctx, fonts)
94}
95
96/// Appends system fonts as fallback families to an existing `FontDefinitions`.
97///
98/// This keeps existing font priority and only adds additional fallback families at the end.
99/// If at least one font is added, the updated definitions are applied to `ctx`.
100///
101/// Returns the newly added font family names (in priority order). If nothing is added, returns an empty list
102/// and does not modify the context.
103///
104/// # Examples
105///
106/// ```no_run
107/// # use egui_system_fonts::{extend_auto, FontStyle};
108/// # fn demo(ctx: &egui::Context) {
109/// let mut defs = egui::FontDefinitions::default();
110/// extend_auto(ctx, &mut defs, FontStyle::Sans);
111/// # }
112/// ```
113pub fn extend_auto(
114    ctx: &egui::Context,
115    defs: &mut FontDefinitions,
116    style: FontStyle,
117) -> Vec<String> {
118    let (locale, region, fonts) = system_fonts::find_for_system_locale(style);
119    log::info!(
120        "Detected locale: {:?}, region: {:?}, style: {:?}, candidates: {}",
121        locale,
122        region,
123        style,
124        fonts.len()
125    );
126    let installed = append_found_fonts(defs, fonts);
127    if !installed.is_empty() {
128        ctx.set_fonts(defs.clone());
129    }
130    installed
131}
132
133/// Appends system fonts for the given region as fallback families to an existing `FontDefinitions`.
134///
135/// If at least one font is added, the updated definitions are applied to `ctx`.
136/// Returns the newly added font family names (in priority order).
137///
138/// # Examples
139///
140/// ```no_run
141/// # use egui_system_fonts::{extend_with_region, FontRegion, FontStyle};
142/// # fn demo(ctx: &egui::Context) {
143/// let mut defs = egui::FontDefinitions::default();
144/// extend_with_region(ctx, &mut defs, FontRegion::Japanese, FontStyle::Sans);
145/// # }
146/// ```
147pub fn extend_with_region(
148    ctx: &egui::Context,
149    defs: &mut FontDefinitions,
150    region: FontRegion,
151    style: FontStyle,
152) -> Vec<String> {
153    let presets = system_fonts::presets_for_region(region);
154    extend_with_presets(ctx, defs, presets, style)
155}
156
157/// Appends system fonts resolved from the given presets as fallback families to an existing `FontDefinitions`.
158///
159/// Presets are evaluated in priority order. If at least one font is added, the updated definitions are applied
160/// to `ctx`. Returns the newly added font family names (in priority order).
161///
162/// # Examples
163///
164/// ```no_run
165/// # use egui_system_fonts::{extend_with_presets, FontPreset, FontStyle};
166/// # fn demo(ctx: &egui::Context) {
167/// let mut defs = egui::FontDefinitions::default();
168/// let presets = [FontPreset::TraditionalChinese, FontPreset::Latin];
169/// extend_with_presets(ctx, &mut defs, presets, FontStyle::Serif);
170/// # }
171/// ```
172pub fn extend_with_presets<I>(
173    ctx: &egui::Context,
174    defs: &mut FontDefinitions,
175    presets: I,
176    style: FontStyle,
177) -> Vec<String>
178where
179    I: IntoIterator<Item = FontPreset>,
180{
181    let fonts = system_fonts::find_from_presets(presets, style);
182    let installed = append_found_fonts(defs, fonts);
183    if !installed.is_empty() {
184        ctx.set_fonts(defs.clone());
185    }
186    installed
187}
188
189fn set_found_fonts(ctx: &egui::Context, fonts: Vec<system_fonts::FoundFont>) -> Vec<String> {
190    let mut defs = FontDefinitions::default();
191
192    let mut installed_names: Vec<String> = Vec::new();
193    let mut keys_in_priority: Vec<String> = Vec::new();
194
195    for f in fonts {
196        let Some(bytes) = read_font_bytes(f.source) else {
197            continue;
198        };
199
200        defs.font_data
201            .insert(f.key.clone(), FontData::from_owned(bytes).into());
202
203        keys_in_priority.push(f.key.clone());
204        installed_names.push(f.family);
205    }
206
207    if installed_names.is_empty() {
208        log::warn!("No matching system fonts found.");
209        return vec![];
210    }
211
212    for key in keys_in_priority.into_iter().rev() {
213        insert_front(&mut defs.families, FontFamily::Proportional, key.clone());
214        insert_front(&mut defs.families, FontFamily::Monospace, key);
215    }
216
217    ctx.set_fonts(defs);
218    log::info!("Set fonts (family names): {:?}", installed_names);
219
220    installed_names
221}
222
223fn append_found_fonts(
224    defs: &mut FontDefinitions,
225    fonts: Vec<system_fonts::FoundFont>,
226) -> Vec<String> {
227    let mut installed_names: Vec<String> = Vec::new();
228    let mut keys_in_priority: Vec<String> = Vec::new();
229
230    for f in fonts {
231        if defs.font_data.contains_key(&f.key) {
232            continue;
233        }
234
235        let Some(bytes) = read_font_bytes(f.source) else {
236            continue;
237        };
238
239        defs.font_data
240            .insert(f.key.clone(), FontData::from_owned(bytes).into());
241
242        keys_in_priority.push(f.key.clone());
243        installed_names.push(f.family);
244    }
245
246    if installed_names.is_empty() {
247        return vec![];
248    }
249
250    for key in keys_in_priority.into_iter() {
251        insert_back(&mut defs.families, FontFamily::Proportional, key.clone());
252        insert_back(&mut defs.families, FontFamily::Monospace, key);
253    }
254
255    installed_names
256}
257
258fn read_font_bytes(source: FoundFontSource) -> Option<Vec<u8>> {
259    match source {
260        FoundFontSource::Path(path) => match std::fs::read(&path) {
261            Ok(b) => Some(b),
262            Err(e) => {
263                log::debug!("Failed to read font file {:?}: {}", path, e);
264                None
265            }
266        },
267        FoundFontSource::Bytes(b) => Some(b.as_ref().to_vec()),
268    }
269}
270
271fn insert_front(families: &mut BTreeMap<FontFamily, Vec<String>>, family: FontFamily, key: String) {
272    let list = families.entry(family).or_default();
273    if list.iter().any(|k| k == &key) {
274        return;
275    }
276    list.insert(0, key);
277}
278
279fn insert_back(families: &mut BTreeMap<FontFamily, Vec<String>>, family: FontFamily, key: String) {
280    let list = families.entry(family).or_default();
281    if list.iter().any(|k| k == &key) {
282        return;
283    }
284    list.push(key);
285}