armas_basic/fonts/mod.rs
1//! Font utilities for loading common fonts with multiple weights
2//!
3//! This module provides helper functions to easily load popular fonts
4//! with multiple weights (regular, medium, semibold, bold) for use in
5//! Armas components.
6//!
7//! Since egui doesn't natively support font weights, this module works
8//! around the limitation by loading separate font files for each weight
9//! and registering them as separate font families.
10//!
11//! ## Usage
12//!
13//! ```rust,ignore
14//! use armas_basic::fonts::{FontWeight, FontFamilyBuilder};
15//! use eframe::egui;
16//!
17//! fn setup_fonts(ctx: &egui::Context) {
18//! // Build font definitions
19//! let mut builder = FontFamilyBuilder::new();
20//!
21//! builder.add_family(
22//! "Inter",
23//! include_bytes!("../../fonts/Inter-Regular.ttf"),
24//! Some(include_bytes!("../../fonts/Inter-Medium.ttf")),
25//! Some(include_bytes!("../../fonts/Inter-SemiBold.ttf")),
26//! Some(include_bytes!("../../fonts/Inter-Bold.ttf")),
27//! );
28//!
29//! // Apply to context (must be done before first frame or in CreationContext)
30//! builder.install(ctx, true); // true = set as default
31//! }
32//!
33//! fn my_button(ui: &mut egui::Ui) {
34//! // Use the medium weight variant
35//! let font = FontWeight::medium("Inter", 14.0);
36//! ui.label(egui::RichText::new("Click me").font(font));
37//! }
38//! ```
39
40use egui::{Context, FontData, FontDefinitions, FontFamily, FontId};
41use std::collections::HashMap;
42use std::sync::Arc;
43
44/// Font weight variants
45///
46/// Since egui doesn't natively support font weights, we simulate them
47/// by using separate font families for each weight.
48pub struct FontWeight;
49
50impl FontWeight {
51 /// Get `FontId` for regular weight (400)
52 #[must_use]
53 pub fn regular(family_name: &str, size: f32) -> FontId {
54 FontId::new(size, FontFamily::Name(family_name.into()))
55 }
56
57 /// Get `FontId` for medium weight (500)
58 #[must_use]
59 pub fn medium(family_name: &str, size: f32) -> FontId {
60 FontId::new(
61 size,
62 FontFamily::Name(format!("{family_name}Medium").into()),
63 )
64 }
65
66 /// Get `FontId` for semibold weight (600)
67 #[must_use]
68 pub fn semibold(family_name: &str, size: f32) -> FontId {
69 FontId::new(
70 size,
71 FontFamily::Name(format!("{family_name}SemiBold").into()),
72 )
73 }
74
75 /// Get `FontId` for bold weight (700)
76 #[must_use]
77 pub fn bold(family_name: &str, size: f32) -> FontId {
78 FontId::new(size, FontFamily::Name(format!("{family_name}Bold").into()))
79 }
80}
81
82/// Builder for composing multiple font families
83///
84/// This builder allows you to add multiple font families and their weights,
85/// then install them all at once into the egui context. This is more efficient
86/// and safer than calling `load_font_family` multiple times.
87///
88/// # Example
89///
90/// ```rust,ignore
91/// use armas_basic::fonts::FontFamilyBuilder;
92/// use eframe::egui;
93///
94/// fn setup(ctx: &egui::Context) {
95/// let mut builder = FontFamilyBuilder::new();
96///
97/// builder.add_family(
98/// "Inter",
99/// include_bytes!("../../fonts/Inter-Regular.ttf"),
100/// Some(include_bytes!("../../fonts/Inter-Medium.ttf")),
101/// Some(include_bytes!("../../fonts/Inter-SemiBold.ttf")),
102/// Some(include_bytes!("../../fonts/Inter-Bold.ttf")),
103/// );
104///
105/// // Install and set Inter as default
106/// builder.install(ctx, true);
107/// }
108/// ```
109pub struct FontFamilyBuilder {
110 font_data: HashMap<String, Arc<FontData>>,
111 families: HashMap<FontFamily, Vec<String>>,
112 default_family: Option<String>,
113}
114
115impl FontFamilyBuilder {
116 /// Create a new font family builder
117 #[must_use]
118 pub fn new() -> Self {
119 Self {
120 font_data: HashMap::new(),
121 families: HashMap::new(),
122 default_family: None,
123 }
124 }
125
126 /// Add a font family with multiple weights
127 ///
128 /// # Naming Convention
129 ///
130 /// Fonts are registered with the following names:
131 /// - Regular: `{family_name}` (e.g., "Inter")
132 /// - Medium: `{family_name}Medium` (e.g., "`InterMedium`")
133 /// - `SemiBold`: `{family_name}SemiBold` (e.g., "`InterSemiBold`")
134 /// - Bold: `{family_name}Bold` (e.g., "`InterBold`")
135 pub fn add_family(
136 &mut self,
137 family_name: &str,
138 regular: &'static [u8],
139 medium: Option<&'static [u8]>,
140 semibold: Option<&'static [u8]>,
141 bold: Option<&'static [u8]>,
142 ) -> &mut Self {
143 // Load regular weight (required)
144 let regular_key = format!("{}_regular", family_name.to_lowercase());
145 self.font_data.insert(
146 regular_key.clone(),
147 Arc::new(FontData::from_static(regular)),
148 );
149
150 // Create regular family
151 self.families
152 .insert(FontFamily::Name(family_name.into()), vec![regular_key]);
153
154 // Load medium weight if provided
155 if let Some(medium_data) = medium {
156 let medium_key = format!("{}_medium", family_name.to_lowercase());
157 self.font_data.insert(
158 medium_key.clone(),
159 Arc::new(FontData::from_static(medium_data)),
160 );
161
162 self.families.insert(
163 FontFamily::Name(format!("{family_name}Medium").into()),
164 vec![medium_key],
165 );
166 }
167
168 // Load semibold weight if provided
169 if let Some(semibold_data) = semibold {
170 let semibold_key = format!("{}_semibold", family_name.to_lowercase());
171 self.font_data.insert(
172 semibold_key.clone(),
173 Arc::new(FontData::from_static(semibold_data)),
174 );
175
176 self.families.insert(
177 FontFamily::Name(format!("{family_name}SemiBold").into()),
178 vec![semibold_key],
179 );
180 }
181
182 // Load bold weight if provided
183 if let Some(bold_data) = bold {
184 let bold_key = format!("{}_bold", family_name.to_lowercase());
185 self.font_data
186 .insert(bold_key.clone(), Arc::new(FontData::from_static(bold_data)));
187
188 self.families.insert(
189 FontFamily::Name(format!("{family_name}Bold").into()),
190 vec![bold_key],
191 );
192 }
193
194 self
195 }
196
197 /// Set which family should be the default proportional font
198 pub fn set_default(&mut self, family_name: &str) -> &mut Self {
199 self.default_family = Some(family_name.to_string());
200 self
201 }
202
203 /// Install the fonts into the egui context
204 ///
205 /// This merges with existing font definitions, preserving egui's default fonts.
206 ///
207 /// # Arguments
208 ///
209 /// * `ctx` - The egui context (use from `CreationContext` or before first frame)
210 /// * `set_as_default` - If true, sets the first added family as default proportional font
211 pub fn install(self, ctx: &Context, set_as_default: bool) {
212 let mut fonts = FontDefinitions::default();
213
214 // Merge our font data
215 for (key, data) in self.font_data {
216 fonts.font_data.insert(key, data);
217 }
218
219 // Merge our families
220 for (family, keys) in self.families {
221 fonts.families.insert(family, keys);
222 }
223
224 // Set default if requested
225 if set_as_default {
226 if let Some(default_family) = self.default_family.as_ref() {
227 let default_key = format!("{}_regular", default_family.to_lowercase());
228 if let Some(proportional) = fonts.families.get_mut(&FontFamily::Proportional) {
229 proportional.insert(0, default_key);
230 }
231 }
232 }
233
234 ctx.set_fonts(fonts);
235 }
236
237 /// Build `FontDefinitions` without installing to context
238 ///
239 /// Useful if you want to customize further before applying.
240 #[must_use]
241 pub fn build(self) -> FontDefinitions {
242 let mut fonts = FontDefinitions::default();
243
244 for (key, data) in self.font_data {
245 fonts.font_data.insert(key, data);
246 }
247
248 for (family, keys) in self.families {
249 fonts.families.insert(family, keys);
250 }
251
252 fonts
253 }
254}
255
256impl Default for FontFamilyBuilder {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262/// Common font recommendations for Aceternity-style UIs
263///
264/// These fonts are commonly used in modern UI design and pair well
265/// with the Armas component library.
266///
267/// ## Inter
268/// - **Use case**: General UI, buttons, labels
269/// - **Download**: <https://fonts.google.com/specimen/Inter>
270/// - **Weights**: 100-900 (use 400, 500, 600, 700)
271///
272/// ## Geist
273/// - **Use case**: Modern tech UIs
274/// - **Download**: <https://vercel.com/font>
275/// - **Weights**: Regular, Medium, `SemiBold`, Bold
276///
277/// ## `JetBrains` Mono
278/// - **Use case**: Code, monospace needs
279/// - **Download**: <https://www.jetbrains.com/lp/mono/>
280/// - **Weights**: 100-800
281///
282/// ## Example: Loading Inter
283///
284/// ```rust,ignore
285/// # let ctx = &egui::Context::default();
286/// let inter_regular = include_bytes!("../fonts/Inter-Regular.ttf");
287/// let inter_medium = include_bytes!("../fonts/Inter-Medium.ttf");
288/// let inter_semibold = include_bytes!("../fonts/Inter-SemiBold.ttf");
289/// let inter_bold = include_bytes!("../fonts/Inter-Bold.ttf");
290///
291/// armas_basic::fonts::load_font_family(
292/// ctx,
293/// "Inter",
294/// inter_regular,
295/// Some(inter_medium),
296/// Some(inter_semibold),
297/// Some(inter_bold),
298/// );
299///
300/// // Set as default
301/// armas_basic::fonts::set_default_font(ctx, "Inter");
302/// ```
303pub mod recommended {}