Skip to main content

compose_rust/
tokens.rs

1//! Design token tables.
2//!
3//! The tables are authored here, in Rust, because Rust is the single source for everything
4//! that has two representations, and because values written in Kotlin by hand would go
5//! unverified. They are *executed* in the Renderer: codegen writes them into
6//! `Protocol.gen.kt` as Kotlin objects, so they enter the Renderer binary at build time and
7//! never cross the boundary at run time. The Host still sends roles and never a token
8//! table.
9//!
10//! What the Renderer implementer still owns is the rules these values feed: how an
11//! elevation is drawn, how each `ButtonVariant` is styled, and motion.
12
13use crate::schema::{
14    COLOR_ROLE_COUNT, Color, ColorRole, ColorScheme, DesignSystem, ShapeRole, SpaceRole, TypeRole,
15};
16
17/// One `ColorRole` slot in both schemes, in `ColorRole` tag order.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct ColorToken {
20    pub role: ColorRole,
21    pub light: Color,
22    pub dark: Color,
23}
24
25/// One rung of the nine-rung type ladder (13.2).
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct TypeToken {
28    pub role: TypeRole,
29    /// Font size in sp.
30    pub size: f32,
31    /// 100 to 900.
32    pub weight: u16,
33    /// Line height in sp.
34    pub line_height: f32,
35    /// Letter spacing in sp, which may be negative.
36    pub letter_spacing: f32,
37    /// True when the rung uses the monospace family rather than the default one.
38    pub monospace: bool,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct ShapeToken {
43    pub role: ShapeRole,
44    /// Corner radius in dp. `Full` uses a radius large enough to read as a pill.
45    pub radius: f32,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct SpaceToken {
50    pub role: SpaceRole,
51    /// Spacing in dp.
52    pub value: f32,
53}
54
55/// Items 1 to 4 of the 14.6 table for one design system.
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct DesignTokenTable {
58    pub system: DesignSystem,
59    /// The published guideline the values come from, recorded as 14.3 asks.
60    pub reference: &'static str,
61    pub default_family: &'static str,
62    pub monospace_family: &'static str,
63    pub colors: &'static [ColorToken; COLOR_ROLE_COUNT],
64    pub type_scale: &'static [TypeToken; 9],
65    pub shapes: &'static [ShapeToken; 6],
66    pub spaces: &'static [SpaceToken; 7],
67}
68
69impl DesignTokenTable {
70    pub fn color(&self, role: ColorRole, scheme: ColorScheme) -> Color {
71        let token = self.colors[role as usize - 1];
72        // `FollowSystem` is resolved by the Renderer, which is the side that knows.
73        // Asking the Host for it can only mean light.
74        if scheme == ColorScheme::Dark {
75            token.dark
76        } else {
77            token.light
78        }
79    }
80}
81
82/// Every design system's table, in `DesignSystem` tag order.
83///
84/// A fourth design system is one entry here plus one Renderer rule implementation.
85/// Nothing in `widgets.rs`, the Modifier schema or the Property schema moves (14.1).
86pub const DESIGN_TOKENS: &[DesignTokenTable] = &[
87    MATERIAL3,
88    APPLE_HIG,
89    FLUENT,
90    ADWAITA,
91    BREEZE,
92    DEEPIN,
93    LIQUID_GLASS,
94];
95
96pub fn table(system: DesignSystem) -> &'static DesignTokenTable {
97    &DESIGN_TOKENS[system as usize - 1]
98}
99
100macro_rules! colors {
101    ($($role:ident: $light:literal / $dark:literal),+ $(,)?) => {
102        &[$(ColorToken {
103            role: ColorRole::$role,
104            light: Color::rgb($light),
105            dark: Color::rgb($dark),
106        }),+]
107    };
108}
109
110macro_rules! type_scale {
111    ($($role:ident: $size:literal / $weight:literal / $line:literal / $spacing:literal / $mono:literal),+ $(,)?) => {
112        &[$(TypeToken {
113            role: TypeRole::$role,
114            size: $size,
115            weight: $weight,
116            line_height: $line,
117            letter_spacing: $spacing,
118            monospace: $mono,
119        }),+]
120    };
121}
122
123macro_rules! shapes {
124    ($($role:ident: $radius:literal),+ $(,)?) => {
125        &[$(ShapeToken { role: ShapeRole::$role, radius: $radius }),+]
126    };
127}
128
129macro_rules! spaces {
130    ($($role:ident: $value:literal),+ $(,)?) => {
131        &[$(SpaceToken { role: SpaceRole::$role, value: $value }),+]
132    };
133}
134
135const MATERIAL3: DesignTokenTable = DesignTokenTable {
136    system: DesignSystem::Material3,
137    reference: "Material 3 Expressive shape, colour and type scales, m3.material.io, 2025",
138    default_family: "Roboto",
139    monospace_family: "Roboto Mono",
140    colors: colors! {
141        Primary: 0x6750a4 / 0xd0bcff,
142        OnPrimary: 0xffffff / 0x381e72,
143        Secondary: 0x625b71 / 0xccc2dc,
144        OnSecondary: 0xffffff / 0x332d41,
145        Surface: 0xfef7ff / 0x141218,
146        OnSurface: 0x1d1b20 / 0xe6e0e9,
147        SurfaceVariant: 0xe7e0ec / 0x49454f,
148        OnSurfaceVariant: 0x49454f / 0xcac4d0,
149        Background: 0xfef7ff / 0x141218,
150        OnBackground: 0x1d1b20 / 0xe6e0e9,
151        Outline: 0x79747e / 0x938f99,
152        OutlineVariant: 0xcac4d0 / 0x49454f,
153        Error: 0xb3261e / 0xf2b8b5,
154        OnError: 0xffffff / 0x601410,
155        // Material 3's own `surfaceContainer`, which is how this system raises a panel:
156        // `Surface` and `Background` are one value here by design, so the container roles
157        // are the only thing that separates a layer from the page.
158        SurfaceContainer: 0xf3edf7 / 0x211f26,
159        // The baseline scheme's own third accent and its four container tones. Material 3
160        // is where the container idea comes from, so these are its published values rather
161        // than anything derived here.
162        Tertiary: 0x7d5260 / 0xefb8c8,
163        OnTertiary: 0xffffff / 0x492532,
164        PrimaryContainer: 0xeaddff / 0x4f378b,
165        OnPrimaryContainer: 0x21005d / 0xeaddff,
166        SecondaryContainer: 0xe8def8 / 0x4a4458,
167        OnSecondaryContainer: 0x1d192b / 0xe8def8,
168        TertiaryContainer: 0xffd8e4 / 0x633b48,
169        OnTertiaryContainer: 0x31111d / 0xffd8e4,
170        // Code colours. Material 3 publishes no code editor scheme, so these are derived from
171        // the base roles above by the fixed rule in `code_colours.rs`: each syntax ink takes one
172        // base role, the removed ink is the error role, the added ink is the error role turned
173        // to a green hue at the same tone and chroma, and the line and word backgrounds are those
174        // inks laid over the panel. `fr13_1_3_derived_code_colours_follow_the_rule` recomputes
175        // every one of them, so a change to a base role has to be carried through here.
176        SyntaxKeyword: 0x6750a4 / 0xd0bcff,
177        SyntaxString: 0x7d5260 / 0xefb8c8,
178        SyntaxComment: 0x49454f / 0xcac4d0,
179        SyntaxNumber: 0x31111d / 0xffd8e4,
180        SyntaxConstant: 0x31111d / 0xffd8e4,
181        SyntaxType: 0x625b71 / 0xccc2dc,
182        SyntaxFunction: 0x21005d / 0xeaddff,
183        SyntaxVariable: 0x1d1b20 / 0xe6e0e9,
184        SyntaxProperty: 0x1d192b / 0xe8def8,
185        SyntaxOperator: 0x49454f / 0xcac4d0,
186        SyntaxPunctuation: 0x49454f / 0xcac4d0,
187        SyntaxTag: 0x6750a4 / 0xd0bcff,
188        SyntaxAttribute: 0x625b71 / 0xccc2dc,
189        SyntaxEscape: 0xb3261e / 0xf2b8b5,
190        SyntaxMacro: 0x625b71 / 0xccc2dc,
191        DiffAdded: 0x006d17 / 0xb2ceaa,
192        DiffRemoved: 0xb3261e / 0xf2b8b5,
193        DiffModified: 0x7d5260 / 0xefb8c8,
194        DiffAddedContainer: 0xd6dedc / 0x3e4240,
195        DiffRemovedContainer: 0xebd5dd / 0x4b3e43,
196        DiffAddedEmphasis: 0xaac7b4 / 0x5b655b,
197        DiffRemovedEmphasis: 0xe0b1b6 / 0x755c5f,
198    },
199    type_scale: type_scale! {
200        Display: 57.0 / 400 / 64.0 / 0.0 / false,
201        Headline: 32.0 / 400 / 40.0 / 0.0 / false,
202        Title: 22.0 / 400 / 28.0 / 0.0 / false,
203        Subtitle: 16.0 / 500 / 24.0 / 0.15 / false,
204        Body: 16.0 / 400 / 24.0 / 0.5 / false,
205        BodyStrong: 16.0 / 500 / 24.0 / 0.15 / false,
206        Label: 14.0 / 500 / 20.0 / 0.1 / false,
207        Caption: 12.0 / 400 / 16.0 / 0.4 / false,
208        Mono: 14.0 / 400 / 20.0 / 0.0 / true,
209    },
210    // The expressive ladder, which is the baseline one shifted up a rung at every step
211    // above the smallest. This is the loudest thing about the reference screens: a card
212    // there is cut at twenty eight rather than sixteen, a sheet is rounder still, and the
213    // buttons and the chips and the selected destination are all capsules. The baseline
214    // ladder drew the same layout with corners half the size, which reads as the previous
215    // version of Material rather than as this one.
216    shapes: shapes! {
217        None: 0.0,
218        ExtraSmall: 4.0,
219        Small: 12.0,
220        Medium: 16.0,
221        Large: 28.0,
222        Full: 1000.0,
223    },
224    spaces: spaces! {
225        None: 0.0,
226        Xs: 4.0,
227        Sm: 8.0,
228        Md: 16.0,
229        Lg: 24.0,
230        Xl: 32.0,
231        Xxl: 48.0,
232    },
233};
234
235const APPLE_HIG: DesignTokenTable = DesignTokenTable {
236    system: DesignSystem::Cupertino,
237    reference: "Apple Human Interface Guidelines, system colors and Dynamic Type, 2024",
238    default_family: "SF Pro",
239    monospace_family: "SF Mono",
240    colors: colors! {
241        Primary: 0x007aff / 0x0a84ff,
242        OnPrimary: 0xffffff / 0xffffff,
243        Secondary: 0x5856d6 / 0x5e5ce6,
244        OnSecondary: 0xffffff / 0xffffff,
245        Surface: 0xffffff / 0x1c1c1e,
246        OnSurface: 0x000000 / 0xffffff,
247        // The page is the grouped background and the variant is the colour an incoming
248        // message bubble has. They used to carry the same value, so anything filled with
249        // one on a page of the other was drawn, in the right colour, and invisible.
250        SurfaceVariant: 0xe9e9eb / 0x2c2c2e,
251        OnSurfaceVariant: 0x3c3c43 / 0xebebf5,
252        Background: 0xf2f2f7 / 0x000000,
253        OnBackground: 0x000000 / 0xffffff,
254        Outline: 0xc6c6c8 / 0x38383a,
255        OutlineVariant: 0xe5e5ea / 0x48484a,
256        Error: 0xff3b30 / 0xff453a,
257        OnError: 0xffffff / 0xffffff,
258        // secondarySystemGroupedBackground: the colour of a grouped box sitting on the
259        // grouped page. It matches `Surface` here, and that is correct rather than a
260        // duplicate: on iOS a panel on the grouped page is the plain reading surface, and
261        // what makes it a panel is that the page underneath is not.
262        SurfaceContainer: 0xffffff / 0x1c1c1e,
263        // systemPurple, the third of the platform accents after blue and indigo.
264        Tertiary: 0xaf52de / 0xbf5af2,
265        OnTertiary: 0xffffff / 0xffffff,
266        // iOS has no published container tones, so these are the tinted fills the platform
267        // draws by hand: a wash of the accent in light, and a deep, desaturated version of
268        // it in dark, which is what a selected row or a tinted card looks like there.
269        PrimaryContainer: 0xd6e4ff / 0x0a2d52,
270        OnPrimaryContainer: 0x003070 / 0xcfe3ff,
271        SecondaryContainer: 0xe2e0ff / 0x262663,
272        OnSecondaryContainer: 0x2a1b70 / 0xdedcff,
273        TertiaryContainer: 0xf3ddfb / 0x3f1a52,
274        OnTertiaryContainer: 0x3d0b52 / 0xf1d9fa,
275        // Code colours, after Xcode's Default (Light) and Default (Dark). UNVERIFIED: Xcode's
276        // themes are not published as files, so these were written from the shipped editor
277        // rather than read from a source, and need checking against Xcode itself. Xcode has no
278        // diff palette of its own, so the diff inks are the platform's accessible green and red
279        // and the line and word backgrounds are those inks laid over the panel. Where a value
280        // had to be moved in tone to keep 4.5:1 on this system's panel, the original is noted.
281        SyntaxKeyword: 0x9b2393 / 0xfc5fa3,
282        SyntaxString: 0xc41a16 / 0xfc6a5d,
283        // SyntaxComment is 0x5d6c79 / 0x6c7986 in the editor.
284        SyntaxComment: 0x5d6c79 / 0x798693,
285        SyntaxNumber: 0x1c00cf / 0xd0bf69,
286        SyntaxConstant: 0x6c36a9 / 0xa167e6,
287        SyntaxType: 0x3900a0 / 0xd0a8ff,
288        SyntaxFunction: 0x326d74 / 0x67b7a4,
289        SyntaxVariable: 0x000000 / 0xdfdfe0,
290        SyntaxProperty: 0x326d74 / 0x67b7a4,
291        SyntaxOperator: 0x000000 / 0xdfdfe0,
292        SyntaxPunctuation: 0x000000 / 0xdfdfe0,
293        SyntaxTag: 0x0b4f79 / 0x5dd8ff,
294        SyntaxAttribute: 0x815f03 / 0xbf8555,
295        SyntaxEscape: 0x1c00cf / 0xd0bf69,
296        SyntaxMacro: 0x643820 / 0xfd8f3f,
297        // DiffAdded is 0x248a3d / 0x30d158 in the editor.
298        DiffAdded: 0x087a2f / 0x30d158,
299        // DiffRemoved is 0xd70015 / 0xff6961 in the editor.
300        DiffRemoved: 0xce0014 / 0xff6e66,
301        DiffModified: 0x007aff / 0x0a84ff,
302        DiffAddedContainer: 0xe1efe6 / 0x20402a,
303        DiffRemovedContainer: 0xf9e0e3 / 0x492c2c,
304        DiffAddedEmphasis: 0xb5d7c1 / 0x246435,
305        DiffRemovedEmphasis: 0xf0b3b9 / 0x773d3b,
306    },
307    // The large title is bold. Both reference screens set it that way, "Contacts" over a
308    // grouped list and "Cupertino" over a search field, and a large title at book weight
309    // is the one thing that stops an iOS screen reading as an iOS screen.
310    type_scale: type_scale! {
311        Display: 34.0 / 700 / 41.0 / 0.37 / false,
312        Headline: 28.0 / 400 / 34.0 / 0.36 / false,
313        Title: 22.0 / 400 / 28.0 / 0.35 / false,
314        Subtitle: 17.0 / 600 / 22.0 / -0.41 / false,
315        Body: 17.0 / 400 / 22.0 / -0.41 / false,
316        BodyStrong: 17.0 / 600 / 22.0 / -0.41 / false,
317        Label: 15.0 / 400 / 20.0 / -0.24 / false,
318        Caption: 12.0 / 400 / 16.0 / 0.0 / false,
319        Mono: 15.0 / 400 / 20.0 / 0.0 / true,
320    },
321    // HIG corners are continuous curvature. The radius is the same number, but the
322    // Renderer is expected to draw it with a squircle rather than a circular arc.
323    shapes: shapes! {
324        None: 0.0,
325        ExtraSmall: 4.0,
326        Small: 8.0,
327        Medium: 10.0,
328        Large: 14.0,
329        Full: 1000.0,
330    },
331    spaces: spaces! {
332        None: 0.0,
333        Xs: 4.0,
334        Sm: 8.0,
335        Md: 16.0,
336        Lg: 20.0,
337        Xl: 32.0,
338        Xxl: 44.0,
339    },
340};
341
342const FLUENT: DesignTokenTable = DesignTokenTable {
343    system: DesignSystem::Fluent,
344    reference: "WinUI / Fluent 2 web and Windows tokens, fluent2.microsoft.design, 2024",
345    default_family: "Segoe UI Variable",
346    monospace_family: "Cascadia Mono",
347    colors: colors! {
348        Primary: 0x0f6cbd / 0x479ef5,
349        OnPrimary: 0xffffff / 0x000000,
350        Secondary: 0xebf3fc / 0x0c3b5e,
351        OnSecondary: 0x0f548c / 0xffffff,
352        Surface: 0xffffff / 0x292929,
353        OnSurface: 0x242424 / 0xffffff,
354        // Fluent layers by lightness rather than by outline, and the page it layers on is
355        // the solid background base. The old trio sat within five parts of each other, so
356        // the layering was there in the numbers and not on the screen.
357        SurfaceVariant: 0xeaeaea / 0x333333,
358        OnSurfaceVariant: 0x424242 / 0xd6d6d6,
359        Background: 0xf3f3f3 / 0x1f1f1f,
360        OnBackground: 0x242424 / 0xffffff,
361        Outline: 0xd1d1d1 / 0x666666,
362        OutlineVariant: 0xe0e0e0 / 0x3d3d3d,
363        Error: 0xc50f1f / 0xdc626d,
364        OnError: 0xffffff / 0x000000,
365        // The card layer, which Fluent lifts off the solid background base by lightness.
366        SurfaceContainer: 0xffffff / 0x2b2b2b,
367        // The shared purple ramp, which is Fluent's accent beside the brand blue.
368        Tertiary: 0x8764b8 / 0xb18cd9,
369        OnTertiary: 0xffffff / 0x22103a,
370        // Fluent's brand tints, which are how it fills a selected or highlighted region:
371        // two steps of the brand ramp in light, and the dark shades of it in dark. The
372        // secondary tint is a step deeper than the primary one rather than a different
373        // hue, because Fluent's second accent is the same blue used more strongly.
374        PrimaryContainer: 0xcfe4fa / 0x0c3b5e,
375        OnPrimaryContainer: 0x0c3b5e / 0xcfe4fa,
376        SecondaryContainer: 0xb4d6fa / 0x123d61,
377        OnSecondaryContainer: 0x0b3350 / 0xb4d6fa,
378        TertiaryContainer: 0xe8dcf7 / 0x3b2159,
379        OnTertiaryContainer: 0x341a5e / 0xe8dcf7,
380        // Code colours, read from VS Code's Light+ and Dark+ in microsoft/vscode:
381        // extensions/theme-defaults/themes/light_vs.json (f93fdeb4), light_plus.json (09fe3fcb),
382        // dark_vs.json (f93fdeb4), dark_plus.json (e061feef). Punctuation is the editor
383        // foreground and macros take keyword.control, which is what a preprocessor directive is
384        // scoped as. The diff backgrounds are VS Code's own defaults from
385        // src/vs/platform/theme/common/colors/editorColors.ts (463b5e85): lines at
386        // rgba(155,185,85,.2) and rgba(255,0,0,.2), words at #9ccc2c40/33 and #ff000033 over
387        // them, resolved over this system's panel. Where a value had to be moved in tone to keep
388        // 4.5:1 on this system's panel, the editor's own value is noted.
389        SyntaxKeyword: 0x0000ff / 0x569cd6,
390        SyntaxString: 0xa31515 / 0xce9178,
391        // SyntaxComment is 0x008000 / 0x6a9955 in the editor.
392        SyntaxComment: 0x008000 / 0x6f9e5a,
393        SyntaxNumber: 0x098658 / 0xb5cea8,
394        SyntaxConstant: 0x0070c1 / 0x4fc1ff,
395        SyntaxType: 0x267f99 / 0x4ec9b0,
396        SyntaxFunction: 0x795e26 / 0xdcdcaa,
397        SyntaxVariable: 0x001080 / 0x9cdcfe,
398        SyntaxProperty: 0x001080 / 0x9cdcfe,
399        SyntaxOperator: 0x000000 / 0xd4d4d4,
400        SyntaxPunctuation: 0x000000 / 0xd4d4d4,
401        SyntaxTag: 0x800000 / 0x569cd6,
402        SyntaxAttribute: 0xe50000 / 0x9cdcfe,
403        SyntaxEscape: 0xee0000 / 0xd7ba7d,
404        SyntaxMacro: 0xaf00db / 0xc586c0,
405        // DiffAdded is 0x098658 / 0xb5cea8 in the editor.
406        DiffAdded: 0x007b50 / 0xb5cea8,
407        DiffRemoved: 0xa31515 / 0xce9178,
408        DiffModified: 0x0451a5 / 0x569cd6,
409        DiffAddedContainer: 0xebf1dd / 0x414733,
410        DiffRemovedContainer: 0xffcccc / 0x552222,
411        DiffAddedEmphasis: 0xd7e8b1 / 0x536232,
412        DiffRemovedEmphasis: 0xff9999 / 0x771b1b,
413    },
414    type_scale: type_scale! {
415        Display: 40.0 / 600 / 52.0 / 0.0 / false,
416        Headline: 28.0 / 600 / 36.0 / 0.0 / false,
417        Title: 20.0 / 600 / 28.0 / 0.0 / false,
418        Subtitle: 16.0 / 600 / 22.0 / 0.0 / false,
419        Body: 14.0 / 400 / 20.0 / 0.0 / false,
420        BodyStrong: 14.0 / 600 / 20.0 / 0.0 / false,
421        Label: 12.0 / 400 / 16.0 / 0.0 / false,
422        Caption: 12.0 / 400 / 16.0 / 0.0 / false,
423        Mono: 13.0 / 400 / 18.0 / 0.0 / true,
424    },
425    shapes: shapes! {
426        None: 0.0,
427        ExtraSmall: 2.0,
428        Small: 3.0,
429        Medium: 4.0,
430        Large: 8.0,
431        Full: 1000.0,
432    },
433    spaces: spaces! {
434        None: 0.0,
435        Xs: 2.0,
436        Sm: 4.0,
437        Md: 8.0,
438        Lg: 12.0,
439        Xl: 20.0,
440        Xxl: 32.0,
441    },
442};
443
444const ADWAITA: DesignTokenTable = DesignTokenTable {
445    system: DesignSystem::Gnome,
446    reference: "GNOME Human Interface Guidelines and the libadwaita named colours, GNOME 50",
447    default_family: "Cantarell",
448    monospace_family: "Source Code Pro",
449    colors: colors! {
450        // accent_bg_color, blue 3 in the GNOME palette.
451        Primary: 0x3584e4 / 0x3584e4,
452        OnPrimary: 0xffffff / 0xffffff,
453        // purple 3, the rare second accent. Never a second button colour.
454        Secondary: 0x9141ac / 0xc061cb,
455        // White in both schemes, which is what libadwaita puts on every accent fill but
456        // yellow. On the lighter purple 2 of the dark scheme that measures 3.6:1, which
457        // carries a short control label but would not carry body text. Nothing puts body
458        // text on the second accent.
459        OnSecondary: 0xffffff / 0xffffff,
460        // view_bg_color: the white of a list or a text view.
461        Surface: 0xffffff / 0x1e1e1e,
462        OnSurface: 0x2e3436 / 0xffffff,
463        // headerbar_bg_color: the slightly darker grey of chrome.
464        SurfaceVariant: 0xebebeb / 0x303030,
465        OnSurfaceVariant: 0x5e5c64 / 0xc0bfbc,
466        // window_bg_color.
467        Background: 0xfafafa / 0x242424,
468        OnBackground: 0x2e3436 / 0xffffff,
469        Outline: 0xcdc7c2 / 0x52514f,
470        OutlineVariant: 0xe6e3e1 / 0x3a3a3a,
471        // red 3 in light. Dark takes red 1, because the darker destructive red loses too
472        // much contrast against a dark window.
473        Error: 0xe01b24 / 0xff7b63,
474        OnError: 0xffffff / 0x2a0a06,
475        // An Adwaita card in light is white with a hairline around it, and white against
476        // window_bg_color is five parts of grey: the border is what you actually see. A
477        // role that has to be visible on the page by itself cannot be that, so the layer
478        // is sidebar_bg_color, the grey Adwaita already uses for a panel beside the view.
479        SurfaceContainer: 0xebebeb / 0x303030,
480        // teal 4, the third of the GNOME accent colours after blue and purple. Like the
481        // other two it holds one value across both schemes, which is how libadwaita ships
482        // its accents.
483        Tertiary: 0x2190a4 / 0x2190a4,
484        OnTertiary: 0xffffff / 0xffffff,
485        // Adwaita has no container tones of its own: it tints by drawing the accent at a
486        // low alpha over whatever is behind. That cannot be a role, because a role answers
487        // before anything knows what is behind it, so these are that same tint resolved
488        // against the window colour, one per accent.
489        PrimaryContainer: 0xd4e5fb / 0x1b3c5e,
490        OnPrimaryContainer: 0x0d3b70 / 0xcfe0f7,
491        SecondaryContainer: 0xecd9f1 / 0x44234c,
492        OnSecondaryContainer: 0x45164f / 0xecd9f1,
493        TertiaryContainer: 0xcfe9ed / 0x134249,
494        OnTertiaryContainer: 0x0a3d45 / 0xcfe9ed,
495        // Code colours, read from GtkSourceView's styles in GNOME/gtksourceview:
496        // data/styles/Adwaita.xml (f9e0f254) and Adwaita-dark.xml (c4604e35). Keywords are
497        // def:statement, variables def:identifier, tags and attributes xml:element-name and
498        // xml:attribute-name, escapes def:special-char, macros def:preprocessor, and property,
499        // operator and punctuation the text foreground. The diff inks are diff:added-line,
500        // diff:removed-line and diff:changed-line; the line and word backgrounds are those inks
501        // laid over the panel. Where a value had to be moved in tone to keep 4.5:1 on this
502        // system's grey panel, the scheme's own value is noted.
503        // SyntaxKeyword is 0xc64600 / 0xffa348 in the editor.
504        SyntaxKeyword: 0xba4100 / 0xffa348,
505        // SyntaxString is 0x218787 / 0x5bc8af in the editor.
506        SyntaxString: 0x007778 / 0x5bc8af,
507        // SyntaxComment is 0x77767b / 0x777777 in the editor.
508        SyntaxComment: 0x6a6a6e / 0x999898,
509        // SyntaxNumber is 0x4e57ba / 0x7d8ac7 in the editor.
510        SyntaxNumber: 0x4e57ba / 0x8794d2,
511        // SyntaxConstant is 0x4e57ba / 0x7d8ac7 in the editor.
512        SyntaxConstant: 0x4e57ba / 0x8794d2,
513        // SyntaxType is 0x218787 / 0x5bc8af in the editor.
514        SyntaxType: 0x007778 / 0x5bc8af,
515        // SyntaxFunction is 0x1c71d8 / 0x62a0ea in the editor.
516        SyntaxFunction: 0x0067cd / 0x62a0ea,
517        // SyntaxVariable is 0x4e9a06 / 0x4e9a06 in the editor.
518        SyntaxVariable: 0x3b7800 / 0x5dab1e,
519        SyntaxProperty: 0x504e55 / 0xc0bfbc,
520        SyntaxOperator: 0x504e55 / 0xc0bfbc,
521        SyntaxPunctuation: 0x504e55 / 0xc0bfbc,
522        // SyntaxTag is 0x218787 / 0x33b2a4 in the editor.
523        SyntaxTag: 0x007778 / 0x33b2a4,
524        // SyntaxAttribute is 0xc64600 / 0xff7800 in the editor.
525        SyntaxAttribute: 0xba4100 / 0xff7800,
526        // SyntaxEscape is 0xed333b / 0xf66151 in the editor.
527        SyntaxEscape: 0xd01a2a / 0xfd6656,
528        // SyntaxMacro is 0xc64600 / 0xe66100 in the editor.
529        SyntaxMacro: 0xba4100 / 0xfb7018,
530        // DiffAdded is 0x26a1a2 / 0x33b2a4 in the editor.
531        DiffAdded: 0x006a6b / 0x56cebf,
532        // DiffRemoved is 0xf66151 / 0xf66151 in the editor.
533        DiffRemoved: 0xae2d23 / 0xffa295,
534        DiffModified: 0xe66100 / 0xff7800,
535        DiffAddedContainer: 0xcfdcdc / 0x38504d,
536        DiffRemovedContainer: 0xe4d4d3 / 0x594744,
537        DiffAddedEmphasis: 0xa5c4c5 / 0x3f6f69,
538        DiffRemovedEmphasis: 0xd9b2af / 0x835e58,
539    },
540    // libadwaita declares its title classes in points against an 11pt Cantarell body, and
541    // they are heavy: the largest title is weight 800, not 700. Those point values are
542    // carried over to sp here, which is why the body is 15 and not the 14 a Material scale
543    // would use.
544    type_scale: type_scale! {
545        Display: 44.0 / 800 / 52.0 / -0.5 / false,
546        Headline: 32.0 / 800 / 40.0 / -0.25 / false,
547        Title: 24.0 / 700 / 32.0 / 0.0 / false,
548        Subtitle: 20.0 / 700 / 28.0 / 0.0 / false,
549        Body: 15.0 / 400 / 22.0 / 0.0 / false,
550        BodyStrong: 15.0 / 700 / 22.0 / 0.0 / false,
551        Label: 13.0 / 700 / 18.0 / 0.1 / false,
552        Caption: 12.0 / 400 / 16.0 / 0.0 / false,
553        Mono: 14.0 / 400 / 20.0 / 0.0 / true,
554    },
555    // libadwaita's own named radii: six on a button or an entry, twelve on a card or a
556    // popover, fifteen on a window. The middle and top rungs were eight and twelve, which
557    // put a card where a button belongs and a window where a card belongs, and the GNOME
558    // 50 screen in docs/references/design-systems/gnome50/ shows the difference plainly:
559    // the preferences groups are rounded well past the buttons inside them, and the
560    // window is rounder again. Pills are kept for suggested actions and search entries.
561    shapes: shapes! {
562        None: 0.0,
563        ExtraSmall: 4.0,
564        Small: 6.0,
565        Medium: 12.0,
566        Large: 15.0,
567        Full: 1000.0,
568    },
569    // GNOME lays out on a six pixel grid, and its dialogs are roomy.
570    spaces: spaces! {
571        None: 0.0,
572        Xs: 3.0,
573        Sm: 6.0,
574        Md: 12.0,
575        Lg: 18.0,
576        Xl: 24.0,
577        Xxl: 36.0,
578    },
579};
580
581const BREEZE: DesignTokenTable = DesignTokenTable {
582    system: DesignSystem::Breeze,
583    reference: "KDE Breeze colour schemes and the KDE Human Interface Guidelines, Plasma 6",
584    default_family: "Noto Sans",
585    monospace_family: "Hack",
586    colors: colors! {
587        // Plasma blue, the default highlight in both schemes.
588        Primary: 0x3daee9 / 0x3daee9,
589        // Breeze puts white on the highlight, which measures under three to one against
590        // this blue. Dark ink keeps the same blue and stays readable at small sizes.
591        OnPrimary: 0x06222e / 0x06222e,
592        // The Breeze "positive" teal, its second accent. Dark ink again, for the same
593        // reason: white on this green is about two to one.
594        Secondary: 0x16a085 / 0x1abc9c,
595        OnSecondary: 0x03201b / 0x03201b,
596        // View background: the white of a list or an entry.
597        Surface: 0xfcfcfc / 0x1b1e20,
598        OnSurface: 0x232629 / 0xfcfcfc,
599        // A step deeper than the window, in Breeze's own grey.
600        SurfaceVariant: 0xe1e3e5 / 0x31363b,
601        OnSurfaceVariant: 0x4d5052 / 0xbdc3c7,
602        // Window background, a touch cooler and darker than the view.
603        Background: 0xeff0f1 / 0x232629,
604        OnBackground: 0x232629 / 0xfcfcfc,
605        Outline: 0xbdc3c7 / 0x4d5155,
606        OutlineVariant: 0xd8dbdd / 0x3f4449,
607        // Breeze "negative".
608        Error: 0xda4453 / 0xed8079,
609        OnError: 0xffffff / 0x2a0806,
610        // A framed panel in Plasma is the view colour sitting on the window: a step
611        // lighter than the window in light, and a step darker in dark, because Breeze
612        // Dark's view really is darker than its window. Either way the panel separates
613        // from the page, which is the whole promise of the role.
614        SurfaceContainer: 0xfcfcfc / 0x1b1e20,
615        // Breeze "neutral", the amber it uses for a warning state, taken here as the third
616        // accent. Dark ink on it for the same reason the other two carry dark ink: white
617        // on any of these Breeze fills measures under three to one.
618        Tertiary: 0xf67400 / 0xf8a44c,
619        OnTertiary: 0x2b1200 / 0x2b1200,
620        // Plasma tints a selected region with the highlight colour at low alpha over the
621        // view. Resolved against the window colour, once per accent, so the role can answer
622        // without knowing what it is over.
623        PrimaryContainer: 0xd3ecf9 / 0x123b4f,
624        OnPrimaryContainer: 0x0b3b52 / 0xcde6f5,
625        SecondaryContainer: 0xd2ece5 / 0x103a32,
626        OnSecondaryContainer: 0x083b31 / 0xcfe8e0,
627        TertiaryContainer: 0xfae0c4 / 0x4a3113,
628        OnTertiaryContainer: 0x4a2c00 / 0xf8dfc3,
629        // Code colours, read from KSyntaxHighlighting in KDE/syntax-highlighting:
630        // data/themes/breeze-light.theme (b5576b24) and breeze-dark.theme (9d1d9a4e). Keywords
631        // are the normal text colour there, set bold by the editor. The diff inks follow
632        // data/syntax/diff.xml (f8b28165), which draws an added line as Variable and a removed
633        // line as String, and the modified ink is the theme's ModifiedLines. The backgrounds are
634        // those inks laid over the panel. Where a value had to be moved in tone to keep 4.5:1,
635        // the scheme's own value is noted.
636        SyntaxKeyword: 0x1f1c1b / 0xcfcfc2,
637        SyntaxString: 0xbf0303 / 0xf44f4f,
638        // SyntaxComment is 0x898887 / 0x7a7c7d in the editor.
639        SyntaxComment: 0x747473 / 0x848687,
640        // SyntaxNumber is 0xb08000 / 0xf67400 in the editor.
641        SyntaxNumber: 0x966d00 / 0xf67400,
642        SyntaxConstant: 0xaa5500 / 0x27aeae,
643        // SyntaxType is 0x0057ae / 0x2980b9 in the editor.
644        SyntaxType: 0x0057ae / 0x3b8dc7,
645        // SyntaxFunction is 0x644a9b / 0x8e44ad in the editor.
646        SyntaxFunction: 0x644a9b / 0xb266d1,
647        SyntaxVariable: 0x0057ae / 0x27aeae,
648        SyntaxProperty: 0x0057ae / 0x27aeae,
649        // SyntaxOperator is 0xca60ca / 0x3f8058 in the editor.
650        SyntaxOperator: 0xaf48b1 / 0x519269,
651        SyntaxPunctuation: 0x1f1c1b / 0xcfcfc2,
652        SyntaxTag: 0x1f1c1b / 0xcfcfc2,
653        // SyntaxAttribute is 0x0057ae / 0x2980b9 in the editor.
654        SyntaxAttribute: 0x0057ae / 0x3b8dc7,
655        // SyntaxEscape is 0x3daee9 / 0x3daee9 in the editor.
656        SyntaxEscape: 0x007bac / 0x3daee9,
657        SyntaxMacro: 0x006e28 / 0x27ae60,
658        // DiffAdded is 0x0057ae / 0x27aeae in the editor.
659        DiffAdded: 0x0057ae / 0x2cb1b1,
660        // DiffRemoved is 0xbf0303 / 0xf44f4f in the editor.
661        DiffRemoved: 0xbf0303 / 0xff736e,
662        DiffModified: 0xfdbc4b / 0xc04900,
663        DiffAddedContainer: 0xdee8f3 / 0x1e3b3d,
664        DiffRemovedContainer: 0xf5dede / 0x492f30,
665        DiffAddedEmphasis: 0xb0cae5 / 0x22595a,
666        DiffRemovedEmphasis: 0xeab1b1 / 0x76403f,
667    },
668    // Plasma sets its interface in Noto Sans at 10pt, a step smaller than Adwaita's 11pt
669    // Cantarell, and its headings are semi bold rather than the near black weights
670    // libadwaita uses. Smaller and lighter at every rung, with tighter line heights to
671    // match the denser spacing.
672    type_scale: type_scale! {
673        Display: 34.0 / 600 / 40.0 / 0.0 / false,
674        Headline: 26.0 / 600 / 32.0 / 0.0 / false,
675        Title: 19.0 / 600 / 24.0 / 0.0 / false,
676        Subtitle: 16.0 / 500 / 21.0 / 0.0 / false,
677        Body: 13.0 / 400 / 19.0 / 0.0 / false,
678        BodyStrong: 13.0 / 600 / 19.0 / 0.0 / false,
679        Label: 12.0 / 500 / 16.0 / 0.2 / false,
680        Caption: 11.0 / 400 / 15.0 / 0.0 / false,
681        Mono: 12.0 / 400 / 17.0 / 0.0 / true,
682    },
683    // Breeze barely rounds. Frames and buttons are a two to four pixel radius and the
684    // largest containers stop at six, which is where the language gets its drafted look.
685    shapes: shapes! {
686        None: 0.0,
687        ExtraSmall: 2.0,
688        Small: 3.0,
689        Medium: 4.0,
690        Large: 6.0,
691        Full: 1000.0,
692    },
693    // Kirigami's ladder: small spacing 4, large spacing 8, grid unit 18.
694    spaces: spaces! {
695        None: 0.0,
696        Xs: 2.0,
697        Sm: 4.0,
698        Md: 8.0,
699        Lg: 12.0,
700        Xl: 18.0,
701        Xxl: 24.0,
702    },
703};
704
705const DEEPIN: DesignTokenTable = DesignTokenTable {
706    system: DesignSystem::Deepin,
707    reference: "Deepin Design specification and the DTK control defaults, deepin 23",
708    default_family: "Noto Sans",
709    monospace_family: "Noto Sans Mono",
710    // Neutral greys, not warm ones. The palette here was written from memory once and
711    // described itself as warm, with a trace of brown through every neutral. Deepin's own
712    // screens are not: its system monitor is a white list on a light grey window with a
713    // plain grey search field, and its calculator is a near black window of dark grey
714    // keys. Nothing in either is warm, and the warmth was the most visible thing about
715    // this system, so it was the most visible thing that was wrong.
716    colors: colors! {
717        // The brand blue.
718        Primary: 0x0081ff / 0x3ba2ff,
719        OnPrimary: 0xffffff / 0x04203a,
720        // The amber that sits beside the blue in Deepin's own readouts, where a second
721        // series is drawn in it.
722        Secondary: 0xf2a13c / 0xffb964,
723        OnSecondary: 0x2b1a05 / 0x33200a,
724        // The reading surface: a white list in light, a dark grey key in dark.
725        Surface: 0xffffff / 0x2a2a2a,
726        OnSurface: 0x202020 / 0xf0f0f0,
727        // The fill of a search field, and of the alternate row in a list.
728        SurfaceVariant: 0xe6e6e6 / 0x3a3a3a,
729        OnSurfaceVariant: 0x5a5a5a / 0xb4b4b4,
730        // The window, which is the reading surface itself in light: a Deepin window is
731        // white and what separates from it is the grey well beside the content, not a
732        // white card on a grey page.
733        Background: 0xffffff / 0x1a1a1a,
734        OnBackground: 0x202020 / 0xf0f0f0,
735        Outline: 0xcdcdcd / 0x4d4d4d,
736        OutlineVariant: 0xe0e0e0 / 0x333333,
737        Error: 0xff5736 / 0xff8a73,
738        OnError: 0xffffff / 0x34110a,
739        // The panel: the grey of a sidebar sunk into the white window, and in dark the
740        // key grey, which is a step lighter than the near black window. Either way the
741        // panel separates from the page, which is the promise of the role.
742        SurfaceContainer: 0xf1f1f1 / 0x2a2a2a,
743        // The violet of the deepin palette, the one accent here that is neither the brand
744        // blue nor a warm colour.
745        Tertiary: 0x7a5bd6 / 0x9f8ae3,
746        OnTertiary: 0xffffff / 0x1d0f45,
747        // Tints of the three accents, resolved neutrally against the page colour.
748        PrimaryContainer: 0xd8ecff / 0x203547,
749        OnPrimaryContainer: 0x00407f / 0x9dd0ff,
750        SecondaryContainer: 0xfdf0e1 / 0x473928,
751        OnSecondaryContainer: 0x79501e / 0xffdcb1,
752        TertiaryContainer: 0xebe6f8 / 0x343042,
753        OnTertiaryContainer: 0x3d2d6b / 0xcfc4f1,
754        // Code colours. Deepin publishes no code editor scheme, so these are derived from the
755        // base roles above by the same rule Material 3's are. The brand blue, the amber and the
756        // error orange do not reach 4.5:1 on the grey panel as they are, so the rule moves them
757        // darker in tone, keeping their hue and chroma.
758        SyntaxKeyword: 0x006bd5 / 0x3ba2ff,
759        SyntaxString: 0x7758d3 / 0x9f8ae3,
760        SyntaxComment: 0x5a5a5a / 0xb4b4b4,
761        SyntaxNumber: 0x3d2d6b / 0xcfc4f1,
762        SyntaxConstant: 0x3d2d6b / 0xcfc4f1,
763        SyntaxType: 0x9b5e00 / 0xffb964,
764        SyntaxFunction: 0x00407f / 0x9dd0ff,
765        SyntaxVariable: 0x202020 / 0xf0f0f0,
766        SyntaxProperty: 0x79501e / 0xffdcb1,
767        SyntaxOperator: 0x5a5a5a / 0xb4b4b4,
768        SyntaxPunctuation: 0x5a5a5a / 0xb4b4b4,
769        SyntaxTag: 0x006bd5 / 0x3ba2ff,
770        SyntaxAttribute: 0x9b5e00 / 0xffb964,
771        SyntaxEscape: 0xcd3416 / 0xff8a73,
772        SyntaxMacro: 0x9b5e00 / 0xffb964,
773        DiffAdded: 0x007118 / 0x76c270,
774        DiffRemoved: 0xbb2608 / 0xff9783,
775        DiffModified: 0x7a5bd6 / 0x9f8ae3,
776        DiffAddedContainer: 0xd4e2d7 / 0x394838,
777        DiffRemovedContainer: 0xebd9d5 / 0x55403c,
778        DiffAddedEmphasis: 0xa9cbb0 / 0x486746,
779        DiffRemovedEmphasis: 0xe1b4ab / 0x7f564e,
780    },
781    // A middle weight ladder. The body sits between Breeze's 13 and Adwaita's 15, and the
782    // headings are semi bold with loose line heights, which suits the rounded shapes and
783    // survives on a desktop whose default font is unknown.
784    type_scale: type_scale! {
785        Display: 40.0 / 600 / 50.0 / 0.0 / false,
786        Headline: 30.0 / 600 / 38.0 / 0.0 / false,
787        Title: 22.0 / 500 / 30.0 / 0.0 / false,
788        Subtitle: 18.0 / 500 / 26.0 / 0.0 / false,
789        Body: 14.0 / 400 / 21.0 / 0.0 / false,
790        BodyStrong: 14.0 / 600 / 21.0 / 0.0 / false,
791        Label: 13.0 / 500 / 18.0 / 0.3 / false,
792        Caption: 12.0 / 400 / 17.0 / 0.0 / false,
793        Mono: 13.0 / 400 / 19.0 / 0.0 / true,
794    },
795    // The roundest of the three Linux systems. Deepin's windows are rounded far past
796    // anything GTK or Qt does, and its controls follow at a smaller radius, so even a
797    // button reads as a lozenge next to a Breeze rectangle. The middle rung is ten rather
798    // than twelve: the keys in the calculator screen are cut at about a sixth of their
799    // height, which is where a Deepin button sits, and twelve was Adwaita's card radius
800    // borrowed for a button.
801    shapes: shapes! {
802        None: 0.0,
803        ExtraSmall: 6.0,
804        Small: 8.0,
805        Medium: 10.0,
806        Large: 18.0,
807        Full: 1000.0,
808    },
809    // Roomy, and on a ten pixel rhythm rather than GNOME's six or Kirigami's four.
810    spaces: spaces! {
811        None: 0.0,
812        Xs: 4.0,
813        Sm: 10.0,
814        Md: 16.0,
815        Lg: 20.0,
816        Xl: 30.0,
817        Xxl: 40.0,
818    },
819};
820
821/// Apple's current language, the one macOS 26 and iOS 26 draw.
822///
823/// It shares a palette with `APPLE_HIG` above, because the system colours did not change:
824/// what changed is what a surface is made of, how deep a corner is cut, and how far a
825/// control is allowed to float. Two of those three are rules rather than tokens, so the
826/// visible difference in this table is the shape ladder.
827const LIQUID_GLASS: DesignTokenTable = DesignTokenTable {
828    system: DesignSystem::LiquidGlass,
829    reference: "Apple Human Interface Guidelines, Liquid Glass, system colors and Dynamic Type, 2026",
830    default_family: "SF Pro",
831    monospace_family: "SF Mono",
832    colors: colors! {
833        Primary: 0x007aff / 0x0a84ff,
834        OnPrimary: 0xffffff / 0xffffff,
835        Secondary: 0x5856d6 / 0x5e5ce6,
836        OnSecondary: 0xffffff / 0xffffff,
837        Surface: 0xffffff / 0x1c1c1e,
838        OnSurface: 0x000000 / 0xffffff,
839        // The secondary fill, which is what a glass surface tints towards when the blur
840        // is unavailable and what a grouped row sits on.
841        SurfaceVariant: 0xe9e9eb / 0x2c2c2e,
842        OnSurfaceVariant: 0x3c3c43 / 0xebebf5,
843        // The page is the document, not a grey well for it to sit in. That is the sharpest
844        // difference from the flat language beside it, where a grouped page is grey and
845        // the white rectangles on it are what a reader looks at: here the separation
846        // comes from the material a panel is made of, so the page underneath can be the
847        // reading surface itself.
848        //
849        // The dark page is pure black, which is right on a phone and wrong in a desktop
850        // window. A window is the other value Apple specifies for this one role, and the
851        // component rules choose between the two by window size. No other role is
852        // specified twice, so no other role is chosen anywhere but here.
853        Background: 0xffffff / 0x000000,
854        OnBackground: 0x000000 / 0xffffff,
855        Outline: 0xc6c6c8 / 0x38383a,
856        OutlineVariant: 0xe5e5ea / 0x48484a,
857        Error: 0xff3b30 / 0xff453a,
858        OnError: 0xffffff / 0xffffff,
859        // A panel has to lift off a page that is already white, so this is the lightest
860        // system grey rather than another white.
861        SurfaceContainer: 0xf2f2f7 / 0x1c1c1e,
862        // Liquid Glass keeps Cupertino's accents, because it is the same platform
863        // palette seen through a different material. What changes is how a surface is
864        // drawn, not which purple Apple uses.
865        Tertiary: 0xaf52de / 0xbf5af2,
866        OnTertiary: 0xffffff / 0xffffff,
867        PrimaryContainer: 0xd6e4ff / 0x0a2d52,
868        OnPrimaryContainer: 0x003070 / 0xcfe3ff,
869        SecondaryContainer: 0xe2e0ff / 0x262663,
870        OnSecondaryContainer: 0x2a1b70 / 0xdedcff,
871        TertiaryContainer: 0xf3ddfb / 0x3f1a52,
872        OnTertiaryContainer: 0x3d0b52 / 0xf1d9fa,
873        // Code colours, after Xcode as Cupertino's are, and UNVERIFIED for the same reason. The
874        // panel is a step greyer here, so the diff inks are moved further in tone and the
875        // backgrounds resolve differently.
876        SyntaxKeyword: 0x9b2393 / 0xfc5fa3,
877        SyntaxString: 0xc41a16 / 0xfc6a5d,
878        // SyntaxComment is 0x5d6c79 / 0x6c7986 in the editor.
879        SyntaxComment: 0x5d6c79 / 0x798693,
880        SyntaxNumber: 0x1c00cf / 0xd0bf69,
881        SyntaxConstant: 0x6c36a9 / 0xa167e6,
882        SyntaxType: 0x3900a0 / 0xd0a8ff,
883        SyntaxFunction: 0x326d74 / 0x67b7a4,
884        SyntaxVariable: 0x000000 / 0xdfdfe0,
885        SyntaxProperty: 0x326d74 / 0x67b7a4,
886        SyntaxOperator: 0x000000 / 0xdfdfe0,
887        SyntaxPunctuation: 0x000000 / 0xdfdfe0,
888        SyntaxTag: 0x0b4f79 / 0x5dd8ff,
889        SyntaxAttribute: 0x815f03 / 0xbf8555,
890        SyntaxEscape: 0x1c00cf / 0xd0bf69,
891        SyntaxMacro: 0x643820 / 0xfd8f3f,
892        // DiffAdded is 0x248a3d / 0x30d158 in the editor.
893        DiffAdded: 0x00722a / 0x30d158,
894        // DiffRemoved is 0xd70015 / 0xff6961 in the editor.
895        DiffRemoved: 0xc40012 / 0xff6e66,
896        DiffModified: 0x007aff / 0x0a84ff,
897        DiffAddedContainer: 0xd5e3de / 0x20402a,
898        DiffRemovedContainer: 0xecd5dc / 0x492c2c,
899        DiffAddedEmphasis: 0xa9ccb9 / 0x246435,
900        DiffRemovedEmphasis: 0xe4a9b2 / 0x773d3b,
901    },
902    // Same sizes as the flat language, because Dynamic Type did not move, and heavier at
903    // every rung that labels something. A label sitting on a translucent surface competes
904    // with whatever shows through it, and every control label in the reference screens is
905    // set semibold for that reason: the buttons in an alert, the segments of a picker, the
906    // section heads of a formatting sheet.
907    type_scale: type_scale! {
908        Display: 34.0 / 700 / 41.0 / 0.37 / false,
909        Headline: 28.0 / 700 / 34.0 / 0.36 / false,
910        Title: 22.0 / 700 / 28.0 / 0.35 / false,
911        Subtitle: 17.0 / 600 / 22.0 / -0.41 / false,
912        Body: 17.0 / 400 / 22.0 / -0.41 / false,
913        BodyStrong: 17.0 / 600 / 22.0 / -0.41 / false,
914        Label: 15.0 / 600 / 20.0 / -0.24 / false,
915        Caption: 12.0 / 500 / 16.0 / 0.0 / false,
916        Mono: 15.0 / 400 / 20.0 / 0.0 / true,
917    },
918    // Deeper than the flat language, and continuous rather than circular. A surface that
919    // catches light along its rim needs a corner long enough for the rim to travel round
920    // it; a ten dp arc pinches that highlight into a point. The Renderer draws these as a
921    // superellipse, which is the other half of the same decision.
922    shapes: shapes! {
923        None: 0.0,
924        ExtraSmall: 6.0,
925        Small: 10.0,
926        Medium: 16.0,
927        Large: 22.0,
928        Full: 1000.0,
929    },
930    // Roomier than the flat language at every step above the smallest. Two reasons, both
931    // visible in the reference: a twenty two dp corner needs more room inside it before
932    // text stops crowding the curve, and these surfaces float with a margin around them
933    // instead of running to the window edge, so the margin is a spacing step as well.
934    spaces: spaces! {
935        None: 0.0,
936        Xs: 4.0,
937        Sm: 10.0,
938        Md: 18.0,
939        Lg: 24.0,
940        Xl: 36.0,
941        Xxl: 48.0,
942    },
943};
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948    use crate::schema::DESIGN_SYSTEM_SCHEMA;
949
950    fn channel(value: u32) -> f64 {
951        let value = f64::from(value) / 255.0;
952        if value <= 0.040_45 {
953            value / 12.92
954        } else {
955            ((value + 0.055) / 1.055).powf(2.4)
956        }
957    }
958
959    fn luminance(color: Color) -> f64 {
960        let argb = color.to_argb();
961        0.2126 * channel((argb >> 16) & 0xff)
962            + 0.7152 * channel((argb >> 8) & 0xff)
963            + 0.0722 * channel(argb & 0xff)
964    }
965
966    fn contrast(first: Color, second: Color) -> f64 {
967        let (high, low) = {
968            let (a, b) = (luminance(first), luminance(second));
969            if a > b { (a, b) } else { (b, a) }
970        };
971        (high + 0.05) / (low + 0.05)
972    }
973
974    /// Every `DesignSystem` variant has a table, so adding a variant without
975    /// its values fails here rather than at run time.
976    #[test]
977    fn fr14_every_design_system_has_a_token_table() {
978        assert_eq!(DESIGN_TOKENS.len(), DESIGN_SYSTEM_SCHEMA.len());
979        for (index, variant) in DESIGN_SYSTEM_SCHEMA.iter().enumerate() {
980            let table = DESIGN_TOKENS[index];
981            assert_eq!(table.system as u16, variant.tag);
982            assert!(
983                !table.reference.is_empty(),
984                "{} has no source",
985                variant.name
986            );
987            assert!(!table.default_family.is_empty());
988            assert!(!table.monospace_family.is_empty());
989        }
990    }
991
992    /// The tables are ordered by role tag, so a Renderer can index them directly.
993    #[test]
994    fn fr14_tokens_are_stored_in_role_tag_order() {
995        for table in DESIGN_TOKENS {
996            for (index, token) in table.colors.iter().enumerate() {
997                assert_eq!(token.role as usize, index + 1);
998            }
999            for (index, token) in table.type_scale.iter().enumerate() {
1000                assert_eq!(token.role as usize, index + 1);
1001            }
1002            for (index, token) in table.shapes.iter().enumerate() {
1003                assert_eq!(token.role as usize, index + 1);
1004            }
1005            for (index, token) in table.spaces.iter().enumerate() {
1006                assert_eq!(token.role as usize, index + 1);
1007            }
1008        }
1009    }
1010
1011    /// A filled thing has to be visible on the page it sits on.
1012    ///
1013    /// Cupertino gave `SurfaceVariant` and `Background` the same value, so a chat bubble
1014    /// filled with the variant on a page of the background was drawn, with the right
1015    /// colour, and was invisible. Layering is the entire reason three roles exist.
1016    ///
1017    /// `Background` and `Surface` are deliberately not compared. Material 3 gives them the
1018    /// same value on purpose and expresses depth through tonal containers and elevation,
1019    /// so requiring those to differ would be requiring Material 3 to stop being itself.
1020    ///
1021    /// That is exactly why `SurfaceContainer` is checked here too. Material 3 having no
1022    /// gap between the page and the surface is not a defect, but it does mean `Surface` is
1023    /// not a role a panel can be made of, and until this role existed there was nothing in
1024    /// the vocabulary that promised to be visible on the page while holding `OnSurface`
1025    /// ink. A calculator readout filled with `Surface` disappeared under Material 3 and
1026    /// nothing here noticed, because nothing here was asked about it.
1027    #[test]
1028    fn fr14_surface_variant_is_visible_against_the_page_and_the_surface() {
1029        /// Summed channel distance in eight bit terms, which is enough to catch a collapse.
1030        fn apart(first: Color, second: Color) -> u32 {
1031            (0..3)
1032                .map(|channel| {
1033                    let shift = channel * 8;
1034                    let a = (first.0 >> shift) & 0xff;
1035                    let b = (second.0 >> shift) & 0xff;
1036                    a.abs_diff(b)
1037                })
1038                .sum()
1039        }
1040
1041        for table in DESIGN_TOKENS {
1042            for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1043                let variant = table.color(ColorRole::SurfaceVariant, scheme);
1044                for under in [ColorRole::Background, ColorRole::Surface] {
1045                    let distance = apart(variant, table.color(under, scheme));
1046                    assert!(
1047                        distance >= 24,
1048                        "{:?} {:?}: SurfaceVariant and {:?} are {} apart, which reads as \
1049                         one flat surface. Anything filled with one on a page of the other \
1050                         disappears.",
1051                        table.system,
1052                        scheme,
1053                        under,
1054                        distance
1055                    );
1056                }
1057                // A panel is raised off the page, so the layer role has to be visible
1058                // against the page. It is not compared against `Surface`, because in
1059                // Cupertino a panel on the grouped page is the plain reading surface and
1060                // the two carrying one value there is the correct answer, not a collapse.
1061                let container = table.color(ColorRole::SurfaceContainer, scheme);
1062                let distance = apart(container, table.color(ColorRole::Background, scheme));
1063                assert!(
1064                    distance >= 24,
1065                    "{:?} {:?}: SurfaceContainer and Background are {} apart, so a panel \
1066                     made of it is drawn and cannot be seen. This is the one role a panel \
1067                     can rely on, so it has to lift off the page in every system.",
1068                    table.system,
1069                    scheme,
1070                    distance
1071                );
1072                // The accent containers answer the same question for a tinted panel. A
1073                // tint nobody can see is a panel that is not there, and these are the
1074                // fills a screen made of coloured tiles is built out of.
1075                for container in [
1076                    ColorRole::PrimaryContainer,
1077                    ColorRole::SecondaryContainer,
1078                    ColorRole::TertiaryContainer,
1079                ] {
1080                    let distance = apart(
1081                        table.color(container, scheme),
1082                        table.color(ColorRole::Background, scheme),
1083                    );
1084                    assert!(
1085                        distance >= 24,
1086                        "{:?} {:?}: {:?} and Background are {} apart, so a tinted panel is \
1087                         drawn and cannot be seen.",
1088                        table.system,
1089                        scheme,
1090                        container,
1091                        distance
1092                    );
1093                }
1094            }
1095        }
1096    }
1097
1098    /// The ladder descends from Display to Caption in every system. `Mono` sits
1099    /// outside the ladder, because it is a family choice and not a rung.
1100    #[test]
1101    fn fr13_type_ladder_is_monotonic() {
1102        for table in DESIGN_TOKENS {
1103            let ladder = &table.type_scale[..8];
1104            for pair in ladder.windows(2) {
1105                assert!(
1106                    pair[0].size >= pair[1].size,
1107                    "{:?}: {:?} is not larger than {:?}",
1108                    table.system,
1109                    pair[0].role,
1110                    pair[1].role
1111                );
1112            }
1113            for token in table.type_scale.iter() {
1114                assert!(token.size > 0.0);
1115                assert!((100..=900).contains(&token.weight));
1116                assert!(token.line_height >= token.size);
1117            }
1118            assert!(table.type_scale[TypeRole::Mono as usize - 1].monospace);
1119            assert!(
1120                table.type_scale[TypeRole::BodyStrong as usize - 1].weight
1121                    > table.type_scale[TypeRole::Body as usize - 1].weight
1122            );
1123        }
1124    }
1125
1126    /// Radii and spacing grow with the role, starting at zero.
1127    #[test]
1128    fn fr13_shape_and_space_ladders_grow() {
1129        for table in DESIGN_TOKENS {
1130            assert_eq!(table.shapes[0].radius, 0.0);
1131            for pair in table.shapes.windows(2) {
1132                assert!(pair[1].radius > pair[0].radius, "{:?}", table.system);
1133            }
1134            assert_eq!(table.spaces[0].value, 0.0);
1135            for pair in table.spaces.windows(2) {
1136                assert!(pair[1].value > pair[0].value, "{:?}", table.system);
1137            }
1138        }
1139    }
1140
1141    /// `On*` is readable on its pair in both schemes.
1142    ///
1143    /// Reading surfaces hold WCAG AA body text (4.5:1). Accent fills are held to 3:1,
1144    /// which is the AA large-text and non-text bound: Apple's systemBlue with white
1145    /// sits at about 4.0:1 and its systemRed at about 3.4:1, and those are the shipped
1146    /// platform colours. Tightening past that would mean inventing colours that no
1147    /// platform actually uses, which is worse than the looser bound.
1148    #[test]
1149    fn fr14_on_roles_stay_readable() {
1150        let reading = [
1151            (ColorRole::Surface, ColorRole::OnSurface),
1152            (ColorRole::SurfaceVariant, ColorRole::OnSurfaceVariant),
1153            (ColorRole::Background, ColorRole::OnBackground),
1154            // The layer role has no ink of its own: it holds the page's reading ink, and
1155            // that is the promise a caller relies on when filling a panel with it.
1156            (ColorRole::SurfaceContainer, ColorRole::OnSurface),
1157            // The accent containers exist so that a paragraph can land on a tinted panel,
1158            // not just a word, so they are held to the reading bound rather than to the
1159            // looser bound their accents keep.
1160            (ColorRole::PrimaryContainer, ColorRole::OnPrimaryContainer),
1161            (
1162                ColorRole::SecondaryContainer,
1163                ColorRole::OnSecondaryContainer,
1164            ),
1165            (ColorRole::TertiaryContainer, ColorRole::OnTertiaryContainer),
1166        ];
1167        let accent = [
1168            (ColorRole::Primary, ColorRole::OnPrimary),
1169            (ColorRole::Secondary, ColorRole::OnSecondary),
1170            (ColorRole::Tertiary, ColorRole::OnTertiary),
1171            (ColorRole::Error, ColorRole::OnError),
1172        ];
1173        for table in DESIGN_TOKENS {
1174            for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1175                for (base, on) in reading {
1176                    let ratio = contrast(table.color(base, scheme), table.color(on, scheme));
1177                    assert!(
1178                        ratio >= 4.5,
1179                        "{:?} {scheme:?} {base:?}/{on:?} is {ratio:.2}:1",
1180                        table.system
1181                    );
1182                }
1183                for (base, on) in accent {
1184                    let ratio = contrast(table.color(base, scheme), table.color(on, scheme));
1185                    assert!(
1186                        ratio >= 3.0,
1187                        "{:?} {scheme:?} {base:?}/{on:?} is {ratio:.2}:1",
1188                        table.system
1189                    );
1190                }
1191            }
1192        }
1193    }
1194
1195    /// No two design systems answer a whole ladder with the same values.
1196    ///
1197    /// A design system that agrees with another one on every colour, every type rung,
1198    /// every radius or every spacing step is that other system wearing a different name,
1199    /// and a caller who selected it would see no change at all. Checking each ladder
1200    /// separately is what catches a table that was copied and then edited in one place.
1201    #[test]
1202    fn fr14_no_two_design_systems_answer_a_whole_ladder_identically() {
1203        for (index, first) in DESIGN_TOKENS.iter().enumerate() {
1204            for second in &DESIGN_TOKENS[index + 1..] {
1205                assert_ne!(
1206                    first.colors, second.colors,
1207                    "{:?} and {:?} have the same palette",
1208                    first.system, second.system
1209                );
1210                assert_ne!(
1211                    first.type_scale, second.type_scale,
1212                    "{:?} and {:?} have the same type scale",
1213                    first.system, second.system
1214                );
1215                assert_ne!(
1216                    first.shapes, second.shapes,
1217                    "{:?} and {:?} round everything the same way",
1218                    first.system, second.system
1219                );
1220                assert_ne!(
1221                    first.spaces, second.spaces,
1222                    "{:?} and {:?} have the same spacing ladder",
1223                    first.system, second.system
1224                );
1225            }
1226        }
1227    }
1228
1229    /// Code stays readable on the panel it is drawn on, in every system and both schemes.
1230    ///
1231    /// Every syntax ink, comments included, holds the body-text bound on `SurfaceContainer`,
1232    /// which is what a code block is filled with. Each diff ink holds it on its own line
1233    /// background, and so does the body ink, because an unchanged word on a changed line is
1234    /// still body text. The word background has to be visible on the line background, or
1235    /// the words that changed are not marked at all.
1236    #[test]
1237    fn fr13_1_3_code_colours_hold_their_contrast_in_every_system() {
1238        use crate::contrast::{DIFF_ROLES, MIN_SEPARATION, SYNTAX_ROLES, apart};
1239        for table in DESIGN_TOKENS {
1240            for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1241                let panel = table.color(ColorRole::SurfaceContainer, scheme);
1242                for role in SYNTAX_ROLES {
1243                    let ratio = contrast(table.color(role, scheme), panel);
1244                    assert!(
1245                        ratio >= 4.5,
1246                        "{:?} {scheme:?}: {role:?} on the code panel is {ratio:.2}:1",
1247                        table.system
1248                    );
1249                }
1250                for (ink, line, word) in DIFF_ROLES {
1251                    let background = table.color(line, scheme);
1252                    let ratio = contrast(table.color(ink, scheme), background);
1253                    assert!(
1254                        ratio >= 4.5,
1255                        "{:?} {scheme:?}: {ink:?} on {line:?} is {ratio:.2}:1",
1256                        table.system
1257                    );
1258                    let body = contrast(table.color(ColorRole::OnSurface, scheme), background);
1259                    assert!(
1260                        body >= 4.5,
1261                        "{:?} {scheme:?}: body text on {line:?} is {body:.2}:1",
1262                        table.system
1263                    );
1264                    let distance = apart(table.color(word, scheme), background);
1265                    assert!(
1266                        distance >= MIN_SEPARATION,
1267                        "{:?} {scheme:?}: {word:?} is {distance} from {line:?}, so the changed \
1268                         words are not marked",
1269                        table.system
1270                    );
1271                }
1272            }
1273        }
1274    }
1275
1276    /// The same code block does not come out in one keyword colour seven times.
1277    #[test]
1278    fn fr13_1_3_keyword_colour_differs_between_systems() {
1279        for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1280            let keywords: std::collections::BTreeSet<u32> = DESIGN_TOKENS
1281                .iter()
1282                .map(|table| table.color(ColorRole::SyntaxKeyword, scheme).to_argb())
1283                .collect();
1284            assert!(
1285                keywords.len() >= 2,
1286                "every system paints keywords #{:08x} in {scheme:?}",
1287                keywords.first().copied().unwrap_or_default()
1288            );
1289        }
1290    }
1291
1292    /// The two systems without a code editor scheme hold exactly what the rule derives
1293    /// from their own base roles.
1294    ///
1295    /// One unit of slack per channel, and only that. The derivation runs through powers,
1296    /// cube roots and an arctangent, and a different maths library may round the last bit
1297    /// of one of those the other way, which can move a channel that sits on a half by one.
1298    /// Anything more than that is a value that no longer follows the rule.
1299    #[test]
1300    fn fr13_1_3_derived_code_colours_follow_the_rule() {
1301        for system in [DesignSystem::Material3, DesignSystem::Deepin] {
1302            let table = table(system);
1303            for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1304                let derived = crate::code_colours::derive(table, scheme);
1305                assert_eq!(derived.len(), 22);
1306                for (role, expected) in derived {
1307                    let shipped = table.color(role, scheme);
1308                    let off = [16, 8, 0]
1309                        .into_iter()
1310                        .map(|shift| {
1311                            ((shipped.to_argb() >> shift) & 0xff)
1312                                .abs_diff((expected.to_argb() >> shift) & 0xff)
1313                        })
1314                        .max()
1315                        .unwrap_or(0);
1316                    assert!(
1317                        off <= 1,
1318                        "{system:?} {scheme:?}: {role:?} is #{:06x} in the table and the rule \
1319                         gives #{:06x}",
1320                        shipped.to_argb() & 0xff_ffff,
1321                        expected.to_argb() & 0xff_ffff
1322                    );
1323                }
1324            }
1325        }
1326    }
1327
1328    /// A syntax ink taken from a base role is that role's value where it already reads, so
1329    /// a reader can see in the table which role each one came from.
1330    #[test]
1331    fn fr13_1_3_a_derived_syntax_ink_is_its_base_role_where_that_reads() {
1332        for system in [DesignSystem::Material3, DesignSystem::Deepin] {
1333            let table = table(system);
1334            for scheme in [ColorScheme::Light, ColorScheme::Dark] {
1335                let panel = table.color(ColorRole::SurfaceContainer, scheme);
1336                for (role, from) in crate::code_colours::DERIVED_FROM {
1337                    let base = table.color(from, scheme);
1338                    if contrast(base, panel) >= 4.5 {
1339                        assert_eq!(
1340                            table.color(role, scheme),
1341                            base,
1342                            "{system:?} {scheme:?}: {role:?} should be {from:?}"
1343                        );
1344                    }
1345                }
1346                assert_eq!(
1347                    table.color(ColorRole::DiffModified, scheme),
1348                    table.color(ColorRole::Tertiary, scheme)
1349                );
1350            }
1351        }
1352    }
1353
1354    /// `FollowSystem` is the Renderer's call, so the Host-side lookup can only
1355    /// report the light value. This documents that, rather than leaving it a surprise.
1356    #[test]
1357    fn fr14_follow_system_reads_as_light_on_the_host() {
1358        let table = table(DesignSystem::Material3);
1359        assert_eq!(
1360            table.color(ColorRole::Surface, ColorScheme::FollowSystem),
1361            table.color(ColorRole::Surface, ColorScheme::Light)
1362        );
1363    }
1364}