material_ui_rs/design/
fonts.rs1use iced_widget::Text;
4use iced_widget::core::Font;
5use iced_widget::core::font::{Family, Stretch, Style, Weight};
6use iced_widget::core::text as core_text;
7use iced_widget::text::{self, LineHeight};
8
9use crate::{Theme, tokens};
10
11use std::borrow::Cow;
12use std::fmt;
13
14#[cfg(target_arch = "wasm32")]
15#[path = "web_font.rs"]
16mod web_font;
17
18pub const ROBOTO_FAMILY: &str = "Roboto";
19pub const NOTO_SANS_CJK_SC_FAMILY: &str = "Noto Sans CJK SC";
20pub const MATERIAL_SYMBOLS_ROUNDED_FAMILY: &str = "Material Symbols Rounded";
21pub const MATERIAL_SYMBOLS_ROUNDED_FILLED_FAMILY: &str = "Material Symbols Rounded Filled";
22
23pub const ROBOTO_REGULAR_BYTES: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf");
24pub const ROBOTO_MEDIUM_BYTES: &[u8] = include_bytes!("../fonts/Roboto-Medium.ttf");
25pub const ROBOTO_BOLD_BYTES: &[u8] = include_bytes!("../fonts/Roboto-Bold.ttf");
26pub const MATERIAL_SYMBOLS_ROUNDED_BYTES: &[u8] =
27 include_bytes!("../fonts/MaterialSymbolsRounded-Regular.ttf");
28pub const MATERIAL_SYMBOLS_ROUNDED_FILLED_BYTES: &[u8] =
29 include_bytes!("../fonts/MaterialSymbolsRounded-Filled.ttf");
30
31pub const ROBOTO: Font = roboto_for_weight(tokens::typography::WEIGHT_REGULAR);
32pub const ROBOTO_MEDIUM: Font = roboto_for_weight(tokens::typography::WEIGHT_MEDIUM);
33pub const ROBOTO_BOLD: Font = roboto_for_weight(tokens::typography::WEIGHT_BOLD);
34pub const NOTO_SANS_CJK_SC: Font = noto_sans_cjk_sc_for_weight(tokens::typography::WEIGHT_REGULAR);
35pub const NOTO_SANS_CJK_SC_MEDIUM: Font =
36 noto_sans_cjk_sc_for_weight(tokens::typography::WEIGHT_MEDIUM);
37pub const NOTO_SANS_CJK_SC_BOLD: Font =
38 noto_sans_cjk_sc_for_weight(tokens::typography::WEIGHT_BOLD);
39pub const MATERIAL_SYMBOLS_ROUNDED: Font = Font {
40 family: Family::Name(MATERIAL_SYMBOLS_ROUNDED_FAMILY),
41 weight: Weight::Normal,
42 stretch: Stretch::Normal,
43 style: Style::Normal,
44};
45pub const MATERIAL_SYMBOLS_ROUNDED_FILLED: Font = Font {
46 family: Family::Name(MATERIAL_SYMBOLS_ROUNDED_FILLED_FAMILY),
47 weight: Weight::Normal,
48 stretch: Stretch::Normal,
49 style: Style::Normal,
50};
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum WebFontError {
55 UnsupportedPlatform,
57 MissingBrowserApi(&'static str),
59 RequestFailed,
61 HttpStatus(u16),
63 ReadFailed,
65 UnsupportedFormat,
67 FontLoad(iced::font::Error),
69}
70
71impl fmt::Display for WebFontError {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::UnsupportedPlatform => {
75 formatter.write_str("web fonts can only be fetched on WebAssembly")
76 }
77 Self::MissingBrowserApi(api) => write!(formatter, "browser API `{api}` is unavailable"),
78 Self::RequestFailed => formatter.write_str("the browser rejected the font request"),
79 Self::HttpStatus(status) => {
80 write!(formatter, "the font server returned HTTP status {status}")
81 }
82 Self::ReadFailed => formatter.write_str("the font response body could not be read"),
83 Self::UnsupportedFormat => formatter.write_str(
84 "the downloaded file is not a TrueType, OpenType, or TrueType Collection font",
85 ),
86 Self::FontLoad(_) => formatter.write_str("the renderer could not load the web font"),
87 }
88 }
89}
90
91impl std::error::Error for WebFontError {}
92
93pub fn all() -> [Cow<'static, [u8]>; 5] {
94 [
95 Cow::Borrowed(ROBOTO_REGULAR_BYTES),
96 Cow::Borrowed(ROBOTO_MEDIUM_BYTES),
97 Cow::Borrowed(ROBOTO_BOLD_BYTES),
98 Cow::Borrowed(MATERIAL_SYMBOLS_ROUNDED_BYTES),
99 Cow::Borrowed(MATERIAL_SYMBOLS_ROUNDED_FILLED_BYTES),
100 ]
101}
102
103pub fn load_web_font(url: impl Into<String>) -> iced::Task<Result<(), WebFontError>> {
122 #[cfg(target_arch = "wasm32")]
123 {
124 web_font::load(url.into())
125 }
126
127 #[cfg(not(target_arch = "wasm32"))]
128 {
129 let _ = url.into();
130
131 iced::Task::done(Err(WebFontError::UnsupportedPlatform))
132 }
133}
134
135pub const fn roboto_for_type_scale(scale: tokens::typography::TypeScale) -> Font {
136 roboto_for_weight(scale.weight)
137}
138
139pub const fn noto_sans_cjk_sc_for_type_scale(scale: tokens::typography::TypeScale) -> Font {
140 noto_sans_cjk_sc_for_weight(scale.weight)
141}
142
143pub fn font_for_content_type_scale(content: &str, scale: tokens::typography::TypeScale) -> Font {
144 if contains_cjk(content) {
145 noto_sans_cjk_sc_for_type_scale(scale)
146 } else {
147 roboto_for_type_scale(scale)
148 }
149}
150
151pub const fn roboto_for_weight(weight: u16) -> Font {
152 Font {
153 family: Family::Name(ROBOTO_FAMILY),
154 weight: match weight {
155 tokens::typography::WEIGHT_BOLD => Weight::Bold,
156 tokens::typography::WEIGHT_MEDIUM => Weight::Medium,
157 _ => Weight::Normal,
158 },
159 stretch: Stretch::Normal,
160 style: Style::Normal,
161 }
162}
163
164pub const fn noto_sans_cjk_sc_for_weight(weight: u16) -> Font {
165 Font {
166 family: Family::Name(NOTO_SANS_CJK_SC_FAMILY),
167 weight: match weight {
168 tokens::typography::WEIGHT_BOLD => Weight::Bold,
169 tokens::typography::WEIGHT_MEDIUM => Weight::Medium,
170 _ => Weight::Normal,
171 },
172 stretch: Stretch::Normal,
173 style: Style::Normal,
174 }
175}
176
177pub fn contains_cjk(content: &str) -> bool {
178 content.chars().any(is_cjk_codepoint)
179}
180
181pub fn material_symbol_codepoint(name: &str) -> Option<char> {
182 let codepoint = match name {
183 "info" => 0xe88e,
184 "input" => 0xe890,
185 "layers" => 0xe53b,
186 "menu" => 0xe5d2,
187 "navigation" => 0xe55d,
188 "tune" => 0xe429,
189 _ => return None,
190 };
191
192 char::from_u32(codepoint)
193}
194
195fn material_symbol_fragment<'a>(name: impl text::IntoFragment<'a>) -> text::Fragment<'a> {
196 let fragment = name.into_fragment();
197
198 material_symbol_codepoint(fragment.as_ref())
199 .map(|codepoint| text::Fragment::Owned(codepoint.to_string()))
200 .unwrap_or(fragment)
201}
202
203fn is_cjk_codepoint(character: char) -> bool {
204 matches!(
205 character,
206 '\u{2E80}'..='\u{2EFF}'
207 | '\u{3000}'..='\u{303F}'
208 | '\u{3040}'..='\u{30FF}'
209 | '\u{3100}'..='\u{312F}'
210 | '\u{31A0}'..='\u{31BF}'
211 | '\u{31F0}'..='\u{31FF}'
212 | '\u{3400}'..='\u{4DBF}'
213 | '\u{4E00}'..='\u{9FFF}'
214 | '\u{AC00}'..='\u{D7AF}'
215 | '\u{F900}'..='\u{FAFF}'
216 | '\u{20000}'..='\u{2A6DF}'
217 | '\u{2A700}'..='\u{2B73F}'
218 | '\u{2B740}'..='\u{2B81F}'
219 | '\u{2B820}'..='\u{2CEAF}'
220 | '\u{2CEB0}'..='\u{2EBEF}'
221 | '\u{30000}'..='\u{323AF}'
222 )
223}
224
225#[cfg(any(target_arch = "wasm32", test))]
226fn is_supported_web_font(bytes: &[u8]) -> bool {
227 bytes.starts_with(&[0x00, 0x01, 0x00, 0x00])
228 || bytes.starts_with(b"OTTO")
229 || bytes.starts_with(b"ttcf")
230}
231
232pub fn icon<'a, Renderer>(name: impl text::IntoFragment<'a>, size: f32) -> Text<'a, Theme, Renderer>
233where
234 Renderer: core_text::Renderer,
235 Font: Into<Renderer::Font>,
236{
237 Text::new(material_symbol_fragment(name))
238 .font(MATERIAL_SYMBOLS_ROUNDED)
239 .size(size)
240 .line_height(LineHeight::Absolute(size.into()))
241 .shaping(text::Shaping::Advanced)
242}
243
244pub fn filled_icon<'a, Renderer>(
245 name: impl text::IntoFragment<'a>,
246 size: f32,
247) -> Text<'a, Theme, Renderer>
248where
249 Renderer: core_text::Renderer,
250 Font: Into<Renderer::Font>,
251{
252 Text::new(material_symbol_fragment(name))
253 .font(MATERIAL_SYMBOLS_ROUNDED_FILLED)
254 .size(size)
255 .line_height(LineHeight::Absolute(size.into()))
256 .shaping(text::Shaping::Advanced)
257}
258
259#[cfg(test)]
260#[path = "../../tests/design/fonts.rs"]
261mod tests;