1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text.font
18 
19 import androidx.compose.ui.util.fastFilter
20 
21 /**
22  * Given a [FontFamily], [FontWeight] and [FontStyle], matches the best font in the [FontFamily]
23  * that satisfies the requirements of [FontWeight] and [FontStyle].
24  *
25  * For the case without font synthesis, applies the rules at
26  * [CSS 4 Font Matching](https://www.w3.org/TR/css-fonts-4/#font-style-matching).
27  */
28 internal class FontMatcher {
29 
30     /**
31      * Given a [FontFamily], [FontWeight] and [FontStyle], matches the best font in the [FontFamily]
32      * that satisfies the requirements of [FontWeight] and [FontStyle]. If there is not a font that
33      * exactly satisfies the given constraints of [FontWeight] and [FontStyle], the best match will
34      * be returned. The rules for the best match are defined in
35      * [CSS 4 Font Matching](https://www.w3.org/TR/css-fonts-4/#font-style-matching).
36      *
37      * If no fonts match, an empty list is returned.
38      *
39      * @param fontList iterable of fonts to choose the [Font] from
40      * @param fontWeight desired [FontWeight]
41      * @param fontStyle desired [FontStyle]
42      */
matchFontnull43     fun matchFont(fontList: List<Font>, fontWeight: FontWeight, fontStyle: FontStyle): List<Font> {
44         // check for exact match first
45         fontList
46             .fastFilter { it.weight == fontWeight && it.style == fontStyle }
47             .let {
48                 // TODO(b/130797349): IR compiler bug was here
49                 if (it.isNotEmpty()) {
50                     return it
51                 }
52             }
53 
54         // if no exact match, filter with style
55         val fontsToSearch = fontList.fastFilter { it.style == fontStyle }.ifEmpty { fontList }
56 
57         val result =
58             when {
59                 fontWeight < FontWeight.W400 -> {
60                     // If the desired weight is less than 400
61                     // - weights less than or equal to the desired weight are checked in descending
62                     // order
63                     // - followed by weights above the desired weight in ascending order
64 
65                     fontsToSearch.filterByClosestWeight(fontWeight, preferBelow = true)
66                 }
67                 fontWeight > FontWeight.W500 -> {
68                     // If the desired weight is greater than 500
69                     // - weights greater than or equal to the desired weight are checked in
70                     // ascending order
71                     // - followed by weights below the desired weight in descending order
72                     fontsToSearch.filterByClosestWeight(fontWeight, preferBelow = false)
73                 }
74                 else -> {
75                     // If the desired weight is inclusively between 400 and 500
76                     // - weights greater than or equal to the target weight are checked in ascending
77                     // order
78                     // until 500 is hit and checked,
79                     // - followed by weights less than the target weight in descending order,
80                     // - followed by weights greater than 500
81                     fontsToSearch
82                         .filterByClosestWeight(
83                             fontWeight,
84                             preferBelow = false,
85                             minSearchRange = null,
86                             maxSearchRange = FontWeight.W500
87                         )
88                         .ifEmpty {
89                             fontsToSearch.filterByClosestWeight(
90                                 fontWeight,
91                                 preferBelow = false,
92                                 minSearchRange = FontWeight.W500,
93                                 maxSearchRange = null
94                             )
95                         }
96                 }
97             }
98 
99         return result
100     }
101 
102     @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
103     // @VisibleForTesting
filterByClosestWeightnull104     internal inline fun List<Font>.filterByClosestWeight(
105         fontWeight: FontWeight,
106         preferBelow: Boolean,
107         minSearchRange: FontWeight? = null,
108         maxSearchRange: FontWeight? = null,
109     ): List<Font> {
110         var bestWeightAbove: FontWeight? = null
111         var bestWeightBelow: FontWeight? = null
112         for (index in indices) {
113             val font = get(index)
114             val possibleWeight = font.weight
115             if (minSearchRange != null && possibleWeight < minSearchRange) {
116                 continue
117             }
118             if (maxSearchRange != null && possibleWeight > maxSearchRange) {
119                 continue
120             }
121             if (possibleWeight < fontWeight) {
122                 if (bestWeightBelow == null || possibleWeight > bestWeightBelow) {
123                     bestWeightBelow = possibleWeight
124                 }
125             } else if (possibleWeight > fontWeight) {
126                 if (bestWeightAbove == null || possibleWeight < bestWeightAbove) {
127                     bestWeightAbove = possibleWeight
128                 }
129             } else {
130                 // exact weight match, we can exit now
131                 bestWeightAbove = possibleWeight
132                 bestWeightBelow = possibleWeight
133                 break
134             }
135         }
136         val bestWeight =
137             if (preferBelow) {
138                 bestWeightBelow ?: bestWeightAbove
139             } else {
140                 bestWeightAbove ?: bestWeightBelow
141             }
142         return fastFilter { it.weight == bestWeight }
143     }
144 
145     /** @see matchFont */
matchFontnull146     fun matchFont(
147         fontFamily: FontFamily,
148         fontWeight: FontWeight,
149         fontStyle: FontStyle
150     ): List<Font> {
151         if (fontFamily !is FontListFontFamily)
152             throw IllegalArgumentException(
153                 "Only FontFamily instances that presents a list of Fonts can be used"
154             )
155 
156         return matchFont(fontFamily, fontWeight, fontStyle)
157     }
158 
159     /**
160      * Required to disambiguate matchFont(fontListFontFamilyInstance).
161      *
162      * @see matchFont
163      */
matchFontnull164     fun matchFont(
165         fontFamily: FontListFontFamily,
166         fontWeight: FontWeight,
167         fontStyle: FontStyle
168     ): List<Font> {
169         return matchFont(fontFamily.fonts, fontWeight, fontStyle)
170     }
171 }
172