Skip to main content

i_slint_compiler/passes/
embed_glyphs.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use crate::CompilerConfiguration;
5use crate::diagnostics::BuildDiagnostics;
6#[cfg(not(target_arch = "wasm32"))]
7use crate::embedded_resources::{BitmapFont, BitmapGlyph, BitmapGlyphs, CharacterMapEntry};
8#[cfg(not(target_arch = "wasm32"))]
9use crate::expression_tree::BuiltinFunction;
10use crate::expression_tree::{Expression, Unit};
11use crate::object_tree::*;
12use std::collections::HashMap;
13use std::collections::HashSet;
14use std::rc::Rc;
15
16use i_slint_common::sharedfontique::{self, fontique, skrifa};
17#[cfg(not(target_arch = "wasm32"))]
18use skrifa::MetadataProvider;
19
20#[derive(Clone)]
21struct Font {
22    font: fontique::QueryFont,
23}
24
25fn swash_font_ref(font: &Font) -> swash::FontRef<'_> {
26    swash::FontRef::from_index(font.font.blob.data(), font.font.index as usize).unwrap()
27}
28
29#[cfg(target_arch = "wasm32")]
30pub fn embed_glyphs<'a>(
31    _component: &Document,
32    _compiler_config: &CompilerConfiguration,
33    _scale_factor: f64,
34    _pixel_sizes: Vec<i16>,
35    _font_weights: Vec<u16>,
36    _characters_seen: HashSet<char>,
37    _all_docs: impl Iterator<Item = &'a crate::object_tree::Document> + 'a,
38    _diag: &mut BuildDiagnostics,
39) -> bool {
40    false
41}
42
43#[cfg(not(target_arch = "wasm32"))]
44pub fn embed_glyphs<'a>(
45    doc: &Document,
46    compiler_config: &CompilerConfiguration,
47    mut pixel_sizes: Vec<i16>,
48    font_weights: Vec<u16>,
49    mut characters_seen: HashSet<char>,
50    all_docs: impl Iterator<Item = &'a crate::object_tree::Document> + 'a,
51    diag: &mut BuildDiagnostics,
52) {
53    use crate::diagnostics::Spanned;
54
55    let generic_diag_location = doc.node.as_ref().map(|n| n.to_source_location());
56    let scale_factor = compiler_config.const_scale_factor.unwrap_or(1.);
57
58    characters_seen.extend(
59        ('a'..='z')
60            .chain('A'..='Z')
61            .chain('0'..='9')
62            .chain(" '!\"#$%&()*+,-./:;<=>?@\\[]{}^_|~".chars())
63            .chain(std::iter::once('●'))
64            .chain(std::iter::once('…')),
65    );
66
67    if let Ok(sizes_str) = std::env::var("SLINT_FONT_SIZES") {
68        for custom_size_str in sizes_str.split(',') {
69            let custom_size = if let Ok(custom_size) = custom_size_str
70                .parse::<f32>()
71                .map(|size_as_float| (size_as_float * scale_factor) as i16)
72            {
73                custom_size
74            } else {
75                diag.push_error(
76                    format!(
77                        "Invalid font size '{custom_size_str}' specified in `SLINT_FONT_SIZES`"
78                    ),
79                    &generic_diag_location,
80                );
81                return;
82            };
83
84            if let Err(pos) = pixel_sizes.binary_search(&custom_size) {
85                pixel_sizes.insert(pos, custom_size)
86            }
87        }
88    }
89
90    let fallback_fonts = get_fallback_fonts();
91
92    let mut custom_fonts: HashMap<std::path::PathBuf, fontique::QueryFont> = Default::default();
93    let mut font_paths: HashMap<fontique::FamilyId, std::path::PathBuf> = Default::default();
94
95    let mut collection = sharedfontique::create_collection(false);
96
97    for doc in all_docs {
98        for (font_path, import_token) in doc.custom_fonts.iter() {
99            match std::fs::read(font_path) {
100                Err(e) => {
101                    diag.push_error(format!("Error loading font: {e}"), import_token);
102                }
103                Ok(bytes) => {
104                    if let Some(font) = collection
105                        .register_fonts(bytes.into(), None)
106                        .first()
107                        .and_then(|(id, infos)| {
108                            let info = infos.first()?;
109                            collection.get_font_for_info(*id, info)
110                        })
111                    {
112                        font_paths.insert(font.family.0, font_path.into());
113                        custom_fonts.insert(font_path.into(), font);
114                    }
115                }
116            }
117        }
118    }
119
120    let mut custom_face_error = false;
121
122    let default_fonts = if !collection.default_fonts.is_empty() {
123        collection.default_fonts.as_ref().clone()
124    } else {
125        let mut default_fonts: HashMap<std::path::PathBuf, fontique::QueryFont> =
126            Default::default();
127
128        for c in doc.exported_roots() {
129            let (family, source_location) = c
130                .root_element
131                .borrow()
132                .bindings
133                .get("default-font-family")
134                .and_then(|binding| match &binding.borrow().expression {
135                    Expression::StringLiteral(family) => {
136                        Some((Some(family.clone()), binding.borrow().span.clone()))
137                    }
138                    _ => None,
139                })
140                .unwrap_or_default();
141
142            let font = {
143                let mut query = collection.query();
144
145                query.set_families(
146                    family
147                        .as_ref()
148                        .map(|family| fontique::QueryFamily::from(family.as_str()))
149                        .into_iter()
150                        .chain(
151                            sharedfontique::FALLBACK_FAMILIES
152                                .into_iter()
153                                .map(fontique::QueryFamily::Generic),
154                        ),
155                );
156
157                let mut font = None;
158
159                query.matches_with(|queried_font| {
160                    font = Some(queried_font.clone());
161                    fontique::QueryStatus::Stop
162                });
163                font
164            };
165
166            match font {
167                None => {
168                    if let Some(source_location) = source_location {
169                        diag.push_error_with_span("could not find font that provides specified family, falling back to Sans-Serif".to_string(), source_location);
170                    } else {
171                        diag.push_error(
172                            "internal error: could not determine a default font for sans-serif"
173                                .to_string(),
174                            &generic_diag_location,
175                        );
176                    };
177                }
178                Some(query_font) => {
179                    if let Some(font_info) = collection
180                        .family(query_font.family.0)
181                        .and_then(|family_info| family_info.fonts().first().cloned())
182                    {
183                        let path = if let Some(path) = font_paths.get(&query_font.family.0) {
184                            path.clone()
185                        } else {
186                            match &font_info.source().kind {
187                                fontique::SourceKind::Path(path) => path.to_path_buf(),
188                                fontique::SourceKind::Memory(_) => {
189                                    diag.push_error(
190                                    "internal error: memory fonts are not supported in the compiler"
191                                        .to_string(),
192                                    &generic_diag_location,
193                                );
194                                    custom_face_error = true;
195                                    continue;
196                                }
197                            }
198                        };
199                        font_paths.insert(query_font.family.0, path.clone());
200                        default_fonts.insert(path.clone(), query_font);
201                    }
202                }
203            }
204        }
205
206        default_fonts
207    };
208
209    if custom_face_error {
210        return;
211    }
212
213    let register_embedded_font = |path: &std::path::Path, embedded_bitmap_font: BitmapFont| {
214        let resource_id = doc.embedded_file_resources.borrow_mut().push_and_get_key(
215            crate::embedded_resources::EmbeddedResources {
216                path: Some(path.to_string_lossy().as_ref().into()),
217                kind: crate::embedded_resources::EmbeddedResourcesKind::BitmapFontData(
218                    embedded_bitmap_font,
219                ),
220            },
221        );
222
223        for c in doc.exported_roots() {
224            c.init_code.borrow_mut().font_registration_code.push(Expression::FunctionCall {
225                function: BuiltinFunction::RegisterBitmapFont.into(),
226                arguments: vec![Expression::NumberLiteral(resource_id.0 as _, Unit::None)],
227                source_location: None,
228            });
229        }
230    };
231
232    let mut embed_font_by_path = |path: &std::path::Path, font: &fontique::QueryFont| {
233        let Some(family_name) = collection.family_name(font.family.0).to_owned() else {
234            diag.push_error(
235                format!(
236                    "internal error: TrueType font without family name encountered: {}",
237                    path.display()
238                ),
239                &generic_diag_location,
240            );
241            return;
242        };
243
244        let Some(font_ref) = skrifa::FontRef::from_index(font.blob.data(), font.index).ok() else {
245            diag.push_error(
246                format!("internal error: failed to parse font: {}", path.display()),
247                &generic_diag_location,
248            );
249            return;
250        };
251        let axes = font_ref.axes();
252        let wght_axis = axes.iter().find(|axis| axis.tag() == skrifa::Tag::new(b"wght"));
253
254        if let Some(wght_axis) = wght_axis {
255            // Variable font: embed one BitmapFont per requested weight
256            let weights = if font_weights.is_empty() {
257                vec![fontique::FontWeight::NORMAL.value() as u16]
258            } else {
259                font_weights.clone()
260            };
261            for &weight in &weights {
262                let clamped = (weight as f32).clamp(wght_axis.min_value(), wght_axis.max_value());
263                let location = axes.location([("wght", clamped)]);
264                let variations = vec![(skrifa::Tag::new(b"wght"), clamped)];
265
266                let embedded = embed_font(
267                    family_name.to_owned(),
268                    Font { font: font.clone() },
269                    &pixel_sizes,
270                    characters_seen.iter().cloned(),
271                    &fallback_fonts,
272                    compiler_config,
273                    location.coords(),
274                    &variations,
275                    Some(weight),
276                );
277                register_embedded_font(path, embedded);
278            }
279        } else {
280            // Static font: embed once
281            let embedded = embed_font(
282                family_name.to_owned(),
283                Font { font: font.clone() },
284                &pixel_sizes,
285                characters_seen.iter().cloned(),
286                &fallback_fonts,
287                compiler_config,
288                &[],
289                &[],
290                None,
291            );
292            register_embedded_font(path, embedded);
293        }
294    };
295
296    for (path, font) in default_fonts.iter() {
297        custom_fonts.remove(path);
298        embed_font_by_path(path, font);
299    }
300
301    for (path, font) in custom_fonts {
302        embed_font_by_path(&path, &font);
303    }
304}
305
306#[inline(never)] // workaround https://github.com/rust-lang/rust/issues/104099
307fn get_fallback_fonts() -> Vec<Font> {
308    let mut fallback_fonts = Vec::new();
309
310    let mut collection = sharedfontique::create_collection(false);
311    let mut query = collection.query();
312    query.set_families(
313        sharedfontique::FALLBACK_FAMILIES.into_iter().map(fontique::QueryFamily::Generic).chain(
314            core::iter::once(fontique::QueryFamily::Generic(fontique::GenericFamily::Emoji)),
315        ),
316    );
317
318    query.matches_with(|query_font| {
319        fallback_fonts.push(Font { font: query_font.clone() });
320        fontique::QueryStatus::Continue
321    });
322
323    fallback_fonts
324}
325
326#[cfg(not(target_arch = "wasm32"))]
327fn embed_font(
328    family_name: String,
329    font: Font,
330    pixel_sizes: &[i16],
331    character_coverage: impl Iterator<Item = char>,
332    fallback_fonts: &[Font],
333    _compiler_config: &CompilerConfiguration,
334    normalized_coords: &[skrifa::instance::NormalizedCoord],
335    _variations: &[(skrifa::Tag, f32)],
336    override_weight: Option<u16>,
337) -> BitmapFont {
338    let coords_i16: Vec<i16> = normalized_coords.iter().map(|c| c.to_bits()).collect();
339
340    let mut character_map: Vec<CharacterMapEntry> = character_coverage
341        .filter(|code_point| {
342            core::iter::once(&font)
343                .chain(fallback_fonts.iter())
344                .any(|font| swash_font_ref(font).charmap().map(*code_point) != 0)
345        })
346        .enumerate()
347        .map(|(glyph_index, code_point)| CharacterMapEntry {
348            code_point,
349            glyph_index: u16::try_from(glyph_index)
350                .expect("more than 65535 glyphs are not supported"),
351        })
352        .collect();
353
354    #[cfg(feature = "sdf-fonts")]
355    let glyphs = if _compiler_config.use_sdf_fonts {
356        embed_sdf_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, _variations)
357    } else {
358        embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, &coords_i16)
359    };
360    #[cfg(not(feature = "sdf-fonts"))]
361    let glyphs =
362        embed_alpha_map_glyphs(pixel_sizes, &character_map, &font, fallback_fonts, &coords_i16);
363
364    character_map.sort_by_key(|entry| entry.code_point);
365
366    let font_ref = skrifa::FontRef::from_index(font.font.blob.data(), font.font.index).unwrap();
367    let location = skrifa::instance::LocationRef::new(normalized_coords);
368    let metrics =
369        skrifa::metrics::Metrics::new(&font_ref, skrifa::instance::Size::unscaled(), location);
370    let attrs = skrifa::attribute::Attributes::new(&font_ref);
371
372    BitmapFont {
373        family_name,
374        character_map,
375        units_per_em: metrics.units_per_em as f32,
376        ascent: metrics.ascent,
377        descent: metrics.descent,
378        x_height: metrics.x_height.unwrap_or_default(),
379        cap_height: metrics.cap_height.unwrap_or_default(),
380        glyphs,
381        weight: override_weight.unwrap_or(attrs.weight.value() as u16),
382        italic: attrs.style != skrifa::attribute::Style::Normal,
383        #[cfg(feature = "sdf-fonts")]
384        sdf: _compiler_config.use_sdf_fonts,
385        #[cfg(not(feature = "sdf-fonts"))]
386        sdf: false,
387    }
388}
389
390#[cfg(not(target_arch = "wasm32"))]
391fn embed_alpha_map_glyphs(
392    pixel_sizes: &[i16],
393    character_map: &Vec<CharacterMapEntry>,
394    font: &Font,
395    fallback_fonts: &[Font],
396    normalized_coords: &[i16],
397) -> Vec<BitmapGlyphs> {
398    use rayon::prelude::*;
399    use std::cell::RefCell;
400
401    thread_local! {
402        static SCALE_CONTEXT: RefCell<swash::scale::ScaleContext> =
403            RefCell::new(swash::scale::ScaleContext::new());
404    }
405
406    pixel_sizes
407        .par_iter()
408        .map(|pixel_size| {
409            let glyph_data = character_map
410                .par_iter()
411                .map(|CharacterMapEntry { code_point, .. }| {
412                    let font_to_use = core::iter::once(font)
413                        .chain(fallback_fonts.iter())
414                        .find(|f| swash_font_ref(f).charmap().map(*code_point) != 0)
415                        .unwrap_or(font);
416
417                    let font_ref = swash_font_ref(font_to_use);
418                    let glyph_id = font_ref.charmap().map(*code_point);
419                    let gm = font_ref.glyph_metrics(normalized_coords);
420                    let fm = font_ref.metrics(normalized_coords);
421                    let scale = *pixel_size as f32 / fm.units_per_em as f32;
422                    let advance_width = gm.advance_width(glyph_id) * scale;
423
424                    SCALE_CONTEXT.with(|ctx| {
425                        let font_ref = swash_font_ref(font_to_use);
426                        let mut ctx = ctx.borrow_mut();
427                        let mut scaler = ctx
428                            .builder(font_ref)
429                            .size(*pixel_size as f32)
430                            .normalized_coords(normalized_coords)
431                            .build();
432                        let image = swash::scale::Render::new(&[swash::scale::Source::Outline])
433                            .format(swash::zeno::Format::Alpha)
434                            .render(&mut scaler, glyph_id);
435
436                        match image {
437                            Some(image) => {
438                                let p = image.placement;
439                                BitmapGlyph {
440                                    x: i16::try_from(p.left * 64)
441                                        .expect("large glyph x coordinate"),
442                                    y: i16::try_from((p.top - p.height as i32) * 64)
443                                        .expect("large glyph y coordinate"),
444                                    width: i16::try_from(p.width).expect("large width"),
445                                    height: i16::try_from(p.height).expect("large height"),
446                                    x_advance: i16::try_from((advance_width * 64.) as i64)
447                                        .expect("large advance width"),
448                                    data: image.data,
449                                }
450                            }
451                            None => BitmapGlyph {
452                                x: 0,
453                                y: 0,
454                                width: 0,
455                                height: 0,
456                                x_advance: i16::try_from((advance_width * 64.) as i64)
457                                    .expect("large advance width"),
458                                data: vec![],
459                            },
460                        }
461                    })
462                })
463                .collect();
464
465            BitmapGlyphs { pixel_size: *pixel_size, glyph_data }
466        })
467        .collect()
468}
469
470#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
471fn embed_sdf_glyphs(
472    pixel_sizes: &[i16],
473    character_map: &Vec<CharacterMapEntry>,
474    font: &Font,
475    fallback_fonts: &[Font],
476    variations: &[(skrifa::Tag, f32)],
477) -> Vec<BitmapGlyphs> {
478    use rayon::prelude::*;
479
480    const RANGE: f64 = 6.;
481
482    let Some(max_size) = pixel_sizes.iter().max() else {
483        return Vec::new();
484    };
485    let min_size = pixel_sizes.iter().min().expect("we have a 'max' so the vector is not empty");
486    let target_pixel_size = (max_size * 2 / 3).max(16).min(RANGE as i16 * min_size);
487
488    let glyph_data = character_map
489        .par_iter()
490        .map(|CharacterMapEntry { code_point, .. }| {
491            core::iter::once(font)
492                .chain(fallback_fonts.iter())
493                .find_map(|font| {
494                    (swash_font_ref(font).charmap().map(*code_point) != 0).then(|| {
495                        generate_sdf_for_glyph(
496                            font,
497                            *code_point,
498                            target_pixel_size,
499                            RANGE,
500                            variations,
501                        )
502                    })
503                })
504                .unwrap_or_else(|| {
505                    generate_sdf_for_glyph(font, *code_point, target_pixel_size, RANGE, variations)
506                })
507                .unwrap_or_default()
508        })
509        .collect::<Vec<_>>();
510
511    vec![BitmapGlyphs { pixel_size: target_pixel_size, glyph_data }]
512}
513
514#[cfg(all(not(target_arch = "wasm32"), feature = "sdf-fonts"))]
515fn generate_sdf_for_glyph(
516    font: &Font,
517    code_point: char,
518    target_pixel_size: i16,
519    range: f64,
520    variations: &[(skrifa::Tag, f32)],
521) -> Option<BitmapGlyph> {
522    use fdsm::transform::Transform;
523    use nalgebra::{Affine2, Similarity2, Vector2};
524
525    let mut face =
526        fdsm_ttf_parser::ttf_parser::Face::parse(font.font.blob.data(), font.font.index).unwrap();
527    for &(tag, value) in variations {
528        face.set_variation(
529            fdsm_ttf_parser::ttf_parser::Tag(u32::from_be_bytes(tag.to_be_bytes())),
530            value,
531        );
532    }
533    let glyph_id = face.glyph_index(code_point).unwrap_or_default();
534
535    let font_ref = skrifa::FontRef::from_index(font.font.blob.data(), font.font.index).unwrap();
536    let variation_settings: Vec<_> =
537        variations.iter().map(|&(tag, value)| (tag, value)).collect::<Vec<_>>();
538    let location = font_ref.axes().location(variation_settings);
539    let metrics = skrifa::metrics::Metrics::new(
540        &font_ref,
541        skrifa::instance::Size::unscaled(),
542        skrifa::instance::LocationRef::from(&location),
543    );
544    let target_pixel_size = target_pixel_size as f64;
545    let scale = target_pixel_size / metrics.units_per_em as f64;
546
547    // TODO: handle bitmap glyphs (emojis)
548    let Some(bbox) = face.glyph_bounding_box(glyph_id) else {
549        // For example, for space
550        return Some(BitmapGlyph {
551            x_advance: (face.glyph_hor_advance(glyph_id).unwrap_or(0) as f64 * scale * 64.) as i16,
552            ..Default::default()
553        });
554    };
555
556    let mut shape = fdsm_ttf_parser::load_shape_from_face(&face, glyph_id)?;
557
558    let width = ((bbox.x_max as f64 - bbox.x_min as f64) * scale + 2.).ceil() as u32;
559    let height = ((bbox.y_max as f64 - bbox.y_min as f64) * scale + 2.).ceil() as u32;
560    let transformation = nalgebra::convert::<_, Affine2<f64>>(Similarity2::new(
561        Vector2::new(1. - bbox.x_min as f64 * scale, 1. - bbox.y_min as f64 * scale),
562        0.,
563        scale,
564    ));
565
566    // Unlike msdfgen, the transformation is not passed into the
567    // `generate_msdf` function – the coordinates of the control points
568    // must be expressed in terms of pixels on the distance field. To get
569    // the correct units, we pre-transform the shape:
570
571    shape.transform(&transformation);
572
573    let prepared_shape = shape.prepare();
574
575    // Set up the resulting image and generate the distance field:
576
577    let mut sdf = image::GrayImage::new(width, height);
578    fdsm::generate::generate_sdf(&prepared_shape, range, &mut sdf);
579    fdsm::render::correct_sign_sdf(
580        &mut sdf,
581        &prepared_shape,
582        fdsm::bezier::scanline::FillRule::Nonzero,
583    );
584
585    let mut glyph_data = sdf.into_raw();
586
587    // normalize around 0
588    for x in &mut glyph_data {
589        *x = x.wrapping_sub(128);
590    }
591
592    // invert the y coordinate (as the fsdm crate has the y axis inverted)
593    let (w, h) = (width as usize, height as usize);
594    for idx in 0..glyph_data.len() / 2 {
595        glyph_data.swap(idx, (h - idx / w - 1) * w + idx % w);
596    }
597
598    // Add a "0" so that we can always access pos+1 without going out of bound
599    // (so that the last row will look like `data[len-1]*1 + data[len]*0`)
600    glyph_data.push(0);
601
602    let bg = BitmapGlyph {
603        x: i16::try_from((-(1. - bbox.x_min as f64 * scale) * 64.).ceil() as i32)
604            .expect("large glyph x coordinate"),
605        y: i16::try_from((-(1. - bbox.y_min as f64 * scale) * 64.).ceil() as i32)
606            .expect("large glyph y coordinate"),
607        width: i16::try_from(width).expect("large width"),
608        height: i16::try_from(height).expect("large height"),
609        x_advance: i16::try_from(
610            (face.glyph_hor_advance(glyph_id).unwrap() as f64 * scale * 64.).round() as i32,
611        )
612        .expect("large advance width"),
613        data: glyph_data,
614    };
615
616    Some(bg)
617}
618
619fn try_extract_literal_from_element(
620    elem: &ElementRc,
621    property_name: &str,
622    unit: Unit,
623) -> Option<f64> {
624    elem.borrow().bindings.get(property_name).and_then(|expression| {
625        match &expression.borrow().expression {
626            Expression::NumberLiteral(value, u) if *u == unit => Some(*value),
627            Expression::Cast { from, .. } => match from.as_ref() {
628                Expression::NumberLiteral(value, u) if *u == unit => Some(*value),
629                _ => None,
630            },
631            _ => None,
632        }
633    })
634}
635
636pub fn collect_font_sizes_used(
637    component: &Rc<Component>,
638    scale_factor: f64,
639    sizes_seen: &mut Vec<i16>,
640) {
641    let mut add_font_size = |logical_size: f64| {
642        let pixel_size = (logical_size * scale_factor) as i16;
643        match sizes_seen.binary_search(&pixel_size) {
644            Ok(_) => {}
645            Err(pos) => sizes_seen.insert(pos, pixel_size),
646        }
647    };
648
649    recurse_elem_including_sub_components(component, &(), &mut |elem, _| match elem
650        .borrow()
651        .base_type
652        .to_string()
653        .as_str()
654    {
655        "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledTextItem" => {
656            if let Some(font_size) = try_extract_literal_from_element(elem, "font-size", Unit::Px) {
657                add_font_size(font_size)
658            }
659        }
660        "Dialog" | "Window" | "WindowItem" => {
661            if let Some(font_size) =
662                try_extract_literal_from_element(elem, "default-font-size", Unit::Px)
663            {
664                add_font_size(font_size)
665            }
666        }
667        _ => {}
668    });
669}
670
671pub fn collect_font_weights_used(component: &Rc<Component>, weights_seen: &mut Vec<u16>) {
672    let mut add_weight = |weight: f64| {
673        let weight = weight as u16;
674        if let Err(pos) = weights_seen.binary_search(&weight) {
675            weights_seen.insert(pos, weight);
676        }
677    };
678
679    recurse_elem_including_sub_components(component, &(), &mut |elem, _| match elem
680        .borrow()
681        .base_type
682        .to_string()
683        .as_str()
684    {
685        "TextInput" | "Text" | "SimpleText" | "ComplexText" | "StyledTextItem" => {
686            if let Some(weight) = try_extract_literal_from_element(elem, "font-weight", Unit::None)
687            {
688                add_weight(weight)
689            }
690        }
691        "Dialog" | "Window" | "WindowItem" => {
692            if let Some(weight) =
693                try_extract_literal_from_element(elem, "default-font-weight", Unit::None)
694            {
695                add_weight(weight)
696            }
697        }
698        _ => {}
699    });
700}
701
702pub fn scan_string_literals(component: &Rc<Component>, characters_seen: &mut HashSet<char>) {
703    visit_all_expressions(component, |expr, _| {
704        expr.visit_recursive(&mut |expr| {
705            if let Expression::StringLiteral(string) = expr {
706                characters_seen.extend(string.chars());
707            }
708        })
709    })
710}