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