1#[cfg(feature = "gpui-fast")]
58extern crate gpui_fast as gpui;
59
60use std::{
61 borrow::Cow,
62 collections::{HashMap, HashSet},
63 sync::{Arc, LazyLock, Mutex},
64};
65
66use gpui::{
67 AnyElement, App, AssetSource, Element as _, IntoElement, RenderOnce, SharedString, Styled,
68 ViewElement, Window, svg,
69};
70pub use gpui_kit_assets::IconNamed;
71
72#[cfg(doctest)]
74#[doc = include_str!("../README.md")]
75struct ReadmeDoctests;
76
77pub const PATH_PREFIX: &str = "sf-symbols/";
79
80#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
82pub enum Weight {
83 UltraLight,
84 Thin,
85 Light,
86 #[default]
87 Regular,
88 Medium,
89 Semibold,
90 Bold,
91 Heavy,
92 Black,
93}
94
95impl Weight {
96 pub const ALL: [Weight; 9] = [
98 Weight::UltraLight,
99 Weight::Thin,
100 Weight::Light,
101 Weight::Regular,
102 Weight::Medium,
103 Weight::Semibold,
104 Weight::Bold,
105 Weight::Heavy,
106 Weight::Black,
107 ];
108
109 pub const fn as_str(self) -> &'static str {
111 match self {
112 Weight::UltraLight => "ultralight",
113 Weight::Thin => "thin",
114 Weight::Light => "light",
115 Weight::Regular => "regular",
116 Weight::Medium => "medium",
117 Weight::Semibold => "semibold",
118 Weight::Bold => "bold",
119 Weight::Heavy => "heavy",
120 Weight::Black => "black",
121 }
122 }
123
124 pub fn from_name(name: &str) -> Option<Self> {
126 Self::ALL.into_iter().find(|weight| weight.as_str() == name)
127 }
128}
129
130impl std::fmt::Display for Weight {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.write_str(self.as_str())
133 }
134}
135
136impl std::str::FromStr for Weight {
137 type Err = ParseError;
138
139 fn from_str(name: &str) -> Result<Self, ParseError> {
141 Self::from_name(name).ok_or(ParseError(()))
142 }
143}
144
145#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
148pub enum Rendering {
149 #[default]
151 Monochrome,
152 Hierarchical,
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct ParseError(());
159
160impl std::fmt::Display for ParseError {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 f.write_str("unknown SF Symbols weight name")
163 }
164}
165
166impl std::error::Error for ParseError {}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
177pub struct SfSymbol {
178 name: &'static str,
179 weight: Weight,
180 rendering: Rendering,
181 variable_value: Option<u8>,
183}
184
185impl SfSymbol {
186 pub fn new(name: &str) -> Self {
188 static NAMES: LazyLock<Mutex<HashSet<&'static str>>> = LazyLock::new(Default::default);
189
190 let mut names = NAMES.lock().unwrap();
191 let name = match names.get(name) {
192 Some(name) => name,
193 None => {
194 let name: &'static str = Box::leak(name.into());
195 names.insert(name);
196 name
197 }
198 };
199 Self::from_static(name)
200 }
201
202 pub const fn from_static(name: &'static str) -> Self {
204 Self {
205 name,
206 weight: Weight::Regular,
207 rendering: Rendering::Monochrome,
208 variable_value: None,
209 }
210 }
211
212 pub const fn name(self) -> &'static str {
214 self.name
215 }
216
217 pub const fn get_weight(self) -> Weight {
218 self.weight
219 }
220
221 pub const fn get_rendering(self) -> Rendering {
222 self.rendering
223 }
224
225 pub fn get_variable_value(self) -> Option<f32> {
227 self.variable_value.map(|percent| f32::from(percent) / 100.)
228 }
229
230 pub fn weight(mut self, weight: Weight) -> Self {
232 self.weight = weight;
233 self
234 }
235
236 pub fn ultralight(self) -> Self {
237 self.weight(Weight::UltraLight)
238 }
239
240 pub fn thin(self) -> Self {
241 self.weight(Weight::Thin)
242 }
243
244 pub fn light(self) -> Self {
245 self.weight(Weight::Light)
246 }
247
248 pub fn regular(self) -> Self {
249 self.weight(Weight::Regular)
250 }
251
252 pub fn medium(self) -> Self {
253 self.weight(Weight::Medium)
254 }
255
256 pub fn semibold(self) -> Self {
257 self.weight(Weight::Semibold)
258 }
259
260 pub fn bold(self) -> Self {
261 self.weight(Weight::Bold)
262 }
263
264 pub fn heavy(self) -> Self {
265 self.weight(Weight::Heavy)
266 }
267
268 pub fn black(self) -> Self {
269 self.weight(Weight::Black)
270 }
271
272 pub fn rendering(mut self, rendering: Rendering) -> Self {
274 self.rendering = rendering;
275 self
276 }
277
278 pub fn hierarchical(self) -> Self {
280 self.rendering(Rendering::Hierarchical)
281 }
282
283 pub fn monochrome(self) -> Self {
285 self.rendering(Rendering::Monochrome)
286 }
287
288 pub fn variable_value(mut self, value: f32) -> Self {
292 self.variable_value = Some((value.clamp(0., 1.) * 100.).round() as u8);
293 self
294 }
295
296 pub fn path(self) -> SharedString {
298 let mut path = format!("{PATH_PREFIX}{}", self.weight);
299 if self.rendering == Rendering::Hierarchical {
300 path.push_str("+hierarchical");
301 }
302 if let Some(percent) = self.variable_value {
303 path.push_str(&format!("+v{percent}"));
304 }
305 path.push('/');
306 path.push_str(self.name);
307 path.push_str(".svg");
308 path.into()
309 }
310
311 pub fn from_path(path: &str) -> Option<Self> {
313 let rest = path.strip_prefix(PATH_PREFIX)?.strip_suffix(".svg")?;
314 let (style, name) = rest.split_once('/')?;
315 if name.is_empty() || name.contains('/') {
316 return None;
317 }
318 let mut parts = style.split('+');
319 let mut symbol = Self::new(name).weight(Weight::from_name(parts.next()?)?);
320 for part in parts {
321 if part == "hierarchical" && symbol.rendering == Rendering::Monochrome {
322 symbol.rendering = Rendering::Hierarchical;
323 } else if let Some(percent) = part.strip_prefix('v')
324 && symbol.variable_value.is_none()
325 {
326 symbol.variable_value = Some(percent.parse().ok().filter(|p| *p <= 100)?);
327 } else {
328 return None;
329 }
330 }
331 Some(symbol)
332 }
333
334 pub fn exists(self) -> bool {
336 shim::exists(self.name)
337 }
338
339 pub fn svg(self) -> Option<Arc<[u8]>> {
345 type Cache = HashMap<SfSymbol, Option<Arc<[u8]>>>;
346 static CACHE: LazyLock<Mutex<Cache>> = LazyLock::new(Default::default);
347
348 if let Some(cached) = CACHE.lock().unwrap().get(&self) {
349 return cached.clone();
350 }
351 let svg = shim::render(self);
354 if svg.is_some() || !self.exists() {
357 CACHE.lock().unwrap().insert(self, svg.clone());
358 }
359 svg
360 }
361}
362
363pub fn is_available() -> bool {
365 shim::available()
366}
367
368impl From<&'static str> for SfSymbol {
369 fn from(name: &'static str) -> Self {
370 Self::from_static(name)
371 }
372}
373
374impl From<&String> for SfSymbol {
375 fn from(name: &String) -> Self {
376 Self::new(name)
377 }
378}
379
380impl From<&SharedString> for SfSymbol {
381 fn from(name: &SharedString) -> Self {
382 Self::new(name)
383 }
384}
385
386impl std::fmt::Display for SfSymbol {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 f.write_str(self.name)
390 }
391}
392
393impl IconNamed for SfSymbol {
394 fn path(self) -> SharedString {
395 SfSymbol::path(self)
396 }
397}
398
399impl IntoElement for SfSymbol {
406 type Element = ViewElement<Self>;
407
408 #[track_caller]
409 fn into_element(self) -> Self::Element {
410 ViewElement::new(self)
411 }
412
413 #[track_caller]
414 fn into_any_element(self) -> AnyElement {
415 self.into_element().into_any()
416 }
417}
418
419impl RenderOnce for SfSymbol {
420 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
421 let text_style = window.text_style();
422 svg()
423 .path(self.path())
424 .flex_shrink_0()
425 .size(text_style.font_size.to_pixels(window.rem_size()))
426 .text_color(text_style.color)
427 }
428}
429
430impl From<SfSymbol> for AnyElement {
431 fn from(symbol: SfSymbol) -> Self {
432 symbol.into_any_element()
433 }
434}
435
436#[derive(Clone, Copy, Debug, Default)]
446pub struct SfSymbolAssets;
447
448impl SfSymbolAssets {
449 pub fn with<B: AssetSource>(self, fallback: B) -> Layered<Self, B> {
451 Layered(self, fallback)
452 }
453}
454
455impl AssetSource for SfSymbolAssets {
456 fn load(&self, path: &str) -> gpui::Result<Option<Cow<'static, [u8]>>> {
457 Ok(SfSymbol::from_path(path)
458 .and_then(|symbol| symbol.svg())
459 .map(|svg| Cow::Owned(svg.to_vec())))
460 }
461
462 fn list(&self, _path: &str) -> gpui::Result<Vec<SharedString>> {
464 Ok(Vec::new())
465 }
466}
467
468#[derive(Clone, Copy, Debug, Default)]
471pub struct Layered<A, B>(pub A, pub B);
472
473impl<A, B> Layered<A, B> {
474 pub fn with<C: AssetSource>(self, fallback: C) -> Layered<Self, C> {
476 Layered(self, fallback)
477 }
478}
479
480impl<A: AssetSource, B: AssetSource> AssetSource for Layered<A, B> {
481 fn load(&self, path: &str) -> gpui::Result<Option<Cow<'static, [u8]>>> {
482 match self.0.load(path)? {
483 Some(bytes) => Ok(Some(bytes)),
484 None => self.1.load(path),
485 }
486 }
487
488 fn list(&self, path: &str) -> gpui::Result<Vec<SharedString>> {
489 let mut paths = self.0.list(path)?;
490 paths.extend(self.1.list(path)?);
491 paths.sort();
492 paths.dedup();
493 Ok(paths)
494 }
495}
496
497#[cfg(sf_symbols_shim)]
499mod shim {
500 use std::{
501 ffi::{CString, c_char, c_int, c_void},
502 sync::{Arc, mpsc},
503 time::Duration,
504 };
505
506 use crate::{Rendering, SfSymbol};
507
508 const MAIN_THREAD_TIMEOUT: Duration = Duration::from_secs(2);
510
511 unsafe extern "C" {
512 fn gpui_sf_symbol_svg(
514 name: *const c_char,
515 weight: i32,
516 hierarchical: bool,
517 variable_value: f64,
518 length: *mut usize,
519 ) -> *mut u8;
520 fn gpui_sf_symbol_free(buffer: *mut u8);
521 fn gpui_sf_symbol_exists(name: *const c_char) -> bool;
522 fn gpui_sf_symbol_available() -> bool;
523
524 fn pthread_main_np() -> c_int;
526 static _dispatch_main_q: c_void;
527 fn dispatch_async_f(
528 queue: *const c_void,
529 context: *mut c_void,
530 work: unsafe extern "C" fn(*mut c_void),
531 );
532 }
533
534 pub fn available() -> bool {
535 unsafe { gpui_sf_symbol_available() }
536 }
537
538 pub fn exists(name: &str) -> bool {
539 let Ok(name) = CString::new(name) else {
540 return false;
541 };
542 unsafe { gpui_sf_symbol_exists(name.as_ptr()) }
543 }
544
545 pub fn render(symbol: SfSymbol) -> Option<Arc<[u8]>> {
546 if unsafe { pthread_main_np() } != 0 {
547 return render_on_main(symbol);
548 }
549
550 type Job = (SfSymbol, mpsc::Sender<Option<Arc<[u8]>>>);
553 unsafe extern "C" fn run(context: *mut c_void) {
554 let (symbol, done) = *unsafe { Box::from_raw(context.cast::<Job>()) };
555 done.send(render_on_main(symbol)).ok();
556 }
557 let (done, result) = mpsc::channel();
558 let job: Box<Job> = Box::new((symbol, done));
559 unsafe {
560 dispatch_async_f(&raw const _dispatch_main_q, Box::into_raw(job).cast(), run);
561 }
562 result.recv_timeout(MAIN_THREAD_TIMEOUT).ok().flatten()
563 }
564
565 fn render_on_main(symbol: SfSymbol) -> Option<Arc<[u8]>> {
566 let name = CString::new(symbol.name()).ok()?;
567 let variable_value = symbol.get_variable_value().map_or(f64::NAN, f64::from);
568 let mut length = 0;
569 unsafe {
570 let buffer = gpui_sf_symbol_svg(
571 name.as_ptr(),
572 symbol.weight as i32,
574 symbol.rendering == Rendering::Hierarchical,
575 variable_value,
576 &mut length,
577 );
578 if buffer.is_null() {
579 return None;
580 }
581 let svg = Arc::from(std::slice::from_raw_parts(buffer, length));
582 gpui_sf_symbol_free(buffer);
583 Some(svg)
584 }
585 }
586}
587
588#[cfg(not(sf_symbols_shim))]
589mod shim {
590 use std::sync::Arc;
591
592 use crate::SfSymbol;
593
594 pub fn available() -> bool {
595 false
596 }
597
598 pub fn exists(_name: &str) -> bool {
599 false
600 }
601
602 pub fn render(_symbol: SfSymbol) -> Option<Arc<[u8]>> {
603 None
604 }
605}