zed_font_kit/
matching.rs

1// font-kit/src/matching.rs
2//
3// Copyright © 2018 The Pathfinder Project Developers.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
12
13use float_ord::FloatOrd;
14
15use crate::error::SelectionError;
16use crate::properties::{Properties, Stretch, Style, Weight};
17
18/// This follows CSS Fonts Level 3 § 5.2 [1].
19///
20/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
21pub fn find_best_match(
22    candidates: &[Properties],
23    query: &Properties,
24) -> Result<usize, SelectionError> {
25    // Step 4.
26    let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
27    if matching_set.is_empty() {
28        return Err(SelectionError::NotFound);
29    }
30
31    // Step 4a (`font-stretch`).
32    let matching_stretch = if matching_set
33        .iter()
34        .any(|&index| candidates[index].stretch == query.stretch)
35    {
36        // Exact match.
37        query.stretch
38    } else if query.stretch <= Stretch::NORMAL {
39        // Closest width, first checking narrower values and then wider values.
40        match matching_set
41            .iter()
42            .filter(|&&index| candidates[index].stretch < query.stretch)
43            .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
44        {
45            Some(&matching_index) => candidates[matching_index].stretch,
46            None => {
47                let matching_index = *matching_set
48                    .iter()
49                    .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
50                    .unwrap();
51                candidates[matching_index].stretch
52            }
53        }
54    } else {
55        // Closest width, first checking wider values and then narrower values.
56        match matching_set
57            .iter()
58            .filter(|&&index| candidates[index].stretch > query.stretch)
59            .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
60        {
61            Some(&matching_index) => candidates[matching_index].stretch,
62            None => {
63                let matching_index = *matching_set
64                    .iter()
65                    .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
66                    .unwrap();
67                candidates[matching_index].stretch
68            }
69        }
70    };
71    matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
72
73    // Step 4b (`font-style`).
74    let style_preference = match query.style {
75        Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
76        Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
77        Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
78    };
79    let matching_style = *style_preference
80        .iter()
81        .find(|&query_style| {
82            matching_set
83                .iter()
84                .any(|&index| candidates[index].style == *query_style)
85        })
86        .unwrap();
87    matching_set.retain(|&index| candidates[index].style == matching_style);
88
89    // Step 4c (`font-weight`).
90    //
91    // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
92    // just use 450 as the cutoff.
93    let matching_weight = if matching_set
94        .iter()
95        .any(|&index| candidates[index].weight == query.weight)
96    {
97        query.weight
98    } else if query.weight >= Weight(400.0)
99        && query.weight < Weight(450.0)
100        && matching_set
101            .iter()
102            .any(|&index| candidates[index].weight == Weight(500.0))
103    {
104        // Check 500 first.
105        Weight(500.0)
106    } else if query.weight >= Weight(450.0)
107        && query.weight <= Weight(500.0)
108        && matching_set
109            .iter()
110            .any(|&index| candidates[index].weight == Weight(400.0))
111    {
112        // Check 400 first.
113        Weight(400.0)
114    } else if query.weight <= Weight(500.0) {
115        // Closest weight, first checking thinner values and then fatter ones.
116        match matching_set
117            .iter()
118            .filter(|&&index| candidates[index].weight <= query.weight)
119            .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
120        {
121            Some(&matching_index) => candidates[matching_index].weight,
122            None => {
123                let matching_index = *matching_set
124                    .iter()
125                    .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
126                    .unwrap();
127                candidates[matching_index].weight
128            }
129        }
130    } else {
131        // Closest weight, first checking fatter values and then thinner ones.
132        match matching_set
133            .iter()
134            .filter(|&&index| candidates[index].weight >= query.weight)
135            .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
136        {
137            Some(&matching_index) => candidates[matching_index].weight,
138            None => {
139                let matching_index = *matching_set
140                    .iter()
141                    .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
142                    .unwrap();
143                candidates[matching_index].weight
144            }
145        }
146    };
147    matching_set.retain(|&index| candidates[index].weight == matching_weight);
148
149    // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
150
151    // Return the result.
152    matching_set
153        .into_iter()
154        .next()
155        .ok_or(SelectionError::NotFound)
156}