inset_embedder/text.rs
1//! Flutter counterpart: `engine/src/flutter/lib/ui/text.dart` value types
2//! used by painting (`FontWeight`, `TextDecoration`, `TextDirection`, …).
3//! `TextStyle` / `ParagraphStyle` / `Paragraph` are in `paragraph.rs`.
4
5use std::fmt::{self, Debug, Display};
6
7use crate::{clamp_double, lerp_double, lerp_int};
8
9/// A [`TextStyle.height`](https://api.flutter.dev/flutter/dart-ui/TextStyle/height.html)
10/// value that indicates the text span should take the height defined by the
11/// font, which may not be exactly the height of `fontSize`.
12pub const K_TEXT_HEIGHT_NONE: f64 = 0.0;
13
14/// Whether to use the italic type variation of glyphs in the font.
15///
16/// Some modern fonts allow this to be selected in a more fine-grained manner.
17/// See `FontVariation.italic` for details.
18///
19/// Italic type is distinct from slanted glyphs. To control the slant of a
20/// glyph, consider the `FontVariation.slant` font feature.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum FontStyle {
23 /// Use the upright ("Roman") glyphs.
24 Normal,
25
26 /// Use glyphs that have a more pronounced angle and typically a cursive style
27 /// ("italic type").
28 Italic,
29}
30
31/// The thickness of the glyphs used to draw the text.
32///
33/// Values must be in the range 1..1000.
34///
35/// Fonts are typically weighted on a 9-point scale, which, for historical
36/// reasons, uses the names 100 to 900. In Flutter, these are named `w100` to
37/// `w900` and have the following conventional meanings:
38///
39/// * [`w100`](FontWeight::W100): Thin, the thinnest font weight.
40///
41/// * [`w200`](FontWeight::W200): Extra light.
42///
43/// * [`w300`](FontWeight::W300): Light.
44///
45/// * [`w400`](FontWeight::W400): Normal. The constant [`NORMAL`](FontWeight::NORMAL) is an alias for this value.
46///
47/// * [`w500`](FontWeight::W500): Medium.
48///
49/// * [`w600`](FontWeight::W600): Semi-bold.
50///
51/// * [`w700`](FontWeight::W700): Bold. The constant [`BOLD`](FontWeight::BOLD) is an alias for this value.
52///
53/// * [`w800`](FontWeight::W800): Extra-bold.
54///
55/// * [`w900`](FontWeight::W900): Black, the thickest font weight.
56///
57/// For example, the font named "Roboto Medium" is typically exposed as a font
58/// with the name "Roboto" and the weight [`W500`](FontWeight::W500).
59///
60/// Some modern fonts allow the weight to be adjusted in arbitrary increments.
61/// When using these fonts, applications can specify [`FontWeight`] instances
62/// constructed using values other than the predefined values.
63#[derive(Clone, Copy, PartialEq, Eq, Hash)]
64pub struct FontWeight {
65 /// The thickness value of this font weight.
66 pub value: i32,
67}
68
69impl FontWeight {
70 /// Creates a [`FontWeight`] object, which can be added to a text style to
71 /// select the thickness of a font's glyphs.
72 pub fn new(value: i32) -> FontWeight {
73 debug_assert!(
74 (1..=1000).contains(&value),
75 "Font weight must be between 1 and 1000"
76 );
77 FontWeight { value }
78 }
79
80 /// The encoded integer value of this font weight.
81 ///
82 /// Deprecated in Dart in favor of [`value`](Self::value).
83 pub fn index(self) -> i32 {
84 ((self.value / 100) - 1).clamp(0, 8)
85 }
86
87 /// Thin, the least thick.
88 pub const W100: FontWeight = FontWeight { value: 100 };
89
90 /// Extra-light.
91 pub const W200: FontWeight = FontWeight { value: 200 };
92
93 /// Light.
94 pub const W300: FontWeight = FontWeight { value: 300 };
95
96 /// Normal / regular / plain.
97 pub const W400: FontWeight = FontWeight { value: 400 };
98
99 /// Medium.
100 pub const W500: FontWeight = FontWeight { value: 500 };
101
102 /// Semi-bold.
103 pub const W600: FontWeight = FontWeight { value: 600 };
104
105 /// Bold.
106 pub const W700: FontWeight = FontWeight { value: 700 };
107
108 /// Extra-bold.
109 pub const W800: FontWeight = FontWeight { value: 800 };
110
111 /// Black, the most thick.
112 pub const W900: FontWeight = FontWeight { value: 900 };
113
114 /// The default font weight.
115 pub const NORMAL: FontWeight = FontWeight::W400;
116
117 /// A commonly used font weight that is heavier than normal.
118 pub const BOLD: FontWeight = FontWeight::W700;
119
120 /// A list of all the named font weights.
121 pub const VALUES: [FontWeight; 9] = [
122 FontWeight::W100,
123 FontWeight::W200,
124 FontWeight::W300,
125 FontWeight::W400,
126 FontWeight::W500,
127 FontWeight::W600,
128 FontWeight::W700,
129 FontWeight::W800,
130 FontWeight::W900,
131 ];
132
133 /// Linearly interpolates between two font weights.
134 ///
135 /// If both `a` and `b` are null, then this method will return null. Otherwise,
136 /// any null values for `a` or `b` are interpreted as equivalent to [`NORMAL`](FontWeight::NORMAL)
137 /// (also known as [`W400`](FontWeight::W400)).
138 ///
139 /// The `t` argument represents position on the timeline, with 0.0 meaning
140 /// that the interpolation has not started, returning `a` (or something
141 /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
142 /// returning `b` (or something equivalent to `b`), and values in between
143 /// meaning that the interpolation is at the relevant point on the timeline
144 /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
145 /// 1.0, so negative values and values greater than 1.0 are valid. The result
146 /// is clamped to the range [`W100`](FontWeight::W100)–[`W900`](FontWeight::W900).
147 pub fn lerp(a: Option<FontWeight>, b: Option<FontWeight>, t: f64) -> Option<FontWeight> {
148 if a.is_none() && b.is_none() {
149 return None;
150 }
151 let a = a.unwrap_or(FontWeight::NORMAL).value;
152 let b = b.unwrap_or(FontWeight::NORMAL).value;
153 Some(FontWeight::new(
154 lerp_int(a, b, t).round().clamp(100.0, 900.0) as i32,
155 ))
156 }
157}
158
159impl Debug for FontWeight {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 Display::fmt(self, f)
162 }
163}
164
165impl Display for FontWeight {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 if self.value % 100 != 0 {
168 return write!(f, "FontWeight({})", self.value);
169 }
170 let name = match self.index() {
171 0 => "w100",
172 1 => "w200",
173 2 => "w300",
174 3 => "w400",
175 4 => "w500",
176 5 => "w600",
177 6 => "w700",
178 7 => "w800",
179 8 => "w900",
180 _ => unreachable!(),
181 };
182 write!(f, "FontWeight.{name}")
183 }
184}
185
186/// Whether and how to align text horizontally.
187///
188/// The order of this enum must match the order of the values in
189/// RenderStyleConstants.h's ETextAlign.
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
191pub enum TextAlign {
192 /// Align the text on the left edge of the container.
193 Left,
194
195 /// Align the text on the right edge of the container.
196 Right,
197
198 /// Align the text in the center of the container.
199 Center,
200
201 /// Stretch lines of text that end with a soft line break to fill the width of
202 /// the container.
203 ///
204 /// Lines that end with hard line breaks are aligned towards the [`Start`](TextAlign::Start) edge.
205 Justify,
206
207 /// Align the text on the leading edge of the container.
208 ///
209 /// For left-to-right text ([`TextDirection::Ltr`]), this is the left edge.
210 ///
211 /// For right-to-left text ([`TextDirection::Rtl`]), this is the right edge.
212 Start,
213
214 /// Align the text on the trailing edge of the container.
215 ///
216 /// For left-to-right text ([`TextDirection::Ltr`]), this is the right edge.
217 ///
218 /// For right-to-left text ([`TextDirection::Rtl`]), this is the left edge.
219 End,
220}
221
222/// A horizontal line used for aligning text.
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
224pub enum TextBaseline {
225 /// The horizontal line used to align the bottom of glyphs for alphabetic characters.
226 ///
227 /// This baseline is often used for alphabetical scripts like Latin, Greek,
228 /// Cyrillic, etc.
229 ///
230 /// Characters with descenders (like 'p', 'g', or 'y') extend below this line.
231 Alphabetic,
232
233 /// The horizontal line used to align ideographic characters.
234 ///
235 /// This baseline is often used for scripts with uniform square heights,
236 /// like Chinese, Japanese, Korean, etc.
237 Ideographic,
238}
239
240/// A linear decoration to draw near the text.
241#[derive(Clone, Copy, PartialEq, Eq, Hash)]
242pub struct TextDecoration {
243 mask: i32,
244}
245
246impl TextDecoration {
247 /// Do not draw a decoration
248 pub const NONE: TextDecoration = TextDecoration { mask: 0x0 };
249
250 /// Draw a line underneath each line of text
251 pub const UNDERLINE: TextDecoration = TextDecoration { mask: 0x1 };
252
253 /// Draw a line above each line of text
254 pub const OVERLINE: TextDecoration = TextDecoration { mask: 0x2 };
255
256 /// Draw a line through each line of text
257 pub const LINE_THROUGH: TextDecoration = TextDecoration { mask: 0x4 };
258
259 /// Creates a decoration that paints the union of all the given decorations.
260 pub fn combine(decorations: &[TextDecoration]) -> TextDecoration {
261 let mut mask = 0;
262 for decoration in decorations {
263 mask |= decoration.mask;
264 }
265 TextDecoration { mask }
266 }
267
268 /// The bit mask value that represents this decoration.
269 pub fn mask_value(self) -> i32 {
270 self.mask
271 }
272
273 /// Whether this decoration will paint at least as much decoration as the given decoration.
274 pub fn contains(self, other: TextDecoration) -> bool {
275 (self.mask | other.mask) == self.mask
276 }
277}
278
279impl Debug for TextDecoration {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 Display::fmt(self, f)
282 }
283}
284
285impl Display for TextDecoration {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 if self.mask == 0 {
288 return write!(f, "TextDecoration.none");
289 }
290 let mut values = Vec::new();
291 if self.mask & TextDecoration::UNDERLINE.mask != 0 {
292 values.push("underline");
293 }
294 if self.mask & TextDecoration::OVERLINE.mask != 0 {
295 values.push("overline");
296 }
297 if self.mask & TextDecoration::LINE_THROUGH.mask != 0 {
298 values.push("lineThrough");
299 }
300 if values.len() == 1 {
301 write!(f, "TextDecoration.{}", values[0])
302 } else {
303 write!(f, "TextDecoration.combine([{}])", values.join(", "))
304 }
305 }
306}
307
308/// The style in which to draw a text decoration
309#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
310pub enum TextDecorationStyle {
311 /// Draw a solid line
312 Solid,
313
314 /// Draw two lines
315 Double,
316
317 /// Draw a dotted line
318 Dotted,
319
320 /// Draw a dashed line
321 Dashed,
322
323 /// Draw a sinusoidal line
324 Wavy,
325}
326
327/// How the leading of the text is distributed over and under the text.
328#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
329pub enum TextLeadingDistribution {
330 /// Distributes the leading of the text proportionally above and below the
331 /// text, to the font's ascent/descent ratio.
332 Proportional,
333
334 /// Distributes the leading of the text evenly above and below the text
335 /// (i.e. evenly above the font's ascender and below the descender).
336 ///
337 /// The leading can become negative when `TextStyle.height` is smaller than
338 /// 1.0.
339 ///
340 /// This is the default strategy used by CSS, known as "half-leading".
341 Even,
342}
343
344/// Defines how to apply `TextStyle.height` over and under text.
345#[derive(Clone, Copy, Debug, PartialEq)]
346pub struct TextHeightBehavior {
347 /// Whether to apply the `TextStyle.height` modifier to the ascent of the first
348 /// line in the paragraph.
349 pub apply_height_to_first_ascent: bool,
350
351 /// Whether to apply the `TextStyle.height` modifier to the descent of the last
352 /// line in the paragraph.
353 pub apply_height_to_last_descent: bool,
354
355 /// How the leading is distributed over and under the text.
356 pub leading_distribution: TextLeadingDistribution,
357}
358
359impl Default for TextHeightBehavior {
360 fn default() -> TextHeightBehavior {
361 TextHeightBehavior {
362 apply_height_to_first_ascent: true,
363 apply_height_to_last_descent: true,
364 leading_distribution: TextLeadingDistribution::Proportional,
365 }
366 }
367}
368
369/// A direction in which text flows.
370///
371/// Some languages are written from the left to the right (for example, English,
372/// Tamil, or Chinese), while others are written from the right to the left (for
373/// example Aramaic, Hebrew, or Urdu). Some are also written in a mixture, for
374/// example Arabic is mostly written right-to-left, with numerals written
375/// left-to-right.
376///
377/// The text direction must be provided to APIs that render text or lay out
378/// boxes horizontally, so that they can determine which direction to start in:
379/// either right-to-left, [`TextDirection::Rtl`]; or left-to-right,
380/// [`TextDirection::Ltr`].
381///
382/// ## Design discussion
383///
384/// Flutter is designed to address the needs of applications written in any of
385/// the world's currently-used languages, whether they use a right-to-left or
386/// left-to-right writing direction. Flutter does not support other writing
387/// modes, such as vertical text or boustrophedon text, as these are rarely used
388/// in computer programs.
389///
390/// It is common when developing user interface frameworks to pick a default
391/// text direction — typically left-to-right, the direction most familiar to the
392/// engineers working on the framework — because this simplifies the development
393/// of applications on the platform. Unfortunately, this frequently results in
394/// the platform having unexpected left-to-right biases or assumptions, as
395/// engineers will typically miss places where they need to support
396/// right-to-left text. This then results in bugs that only manifest in
397/// right-to-left environments.
398///
399/// In an effort to minimize the extent to which Flutter experiences this
400/// category of issues, the lowest levels of the Flutter framework do not have a
401/// default text reading direction. Any time a reading direction is necessary,
402/// for example when text is to be displayed, or when a
403/// writing-direction-dependent value is to be interpreted, the reading
404/// direction must be explicitly specified. Where possible, such as in `match`
405/// statements, the right-to-left case is listed first, to avoid the impression
406/// that it is an afterthought.
407///
408/// At the higher levels (specifically starting at the widgets library), an
409/// ambient `Directionality` is introduced, which provides a default. Thus, for
410/// instance, a `Text` widget in the scope of a `MaterialApp` widget does not
411/// need to be given an explicit writing direction. The `Directionality.of`
412/// static method can be used to obtain the ambient text direction for a
413/// particular `BuildContext`.
414///
415/// ### Known left-to-right biases in Flutter
416///
417/// Despite the design intent described above, certain left-to-right biases have
418/// nonetheless crept into Flutter's design. These include:
419///
420/// * The `Canvas` origin is at the top left, and the x-axis increases in a
421/// left-to-right direction.
422///
423/// * The default localization in the widgets and material libraries is
424/// American English, which is left-to-right.
425///
426/// ### Visual properties vs directional properties
427///
428/// Many classes in the Flutter framework are offered in two versions, a
429/// visually-oriented variant, and a text-direction-dependent variant. For
430/// example, `EdgeInsets` is described in terms of top, left, right, and
431/// bottom, while `EdgeInsetsDirectional` is described in terms of top, start,
432/// end, and bottom, where start and end correspond to right and left in
433/// right-to-left text and left and right in left-to-right text.
434///
435/// There are distinct use cases for each of these variants.
436///
437/// Text-direction-dependent variants are useful when developing user interfaces
438/// that should "flip" with the text direction. For example, a paragraph of text
439/// in English will typically be left-aligned and a quote will be indented from
440/// the left, while in Arabic it will be right-aligned and indented from the
441/// right. Both of these cases are described by the direction-dependent
442/// [`TextAlign::Start`] and `EdgeInsetsDirectional` start.
443///
444/// In contrast, the visual variants are useful when the text direction is known
445/// and not affected by the reading direction. For example, an application
446/// giving driving directions might show a "turn left" arrow on the left and a
447/// "turn right" arrow on the right — and would do so whether the application
448/// was localized to French (left-to-right) or Hebrew (right-to-left).
449///
450/// In practice, it is also expected that many developers will only be targeting
451/// one language, and in that case it may be simpler to think in visual terms.
452///
453/// The order of this enum must match the order of the values in
454/// `TextDirection.h`'s `TextDirection`.
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
456pub enum TextDirection {
457 /// The text flows from right to left (e.g. Arabic, Hebrew).
458 Rtl,
459
460 /// The text flows from left to right (e.g., English, French).
461 Ltr,
462}
463
464/// A feature tag and value that affect the selection of glyphs in a font.
465///
466/// Named constructors for individual OpenType tags (`alternative`, `fractions`,
467/// …) are deferred until a caller needs them. [`new`](FontFeature::new),
468/// [`enable`](FontFeature::enable), and [`disable`](FontFeature::disable) are
469/// the constructors Flutter's painting `TextStyle` uses.
470#[derive(Clone, Debug, PartialEq, Eq, Hash)]
471pub struct FontFeature {
472 /// The tag that identifies the effect of this feature. Must consist of 4
473 /// ASCII characters.
474 pub feature: String,
475 /// The value assigned to this feature.
476 ///
477 /// Must be a positive integer. Many features are Boolean values using 1
478 /// (on) and 0 (off).
479 pub value: i32,
480}
481
482impl FontFeature {
483 /// Creates a [`FontFeature`] object, which can be added to a text style to
484 /// change how the engine selects glyphs when rendering text.
485 pub fn new(feature: impl Into<String>, value: i32) -> FontFeature {
486 let feature = feature.into();
487 debug_assert!(
488 feature.len() == 4,
489 "Feature tag must be exactly four characters long."
490 );
491 debug_assert!(
492 value >= 0,
493 "Feature value must be zero or a positive integer."
494 );
495 FontFeature { feature, value }
496 }
497
498 /// Create a [`FontFeature`] object that enables the feature with the given
499 /// tag.
500 pub fn enable(feature: impl Into<String>) -> FontFeature {
501 FontFeature::new(feature, 1)
502 }
503
504 /// Create a [`FontFeature`] object that disables the feature with the given
505 /// tag.
506 pub fn disable(feature: impl Into<String>) -> FontFeature {
507 FontFeature::new(feature, 0)
508 }
509}
510
511impl Display for FontFeature {
512 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513 write!(f, "FontFeature('{}', {})", self.feature, self.value)
514 }
515}
516
517/// An axis tag and value that can be used to customize variable fonts.
518///
519/// Named constructors besides [`italic`](FontVariation::italic),
520/// [`optical_size`](FontVariation::optical_size), [`slant`](FontVariation::slant),
521/// [`width`](FontVariation::width), and [`weight`](FontVariation::weight) are
522/// not added until a caller needs them.
523#[derive(Clone, Debug, PartialEq)]
524pub struct FontVariation {
525 /// The tag that identifies the design axis.
526 ///
527 /// An axis tag must consist of 4 ASCII characters.
528 pub axis: String,
529 /// The value assigned to this design axis.
530 pub value: f64,
531}
532
533impl FontVariation {
534 /// Creates a [`FontVariation`] object, which can be added to a text style to
535 /// change the variable attributes of a font.
536 pub fn new(axis: impl Into<String>, value: f64) -> FontVariation {
537 let axis = axis.into();
538 debug_assert!(
539 axis.len() == 4,
540 "Axis tag must be exactly four characters long."
541 );
542 debug_assert!(
543 (-32768.0..32768.0).contains(&value),
544 "Value must be representable as a signed 16.16 fixed-point number, i.e. it must be in this range: -32768.0 ≤ value < 32768.0"
545 );
546 FontVariation { axis, value }
547 }
548
549 /// Variable font style. (`ital`)
550 pub fn italic(value: f64) -> FontVariation {
551 debug_assert!((0.0..=1.0).contains(&value));
552 FontVariation::new("ital", value)
553 }
554
555 /// Optical size optimization. (`opsz`)
556 pub fn optical_size(value: f64) -> FontVariation {
557 debug_assert!(value > 0.0);
558 FontVariation::new("opsz", value)
559 }
560
561 /// Variable font slant. (`slnt`)
562 pub fn slant(value: f64) -> FontVariation {
563 debug_assert!(value > -90.0 && value < 90.0);
564 FontVariation::new("slnt", value)
565 }
566
567 /// Variable font width. (`wdth`)
568 pub fn width(value: f64) -> FontVariation {
569 debug_assert!(value >= 0.0);
570 FontVariation::new("wdth", value)
571 }
572
573 /// Variable font weight. (`wght`)
574 pub fn weight(value: f64) -> FontVariation {
575 debug_assert!((1.0..=1000.0).contains(&value));
576 FontVariation::new("wght", value)
577 }
578
579 /// Linearly interpolates between two font variations.
580 ///
581 /// If the two variations have different axis tags, the interpolation
582 /// switches abruptly from one to the other at t=0.5. Otherwise, the value
583 /// is interpolated.
584 pub fn lerp(
585 a: Option<&FontVariation>,
586 b: Option<&FontVariation>,
587 t: f64,
588 ) -> Option<FontVariation> {
589 if a.map(|v| v.axis.as_str()) != b.map(|v| v.axis.as_str()) || (a.is_none() && b.is_none())
590 {
591 return if t < 0.5 { a.cloned() } else { b.cloned() };
592 }
593 Some(FontVariation::new(
594 a.unwrap().axis.clone(),
595 clamp_double(
596 lerp_double(Some(a.unwrap().value), Some(b.unwrap().value), t).unwrap(),
597 -32768.0,
598 32768.0 - 1.0 / 65536.0,
599 ),
600 ))
601 }
602}
603
604impl Display for FontVariation {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 write!(f, "FontVariation('{}', {})", self.axis, self.value)
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn font_weight_lerp_treats_null_as_normal() {
616 assert!(FontWeight::lerp(None, None, 0.5).is_none());
617 assert_eq!(
618 FontWeight::lerp(None, Some(FontWeight::W700), 0.0),
619 Some(FontWeight::NORMAL)
620 );
621 assert_eq!(
622 FontWeight::lerp(Some(FontWeight::W100), Some(FontWeight::W900), 0.5),
623 Some(FontWeight::W500)
624 );
625 }
626
627 #[test]
628 fn text_decoration_combine_unions_masks() {
629 let combined =
630 TextDecoration::combine(&[TextDecoration::UNDERLINE, TextDecoration::LINE_THROUGH]);
631 assert!(combined.contains(TextDecoration::UNDERLINE));
632 assert!(combined.contains(TextDecoration::LINE_THROUGH));
633 assert!(!combined.contains(TextDecoration::OVERLINE));
634 assert_eq!(
635 format!("{combined}"),
636 "TextDecoration.combine([underline, lineThrough])"
637 );
638 }
639
640 #[test]
641 fn text_direction_lists_rtl_first() {
642 assert_eq!(TextDirection::Rtl as u8, 0);
643 assert_eq!(TextDirection::Ltr as u8, 1);
644 }
645
646 #[test]
647 fn font_variation_lerp_switches_axis_at_half() {
648 let a = FontVariation::weight(400.0);
649 let b = FontVariation::width(100.0);
650 assert_eq!(
651 FontVariation::lerp(Some(&a), Some(&b), 0.4).unwrap().axis,
652 "wght"
653 );
654 assert_eq!(
655 FontVariation::lerp(Some(&a), Some(&b), 0.6).unwrap().axis,
656 "wdth"
657 );
658 }
659
660 #[test]
661 fn font_variation_lerp_interpolates_same_axis() {
662 let a = FontVariation::weight(100.0);
663 let b = FontVariation::weight(900.0);
664 let mid = FontVariation::lerp(Some(&a), Some(&b), 0.5).unwrap();
665 assert_eq!(mid.axis, "wght");
666 assert!((mid.value - 500.0).abs() < 1e-9);
667 }
668}