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}