1 /* <lambda>null2 * Copyright 2022 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.runtime.Immutable 20 import androidx.compose.ui.text.internal.requirePrecondition 21 import androidx.compose.ui.text.internal.requirePreconditionNotNull 22 import androidx.compose.ui.unit.Density 23 import androidx.compose.ui.unit.TextUnit 24 import androidx.compose.ui.util.fastAny 25 26 /** 27 * Set font variation settings. 28 * 29 * To learn more about the font variation settings, see the list supported by 30 * [fonts.google.com](https://fonts.google.com/variablefonts#axis-definitions). 31 */ 32 object FontVariation { 33 /** 34 * A collection of settings to apply to a single font. 35 * 36 * Settings must be unique on [Setting.axisName] 37 */ 38 @Immutable 39 class Settings(vararg settings: Setting) { 40 /** All settings, unique by [FontVariation.Setting.axisName] */ 41 val settings: List<Setting> 42 43 /** 44 * True if density is required to resolve any of these settings 45 * 46 * If false, density will not affect the result of any [Setting.toVariationValue]. 47 */ 48 internal val needsDensity: Boolean 49 50 init { 51 this.settings = 52 ArrayList( 53 settings 54 .groupBy { it.axisName } 55 .flatMap { (key, value) -> 56 require(value.size == 1) { 57 "'$key' must be unique. Actual [ [${value.joinToString()}]" 58 } 59 value 60 } 61 ) 62 63 needsDensity = this.settings.fastAny { it.needsDensity } 64 } 65 66 override fun equals(other: Any?): Boolean { 67 if (this === other) return true 68 if (other !is Settings) return false 69 70 if (settings != other.settings) return false 71 72 return true 73 } 74 75 override fun hashCode(): Int { 76 return settings.hashCode() 77 } 78 } 79 80 /** Represents a single point in a variation, such as 0.7 or 100 */ 81 @Immutable 82 sealed interface Setting { 83 /** 84 * Convert a value to a final value for use as a font variation setting. 85 * 86 * If [needsDensity] is false, density may be null 87 * 88 * @param density to resolve from Compose types to feature-specific ranges. 89 */ 90 fun toVariationValue(density: Density?): Float 91 92 /** 93 * True if this setting requires density to resolve 94 * 95 * When false, may toVariationValue may be called with null or any Density 96 */ 97 val needsDensity: Boolean 98 99 /** The font variation axis, such as 'wdth' or 'ital' */ 100 val axisName: String 101 } 102 103 @Immutable 104 private class SettingFloat(override val axisName: String, val value: Float) : Setting { 105 override fun toVariationValue(density: Density?): Float = value 106 107 override val needsDensity: Boolean = false 108 109 override fun equals(other: Any?): Boolean { 110 if (this === other) return true 111 if (other !is SettingFloat) return false 112 113 if (axisName != other.axisName) return false 114 if (value != other.value) return false 115 116 return true 117 } 118 119 override fun hashCode(): Int { 120 var result = axisName.hashCode() 121 result = 31 * result + value.hashCode() 122 return result 123 } 124 125 override fun toString(): String { 126 return "FontVariation.Setting(axisName='$axisName', value=$value)" 127 } 128 } 129 130 @Immutable 131 private class SettingTextUnit(override val axisName: String, val value: TextUnit) : Setting { 132 override fun toVariationValue(density: Density?): Float { 133 // we don't care about pixel density as 12sp is the same "visual" size on all devices 134 // instead we only care about font scaling, which changes visual size 135 requirePreconditionNotNull(density) { "density must not be null" } 136 return value.value * density.fontScale 137 } 138 139 override val needsDensity: Boolean = true 140 141 override fun equals(other: Any?): Boolean { 142 if (this === other) return true 143 if (other !is SettingTextUnit) return false 144 145 if (axisName != other.axisName) return false 146 if (value != other.value) return false 147 148 return true 149 } 150 151 override fun hashCode(): Int { 152 var result = axisName.hashCode() 153 result = 31 * result + value.hashCode() 154 return result 155 } 156 157 override fun toString(): String { 158 return "FontVariation.Setting(axisName='$axisName', value=$value)" 159 } 160 } 161 162 @Immutable 163 private class SettingInt(override val axisName: String, val value: Int) : Setting { 164 override fun toVariationValue(density: Density?): Float = value.toFloat() 165 166 override val needsDensity: Boolean = false 167 168 override fun equals(other: Any?): Boolean { 169 if (this === other) return true 170 if (other !is SettingInt) return false 171 172 if (axisName != other.axisName) return false 173 if (value != other.value) return false 174 175 return true 176 } 177 178 override fun hashCode(): Int { 179 var result = axisName.hashCode() 180 result = 31 * result + value 181 return result 182 } 183 184 override fun toString(): String { 185 return "FontVariation.Setting(axisName='$axisName', value=$value)" 186 } 187 } 188 189 /** 190 * Create a font variation setting for any axis supported by a font. 191 * 192 * ``` 193 * val setting = FontVariation.Setting('wght', 400f); 194 * ``` 195 * 196 * You should typically not use this in app-code directly, instead define a method for each 197 * setting supported by your app/font. 198 * 199 * If you had a setting `fzzt` that set a variation setting called fizzable between 1 and 11, 200 * define a function like this: 201 * ``` 202 * fun FontVariation.fizzable(fiz: Int): FontVariation.Setting { 203 * require(fiz in 1..11) { "'fzzt' must be in 1..11" } 204 * return Setting("fzzt", fiz.toFloat()) 205 * ``` 206 * 207 * @param name axis name, must be 4 characters 208 * @param value value for axis, not validated and directly passed to font 209 */ 210 fun Setting(name: String, value: Float): Setting { 211 requirePrecondition(name.length == 4) { 212 "Name must be exactly four characters. Actual: '$name'" 213 } 214 return SettingFloat(name, value) 215 } 216 217 /** 218 * Italic or upright, equivalent to [FontStyle] 219 * 220 * 'ital', 0.0f is upright, and 1.0f is italic. 221 * 222 * A platform _may_ provide automatic setting of `ital` on font load. When supported, `ital` is 223 * automatically applied based on [FontStyle] if platform and the loaded font support 'ital'. 224 * 225 * Automatic mapping is done via [Settings]\([FontWeight], [FontStyle]\) 226 * 227 * To override this behavior provide an explicit FontVariation.italic to a [Font] that supports 228 * variation settings. 229 * 230 * @param value [0.0f, 1.0f] 231 */ 232 fun italic(value: Float): Setting { 233 requirePrecondition(value in 0.0f..1.0f) { "'ital' must be in 0.0f..1.0f. Actual: $value" } 234 return SettingFloat("ital", value) 235 } 236 237 /** 238 * Optical size is how "big" a font appears to the eye. 239 * 240 * It should be set by a ratio from a font size. 241 * 242 * Adapt the style to specific text sizes. At smaller sizes, letters typically become optimized 243 * for more legibility. At larger sizes, optimized for headlines, with more extreme weights and 244 * widths. 245 * 246 * A Platform _may_ choose to support automatic optical sizing. When present, this will set the 247 * optical size based on the font size. 248 * 249 * To override this behavior provide an explicit FontVariation.opticalSizing to a [Font] that 250 * supports variation settings. 251 * 252 * @param textSize font-size at the expected display, must be in sp 253 */ 254 fun opticalSizing(textSize: TextUnit): Setting { 255 requirePrecondition(textSize.isSp) { "'opsz' must be provided in sp units" } 256 return SettingTextUnit("opsz", textSize) 257 } 258 259 /** 260 * Adjust the style from upright to slanted, also known to typographers as an 'oblique' style. 261 * 262 * Rarely, slant can work in the other direction, called a 'backslanted' or 'reverse oblique' 263 * style. 264 * 265 * 'slnt', values as an angle, 0f is upright. 266 * 267 * @param value -90f to 90f, represents an angle 268 */ 269 fun slant(value: Float): Setting { 270 requirePrecondition(value in -90f..90f) { "'slnt' must be in -90f..90f. Actual: $value" } 271 return SettingFloat("slnt", value) 272 } 273 274 /** 275 * Width of the type. 276 * 277 * Adjust the style from narrower to wider, by varying the proportions of counters, strokes, 278 * spacing and kerning, and other aspects of the type. This typically changes the typographic 279 * color in a subtle way, and so may be used in conjunction with Width and Grade axes. 280 * 281 * 'wdth', such as 10f 282 * 283 * @param value > 0.0f represents the width 284 */ 285 fun width(value: Float): Setting { 286 requirePrecondition(value > 0.0f) { "'wdth' must be strictly > 0.0f. Actual: $value" } 287 return SettingFloat("wdth", value) 288 } 289 290 /** 291 * Weight, equivalent to [FontWeight] 292 * 293 * Setting weight always causes visual text reflow, to make text "bolder" or "thinner" without 294 * reflow see [grade] 295 * 296 * Adjust the style from lighter to bolder in typographic color, by varying stroke weights, 297 * spacing and kerning, and other aspects of the type. This typically changes overall width, and 298 * so may be used in conjunction with Width and Grade axes. 299 * 300 * This is equivalent to [FontWeight], and platforms _may_ support automatically setting 'wghts' 301 * from [FontWeight] during font load. 302 * 303 * Setting this does not change [FontWeight]. If an explicit value and [FontWeight] disagree, 304 * the weight specified by `wght` will be shown if the font supports it. 305 * 306 * Automatic mapping is done via [Settings]\([FontWeight], [FontStyle]\) 307 * 308 * @param value weight, in 1..1000 309 */ 310 fun weight(value: Int): Setting { 311 requirePrecondition(value in 1..1000) { 312 "'wght' value must be in [1, 1000]. Actual: $value" 313 } 314 return SettingInt("wght", value) 315 } 316 317 /** 318 * Change visual weight of text without text reflow. 319 * 320 * Finesse the style from lighter to bolder in typographic color, without any changes overall 321 * width, line breaks or page layout. Negative grade makes the style lighter, while positive 322 * grade makes it bolder. The units are the same as in the Weight axis. 323 * 324 * Visual appearance of text with weight and grade set is similar to text with 325 * 326 * ``` 327 * weight = (weight + grade) 328 * ``` 329 * 330 * @param value grade, in -1000..1000 331 */ 332 fun grade(value: Int): Setting { 333 requirePrecondition(value in -1000..1000) { "'GRAD' must be in -1000..1000" } 334 return SettingInt("GRAD", value) 335 } 336 337 /** 338 * Variation settings to configure a font with [FontWeight] and [FontStyle] 339 * 340 * @param weight to set 'wght' with [weight]\([FontWeight.weight]) 341 * @param style to set 'ital' with [italic]\([FontStyle.value]) 342 * @param settings other settings to apply, must not contain 'wght' or 'ital' 343 * @return settings that configure [FontWeight] and [FontStyle] on a font that supports 'wght' 344 * and 'ital' 345 */ 346 fun Settings(weight: FontWeight, style: FontStyle, vararg settings: Setting): Settings { 347 return Settings(weight(weight.weight), italic(style.value.toFloat()), *settings) 348 } 349 } 350