Skip to main content

frust_material/tokens/
type_scale.rs

1//! [`type_scale`]: the Material 3 (+Expressive) type scale — 15 baseline
2//! roles plus 15 `_emphasized` siblings (30 [`TextStyle`] slots total),
3//! resolving to the bundled Roboto Flex family.
4//!
5//! Source for the numeric progression: <https://m3.material.io/styles/typography/type-scale-tokens>
6//! (verified 2026-07-17; sizes in sp, treated 1:1 as logical px; line heights
7//! are absolute logical pixels, `LineHeight::Absolute`, not a font-size-relative
8//! ratio — M3 publishes them as fixed px values per token, not a ratio).
9//! Cross-verified against Jetpack Compose Material3's generated token file
10//! (`androidx.compose.material3.tokens.TypographyTokens`/`TypeScaleTokens`,
11//! `VERSION: v0_103`, retrieved/verified 2026-07-18) — the emphasized-sibling
12//! weight-step-up rule below traces to that source (see "Emphasized type
13//! scale").
14//!
15//! # M3E true-up (2026-08-19)
16//!
17//! Re-verified against `paadevelopments/material_3_expressive` v1.0.8
18//! (pub.dev; `github.com/paadevelopments/material_3_expressive`),
19//! `lib/foundations/m3e_typography.dart`'s `M3ETypeScale.baseline()` factory
20//! (retrieved 2026-08-19). **Zero divergence**: every one of the 15 baseline
21//! roles' `(size, line_height, letter_spacing, weight)` below matches the
22//! Dart source's `style(...)` call for that role exactly —
23//!
24//! | Role | `m3e_typography.dart` line | Dart tuple `(size, height, tracking, weight)` |
25//! |---|---|---|
26//! | `display_large` | `m3e_typography.dart:51` | `(57, 64, -0.25, w400)` |
27//! | `display_medium` | `m3e_typography.dart:52` | `(45, 52, 0, w400)` |
28//! | `display_small` | `m3e_typography.dart:53` | `(36, 44, 0, w400)` |
29//! | `headline_large` | `m3e_typography.dart:54` | `(32, 40, 0, w400)` |
30//! | `headline_medium` | `m3e_typography.dart:55` | `(28, 36, 0, w400)` |
31//! | `headline_small` | `m3e_typography.dart:56` | `(24, 32, 0, w400)` |
32//! | `title_large` | `m3e_typography.dart:57` | `(22, 28, 0, w400)` |
33//! | `title_medium` | `m3e_typography.dart:58` | `(16, 24, 0.15, w500)` |
34//! | `title_small` | `m3e_typography.dart:59` | `(14, 20, 0.1, w500)` |
35//! | `body_large` | `m3e_typography.dart:60` | `(16, 24, 0.5, w400)` |
36//! | `body_medium` | `m3e_typography.dart:61` | `(14, 20, 0.25, w400)` |
37//! | `body_small` | `m3e_typography.dart:62` | `(12, 16, 0.4, w400)` |
38//! | `label_large` | `m3e_typography.dart:63` | `(14, 20, 0.1, w500)` |
39//! | `label_medium` | `m3e_typography.dart:64` | `(12, 16, 0.5, w500)` |
40//! | `label_small` | `m3e_typography.dart:65` | `(11, 16, 0.5, w500)` |
41//!
42//! (Dart's `height` field is a font-size-relative ratio, `TextStyle(fontSize:
43//! size, height: height / size, ...)` per `m3e_typography.dart:38-45` — the
44//! same absolute line-height-in-px value this module already stores, just
45//! expressed as Flutter's unitless multiplier; not a divergence.) This also
46//! reinforces, rather than contests, the existing `titleLarge` note below: the
47//! M3E reference agrees with m3.material.io that it is Regular (400), not the
48//! secondary source's claimed 500.
49//!
50//! **The emphasized siblings are NOT diffable against this source.**
51//! `m3e_typography.dart` carries no per-role emphasized table at all — M3E's
52//! "emphasized" concept (`M3ETypeVariations.emphasized`,
53//! `m3e_typography.dart:261-265`) is a single variable-font axis preset
54//! (`FontVariation('wght', 600)` + `FontVariation('GRAD', 50)`) applied
55//! uniformly across the whole scale via `M3ETypeScale.apply(fontVariations:
56//! ...)`, structurally different from this catalog's per-role Compose-derived
57//! weight-step-up table (Regular→Medium, Medium→Bold — see "Emphasized type
58//! scale" below). Since the source provides no per-role emphasized values to
59//! diff against, and `GRAD` has no expressible seam in `frust-text` anyway
60//! (see this task's variable-axes spike), the existing per-role table is kept
61//! unchanged — no correction to record.
62//!
63//! # Emphasized type scale
64//!
65//! [`TypeScale`] additionally carries 15 `_emphasized` variants (one per
66//! baseline role, 30 slots total). **Role-count resolution:** an earlier
67//! belief that emphasized variants were "15 baseline + 15 emphasized (30
68//! total), applied to Display/Headline/Title roles" was contested — the "30
69//! total" count was right but the "Display/Headline/Title only" scope was
70//! wrong. Verified directly against the primary source: Jetpack Compose
71//! Material3's generated token file (see citation above) — Compose's
72//! `Typography` class doc comments enumerate an `*Emphasized` property for
73//! *all 15* baseline roles: `displayLarge` through `labelSmall`, not a
74//! Display/Headline/Title-only subset. This module follows that: every one of
75//! the 15 baseline roles gets an emphasized sibling.
76//!
77//! **M3 deltas** (from the same Compose source): size and line height are
78//! unchanged between a role's baseline and emphasized token — only weight
79//! (and, for a handful of roles, letter spacing) shift. Weight always steps
80//! up one rung from the baseline token's own weight: Regular → Medium for
81//! every Regular-weight baseline role (`display_*`, `headline_*`,
82//! `title_large`, `body_*`), and Medium → Bold for every Medium-weight
83//! baseline role (`title_medium`, `title_small`, `label_*`) — so every
84//! emphasized style is guaranteed to differ from its base in weight. Letter
85//! spacing mostly matches the baseline value already in this module's
86//! tables; `body_large` is the one role whose emphasized tracking differs
87//! from its own baseline (0.5px baseline → 0.15px emphasized, matching the
88//! source's `BodyLargeEmphasizedTracking`).
89//!
90//! # Family: Roboto Flex
91//!
92//! [`apply_type`] overrides `family` to [`material_family`] on every slot,
93//! regardless of what `base.family` a caller passes — the same pattern
94//! `frust-cupertino`'s `apply_type`/`sf_family_for_size` uses for its
95//! Text/Display optical-size switch (`plugins/cupertino/src/tokens.rs`'s
96//! `apply_type`, whose own doc comment states the same "overridden per slot,
97//! not inherited from `base`" rule) and `frust-shadcn`'s `sans_family`/
98//! `mono_family` (`plugins/shadcn/src/tokens/theme.rs:118-133`) use for their
99//! own bundled faces.
100//!
101//! **Evidence for the decision** (family named, not left unset):
102//! - The M3E reference bundles Roboto Flex as its example app's font and sets
103//!   it explicitly (`example/pubspec.yaml:28-30`; `README.md:225-235`,
104//!   `paadevelopments/material_3_expressive` v1.0.8) — this catalog mirrors
105//!   that as the design-system's own baseline choice, the same way
106//!   `frust-glyph`/`frust-shadcn` name their own bundled faces directly on
107//!   their type scales rather than leaving `family` unset
108//!   (`plugins/glyph/src/tokens/scales.rs`'s `Face::family`;
109//!   `plugins/shadcn/src/tokens/theme.rs`'s `sans_family`/`mono_family`).
110//! - **A named-but-unregistered family degrades gracefully, it does not
111//!   break rendering**: `frust-text`'s [`FontFamily::Named`] doc comment
112//!   states "a name that doesn't resolve on the current platform falls back
113//!   per parley's own fallback behavior — no error surface is exposed here"
114//!   (`crates/frust-text/src/style.rs:44-51`, line 48 for the fallback
115//!   sentence itself). [`FontFamily::stack_with_generic`] is the
116//!   fallback-safe constructor this module uses (over a bare
117//!   [`FontFamily::named`]) so the stack always ends in a generic slot,
118//!   matching every other bundled-font catalog's own choice.
119//! - **A registered family resolves via the font's own `name` table**, not a
120//!   hand-picked string: `TextContext::register_fonts` reports back the
121//!   family fontique's `Collection::family_name` resolved the registered
122//!   face into (`crates/frust-text/src/context.rs:607-616`), and shadows a
123//!   same-named system family once registered
124//!   (`crates/frust-text/src/context.rs:557-561`). The Roboto Flex face this
125//!   family name targets reports family name `"Roboto Flex"` in its own
126//!   `name` table (`name` ID 1) — verified with `fontTools.ttLib` against the
127//!   upstream Google-Fonts-sourced release
128//!   `material_3_expressive`'s own copy bundles
129//!   (`example/assets/fonts/RobotoFlex.ttf`, the same face
130//!   `plugins/material/fonts/RobotoFlex.ttf` is sourced from), so
131//!   [`ROBOTO_FLEX_FAMILY`]'s literal string is what a registered face
132//!   actually resolves to, not a guess.
133//!
134//! **Degrade documented**: an app that never calls `frust_material::install()`
135//! (or otherwise never drains `frust::register_app_fonts` with the bundled
136//! Roboto Flex bytes) never registers the family, so every role in this type
137//! scale falls back to the platform's generic sans-serif — no error, no
138//! panic, just a different (but still legible) rendered face. `install()`'s
139//! own font-registration wiring is this crate's `baseline()` concern, not
140//! this module's.
141
142use super::ROBOTO_FLEX_FAMILY;
143use frust::TypeScale;
144use frust::authoring::text::{FontFamily, FontWeight, GenericSlot, LineHeight, TextStyle};
145
146/// The Material 3 Expressive type family: the bundled Roboto Flex face, then
147/// the platform's generic sans — so text still renders (in the system sans)
148/// on a host that never registered the face. See this module's "Family:
149/// Roboto Flex" doc section.
150fn material_family() -> FontFamily {
151    FontFamily::stack_with_generic([ROBOTO_FLEX_FAMILY], GenericSlot::SansSerif)
152}
153
154/// One type-scale token's numeric shape: `(size_px, line_height_px,
155/// letter_spacing_px, weight)`.
156type TypeToken = (f32, f32, f32, FontWeight);
157
158const DISPLAY_LARGE: TypeToken = (57.0, 64.0, -0.25, FontWeight::REGULAR);
159const DISPLAY_MEDIUM: TypeToken = (45.0, 52.0, 0.0, FontWeight::REGULAR);
160const DISPLAY_SMALL: TypeToken = (36.0, 44.0, 0.0, FontWeight::REGULAR);
161const HEADLINE_LARGE: TypeToken = (32.0, 40.0, 0.0, FontWeight::REGULAR);
162const HEADLINE_MEDIUM: TypeToken = (28.0, 36.0, 0.0, FontWeight::REGULAR);
163const HEADLINE_SMALL: TypeToken = (24.0, 32.0, 0.0, FontWeight::REGULAR);
164const TITLE_LARGE: TypeToken = (22.0, 28.0, 0.0, FontWeight::REGULAR);
165const TITLE_MEDIUM: TypeToken = (16.0, 24.0, 0.15, FontWeight::MEDIUM);
166const TITLE_SMALL: TypeToken = (14.0, 20.0, 0.1, FontWeight::MEDIUM);
167const BODY_LARGE: TypeToken = (16.0, 24.0, 0.5, FontWeight::REGULAR);
168const BODY_MEDIUM: TypeToken = (14.0, 20.0, 0.25, FontWeight::REGULAR);
169const BODY_SMALL: TypeToken = (12.0, 16.0, 0.4, FontWeight::REGULAR);
170const LABEL_LARGE: TypeToken = (14.0, 20.0, 0.1, FontWeight::MEDIUM);
171const LABEL_MEDIUM: TypeToken = (12.0, 16.0, 0.5, FontWeight::MEDIUM);
172const LABEL_SMALL: TypeToken = (11.0, 16.0, 0.5, FontWeight::MEDIUM);
173
174// M3-Expressive emphasized tokens: same size/line-height as the matching
175// baseline `TypeToken` above in every case; weight steps up one rung from
176// the baseline role's own weight (Regular -> Medium, Medium -> Bold) and
177// letter spacing is the source's `*Emphasized*Tracking` value (see this
178// module's doc comment for the primary-source citation and resolution of
179// the contested Display/Headline/Title-only scope claim).
180const DISPLAY_LARGE_EMPHASIZED: TypeToken = (57.0, 64.0, 0.0, FontWeight::MEDIUM);
181const DISPLAY_MEDIUM_EMPHASIZED: TypeToken = (45.0, 52.0, 0.0, FontWeight::MEDIUM);
182const DISPLAY_SMALL_EMPHASIZED: TypeToken = (36.0, 44.0, 0.0, FontWeight::MEDIUM);
183const HEADLINE_LARGE_EMPHASIZED: TypeToken = (32.0, 40.0, 0.0, FontWeight::MEDIUM);
184const HEADLINE_MEDIUM_EMPHASIZED: TypeToken = (28.0, 36.0, 0.0, FontWeight::MEDIUM);
185const HEADLINE_SMALL_EMPHASIZED: TypeToken = (24.0, 32.0, 0.0, FontWeight::MEDIUM);
186const TITLE_LARGE_EMPHASIZED: TypeToken = (22.0, 28.0, 0.0, FontWeight::MEDIUM);
187const TITLE_MEDIUM_EMPHASIZED: TypeToken = (16.0, 24.0, 0.15, FontWeight::BOLD);
188const TITLE_SMALL_EMPHASIZED: TypeToken = (14.0, 20.0, 0.1, FontWeight::BOLD);
189const BODY_LARGE_EMPHASIZED: TypeToken = (16.0, 24.0, 0.15, FontWeight::MEDIUM);
190const BODY_MEDIUM_EMPHASIZED: TypeToken = (14.0, 20.0, 0.25, FontWeight::MEDIUM);
191const BODY_SMALL_EMPHASIZED: TypeToken = (12.0, 16.0, 0.4, FontWeight::MEDIUM);
192const LABEL_LARGE_EMPHASIZED: TypeToken = (14.0, 20.0, 0.1, FontWeight::BOLD);
193const LABEL_MEDIUM_EMPHASIZED: TypeToken = (12.0, 16.0, 0.5, FontWeight::BOLD);
194const LABEL_SMALL_EMPHASIZED: TypeToken = (11.0, 16.0, 0.5, FontWeight::BOLD);
195
196/// Applies one `token` onto `base`: `family` is always [`material_family`]
197/// (overridden regardless of `base`, see this module's "Family: Roboto Flex"
198/// doc section); `style`/`color`/`align` are preserved from `base`.
199fn apply_type(base: &TextStyle, token: TypeToken) -> TextStyle {
200    let (size, line_height_px, letter_spacing, weight) = token;
201    TextStyle {
202        family: material_family(),
203        size,
204        weight,
205        letter_spacing,
206        line_height: LineHeight::Absolute(line_height_px),
207        ..base.clone()
208    }
209}
210
211/// Builds the Material 3 baseline type scale from `base` (its
212/// `style`/`color`/`align` are preserved on every token; `size`/`weight`/
213/// `letter_spacing`/`line_height`/`family` are all Material-specified —
214/// `family` is overridden to the bundled Roboto Flex stack regardless of
215/// `base`, see this module's "Family: Roboto Flex" doc section for why).
216pub fn type_scale(base: &TextStyle) -> TypeScale {
217    TypeScale {
218        display_large: apply_type(base, DISPLAY_LARGE),
219        display_medium: apply_type(base, DISPLAY_MEDIUM),
220        display_small: apply_type(base, DISPLAY_SMALL),
221        headline_large: apply_type(base, HEADLINE_LARGE),
222        headline_medium: apply_type(base, HEADLINE_MEDIUM),
223        headline_small: apply_type(base, HEADLINE_SMALL),
224        title_large: apply_type(base, TITLE_LARGE),
225        title_medium: apply_type(base, TITLE_MEDIUM),
226        title_small: apply_type(base, TITLE_SMALL),
227        body_large: apply_type(base, BODY_LARGE),
228        body_medium: apply_type(base, BODY_MEDIUM),
229        body_small: apply_type(base, BODY_SMALL),
230        label_large: apply_type(base, LABEL_LARGE),
231        label_medium: apply_type(base, LABEL_MEDIUM),
232        label_small: apply_type(base, LABEL_SMALL),
233        display_large_emphasized: apply_type(base, DISPLAY_LARGE_EMPHASIZED),
234        display_medium_emphasized: apply_type(base, DISPLAY_MEDIUM_EMPHASIZED),
235        display_small_emphasized: apply_type(base, DISPLAY_SMALL_EMPHASIZED),
236        headline_large_emphasized: apply_type(base, HEADLINE_LARGE_EMPHASIZED),
237        headline_medium_emphasized: apply_type(base, HEADLINE_MEDIUM_EMPHASIZED),
238        headline_small_emphasized: apply_type(base, HEADLINE_SMALL_EMPHASIZED),
239        title_large_emphasized: apply_type(base, TITLE_LARGE_EMPHASIZED),
240        title_medium_emphasized: apply_type(base, TITLE_MEDIUM_EMPHASIZED),
241        title_small_emphasized: apply_type(base, TITLE_SMALL_EMPHASIZED),
242        body_large_emphasized: apply_type(base, BODY_LARGE_EMPHASIZED),
243        body_medium_emphasized: apply_type(base, BODY_MEDIUM_EMPHASIZED),
244        body_small_emphasized: apply_type(base, BODY_SMALL_EMPHASIZED),
245        label_large_emphasized: apply_type(base, LABEL_LARGE_EMPHASIZED),
246        label_medium_emphasized: apply_type(base, LABEL_MEDIUM_EMPHASIZED),
247        label_small_emphasized: apply_type(base, LABEL_SMALL_EMPHASIZED),
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use peniko::Color;
254
255    use super::*;
256
257    #[test]
258    fn type_scale_display_large_matches_table() {
259        let scale = type_scale(&TextStyle::new(16.0, Color::BLACK));
260        assert_eq!(scale.display_large.size, 57.0);
261        assert_eq!(scale.display_large.line_height, LineHeight::Absolute(64.0));
262        assert_eq!(scale.display_large.letter_spacing, -0.25);
263        assert_eq!(scale.display_large.weight, FontWeight::REGULAR);
264    }
265
266    #[test]
267    fn type_scale_preserves_base_style_and_color_but_overrides_family() {
268        let base = TextStyle {
269            family: FontFamily::named("Roboto"),
270            ..TextStyle::new(16.0, Color::from_rgb8(1, 2, 3))
271        };
272        let scale = type_scale(&base);
273        assert_ne!(scale.body_large.family, base.family);
274        assert_eq!(scale.body_large.family, material_family());
275        assert_eq!(scale.body_large.color, base.color);
276        assert_eq!(scale.body_large.style, base.style);
277    }
278
279    #[test]
280    fn type_scale_every_role_resolves_to_roboto_flex() {
281        let scale = type_scale(&TextStyle::default());
282        let expected = material_family();
283        let roles: [&TextStyle; 30] = [
284            &scale.display_large,
285            &scale.display_medium,
286            &scale.display_small,
287            &scale.headline_large,
288            &scale.headline_medium,
289            &scale.headline_small,
290            &scale.title_large,
291            &scale.title_medium,
292            &scale.title_small,
293            &scale.body_large,
294            &scale.body_medium,
295            &scale.body_small,
296            &scale.label_large,
297            &scale.label_medium,
298            &scale.label_small,
299            &scale.display_large_emphasized,
300            &scale.display_medium_emphasized,
301            &scale.display_small_emphasized,
302            &scale.headline_large_emphasized,
303            &scale.headline_medium_emphasized,
304            &scale.headline_small_emphasized,
305            &scale.title_large_emphasized,
306            &scale.title_medium_emphasized,
307            &scale.title_small_emphasized,
308            &scale.body_large_emphasized,
309            &scale.body_medium_emphasized,
310            &scale.body_small_emphasized,
311            &scale.label_large_emphasized,
312            &scale.label_medium_emphasized,
313            &scale.label_small_emphasized,
314        ];
315        for role in roles {
316            assert_eq!(role.family, expected);
317        }
318    }
319
320    #[test]
321    fn type_scale_every_emphasized_role_differs_in_weight_from_its_base() {
322        let scale = type_scale(&TextStyle::new(16.0, Color::BLACK));
323        let pairs: [(&TextStyle, &TextStyle); 15] = [
324            (&scale.display_large, &scale.display_large_emphasized),
325            (&scale.display_medium, &scale.display_medium_emphasized),
326            (&scale.display_small, &scale.display_small_emphasized),
327            (&scale.headline_large, &scale.headline_large_emphasized),
328            (&scale.headline_medium, &scale.headline_medium_emphasized),
329            (&scale.headline_small, &scale.headline_small_emphasized),
330            (&scale.title_large, &scale.title_large_emphasized),
331            (&scale.title_medium, &scale.title_medium_emphasized),
332            (&scale.title_small, &scale.title_small_emphasized),
333            (&scale.body_large, &scale.body_large_emphasized),
334            (&scale.body_medium, &scale.body_medium_emphasized),
335            (&scale.body_small, &scale.body_small_emphasized),
336            (&scale.label_large, &scale.label_large_emphasized),
337            (&scale.label_medium, &scale.label_medium_emphasized),
338            (&scale.label_small, &scale.label_small_emphasized),
339        ];
340        for (b, emphasized) in pairs {
341            assert_ne!(
342                b.weight, emphasized.weight,
343                "expected emphasized weight to differ from base weight"
344            );
345        }
346    }
347
348    /// Per-role metric table: every one of the 30 slots pinned against its
349    /// literal `(size, line_height_px, letter_spacing, weight)` — the
350    /// `TypeToken` tuples above, transcribed as literals so this test catches
351    /// an accidental edit to those consts the same way it would a source
352    /// transcription error. Values match `m3e_typography.dart:51-65` for the
353    /// 15 baseline roles (see this module's "M3E true-up" doc section); the
354    /// emphasized 15 are this catalog's own Compose-derived weight-step-up
355    /// table (see "Emphasized type scale").
356    #[test]
357    fn type_scale_every_role_matches_its_literal_metric_table() {
358        let scale = type_scale(&TextStyle::new(16.0, Color::BLACK));
359        let table: [(&str, &TextStyle, f32, f32, f32, FontWeight); 30] = [
360            (
361                "display_large",
362                &scale.display_large,
363                57.0,
364                64.0,
365                -0.25,
366                FontWeight::REGULAR,
367            ),
368            (
369                "display_medium",
370                &scale.display_medium,
371                45.0,
372                52.0,
373                0.0,
374                FontWeight::REGULAR,
375            ),
376            (
377                "display_small",
378                &scale.display_small,
379                36.0,
380                44.0,
381                0.0,
382                FontWeight::REGULAR,
383            ),
384            (
385                "headline_large",
386                &scale.headline_large,
387                32.0,
388                40.0,
389                0.0,
390                FontWeight::REGULAR,
391            ),
392            (
393                "headline_medium",
394                &scale.headline_medium,
395                28.0,
396                36.0,
397                0.0,
398                FontWeight::REGULAR,
399            ),
400            (
401                "headline_small",
402                &scale.headline_small,
403                24.0,
404                32.0,
405                0.0,
406                FontWeight::REGULAR,
407            ),
408            (
409                "title_large",
410                &scale.title_large,
411                22.0,
412                28.0,
413                0.0,
414                FontWeight::REGULAR,
415            ),
416            (
417                "title_medium",
418                &scale.title_medium,
419                16.0,
420                24.0,
421                0.15,
422                FontWeight::MEDIUM,
423            ),
424            (
425                "title_small",
426                &scale.title_small,
427                14.0,
428                20.0,
429                0.1,
430                FontWeight::MEDIUM,
431            ),
432            (
433                "body_large",
434                &scale.body_large,
435                16.0,
436                24.0,
437                0.5,
438                FontWeight::REGULAR,
439            ),
440            (
441                "body_medium",
442                &scale.body_medium,
443                14.0,
444                20.0,
445                0.25,
446                FontWeight::REGULAR,
447            ),
448            (
449                "body_small",
450                &scale.body_small,
451                12.0,
452                16.0,
453                0.4,
454                FontWeight::REGULAR,
455            ),
456            (
457                "label_large",
458                &scale.label_large,
459                14.0,
460                20.0,
461                0.1,
462                FontWeight::MEDIUM,
463            ),
464            (
465                "label_medium",
466                &scale.label_medium,
467                12.0,
468                16.0,
469                0.5,
470                FontWeight::MEDIUM,
471            ),
472            (
473                "label_small",
474                &scale.label_small,
475                11.0,
476                16.0,
477                0.5,
478                FontWeight::MEDIUM,
479            ),
480            (
481                "display_large_emphasized",
482                &scale.display_large_emphasized,
483                57.0,
484                64.0,
485                0.0,
486                FontWeight::MEDIUM,
487            ),
488            (
489                "display_medium_emphasized",
490                &scale.display_medium_emphasized,
491                45.0,
492                52.0,
493                0.0,
494                FontWeight::MEDIUM,
495            ),
496            (
497                "display_small_emphasized",
498                &scale.display_small_emphasized,
499                36.0,
500                44.0,
501                0.0,
502                FontWeight::MEDIUM,
503            ),
504            (
505                "headline_large_emphasized",
506                &scale.headline_large_emphasized,
507                32.0,
508                40.0,
509                0.0,
510                FontWeight::MEDIUM,
511            ),
512            (
513                "headline_medium_emphasized",
514                &scale.headline_medium_emphasized,
515                28.0,
516                36.0,
517                0.0,
518                FontWeight::MEDIUM,
519            ),
520            (
521                "headline_small_emphasized",
522                &scale.headline_small_emphasized,
523                24.0,
524                32.0,
525                0.0,
526                FontWeight::MEDIUM,
527            ),
528            (
529                "title_large_emphasized",
530                &scale.title_large_emphasized,
531                22.0,
532                28.0,
533                0.0,
534                FontWeight::MEDIUM,
535            ),
536            (
537                "title_medium_emphasized",
538                &scale.title_medium_emphasized,
539                16.0,
540                24.0,
541                0.15,
542                FontWeight::BOLD,
543            ),
544            (
545                "title_small_emphasized",
546                &scale.title_small_emphasized,
547                14.0,
548                20.0,
549                0.1,
550                FontWeight::BOLD,
551            ),
552            (
553                "body_large_emphasized",
554                &scale.body_large_emphasized,
555                16.0,
556                24.0,
557                0.15,
558                FontWeight::MEDIUM,
559            ),
560            (
561                "body_medium_emphasized",
562                &scale.body_medium_emphasized,
563                14.0,
564                20.0,
565                0.25,
566                FontWeight::MEDIUM,
567            ),
568            (
569                "body_small_emphasized",
570                &scale.body_small_emphasized,
571                12.0,
572                16.0,
573                0.4,
574                FontWeight::MEDIUM,
575            ),
576            (
577                "label_large_emphasized",
578                &scale.label_large_emphasized,
579                14.0,
580                20.0,
581                0.1,
582                FontWeight::BOLD,
583            ),
584            (
585                "label_medium_emphasized",
586                &scale.label_medium_emphasized,
587                12.0,
588                16.0,
589                0.5,
590                FontWeight::BOLD,
591            ),
592            (
593                "label_small_emphasized",
594                &scale.label_small_emphasized,
595                11.0,
596                16.0,
597                0.5,
598                FontWeight::BOLD,
599            ),
600        ];
601        for (name, style, size, line_height_px, letter_spacing, weight) in table {
602            assert_eq!(style.size, size, "{name} size");
603            assert_eq!(
604                style.line_height,
605                LineHeight::Absolute(line_height_px),
606                "{name} line_height"
607            );
608            assert_eq!(
609                style.letter_spacing, letter_spacing,
610                "{name} letter_spacing"
611            );
612            assert_eq!(style.weight, weight, "{name} weight");
613        }
614    }
615}