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