Skip to main content

gpui_kit_material/tokens/
shape.rs

1//! Corner radii and the connected-group corner rule.
2use gpui::{Pixels, Styled, px};
3
4// The M3 / M3 Expressive corner scale. Reference priority is Figma kit >
5// md3.io > m3e.kyralabs.dev; the Figma Material 3 Design Kit defines it as
6// its `Shape` variable collection (`Corner/*`, VariableCollectionId:55064:15586,
7// mode Baseline, file key ozddDB2Z1v51f1IFZlUuJ2). Fixture:
8// tests/fixtures/figma-sys-tokens.json (`shape_corner_px`). md3.io and m3e
9// (tests/fixtures/m3e-sys-tokens.json) agree on every finite step. Corners are
10// dp-sized geometry and do not follow the text scale.
11
12/// M3 corner none (0).
13pub const RADIUS_NONE: Pixels = px(0.);
14/// M3 corner extra-small (4): snackbars, text fields, small flat surfaces.
15pub const RADIUS_EXTRA_SMALL: Pixels = px(4.);
16/// M3 corner small (8): chips, badges, small covers and notices.
17pub const RADIUS_SMALL: Pixels = px(8.);
18/// M3 corner medium (12): cards.
19pub const RADIUS_MEDIUM: Pixels = px(12.);
20/// M3 corner large (16).
21pub const RADIUS_LARGE: Pixels = px(16.);
22/// M3 Expressive corner large-increased (20).
23pub const RADIUS_LARGE_INCREASED: Pixels = px(20.);
24/// M3 corner extra-large (28): dialogs, large hero surfaces.
25pub const RADIUS_EXTRA_LARGE: Pixels = px(28.);
26/// M3 Expressive corner extra-large-increased (32).
27pub const RADIUS_EXTRA_LARGE_INCREASED: Pixels = px(32.);
28/// M3 Expressive corner extra-extra-large (48).
29pub const RADIUS_EXTRA_EXTRA_LARGE: Pixels = px(48.);
30/// M3 corner full: the kit's `Corner/Full` (1000), large enough to round any
31/// control into a pill.
32pub const RADIUS_FULL: Pixels = px(1000.);
33
34/// Alias of [`RADIUS_FULL`].
35pub const RADIUS_PILL: Pixels = RADIUS_FULL;
36/// Filled and outlined text fields: M3 extra-small (top corners for filled).
37pub const FIELD_RADIUS: Pixels = RADIUS_EXTRA_SMALL;
38/// Cards: M3 medium.
39pub const RADIUS_CARD: Pixels = RADIUS_MEDIUM;
40/// Menus and the fields that open them: M3 large.
41pub const MENU_RADIUS: Pixels = RADIUS_LARGE;
42/// Large standalone cards and hero surfaces: M3 extra-large.
43pub const RADIUS_HERO: Pixels = RADIUS_EXTRA_LARGE;
44
45/// Connected tiles keep 24px corners on their group's outside and 6px
46/// where tiles meet, with 2px of ground between them.
47pub const TILE_RADIUS: Pixels = px(24.);
48pub const TILE_INNER_RADIUS: Pixels = px(6.);
49pub const TILE_GAP: Pixels = px(2.);
50
51/// Which corners of a connected item are on its group's outside, as
52/// top-left, top-right, bottom-right, bottom-left.
53pub type OuterCorners = [bool; 4];
54
55/// Round `element` as a connected tile: outside corners at
56/// [`TILE_RADIUS`], inner ones at [`TILE_INNER_RADIUS`].
57pub fn tile_corners<E: Styled>(element: E, outer: OuterCorners) -> E {
58    let radius = |outside: bool| {
59        if outside {
60            TILE_RADIUS
61        } else {
62            TILE_INNER_RADIUS
63        }
64    };
65    element
66        .rounded_tl(radius(outer[0]))
67        .rounded_tr(radius(outer[1]))
68        .rounded_br(radius(outer[2]))
69        .rounded_bl(radius(outer[3]))
70}
71
72/// The outside corners of item `index` in a vertical group of `count`.
73pub fn list_corners(index: usize, count: usize) -> OuterCorners {
74    let first = index == 0;
75    let last = index + 1 >= count;
76    [first, first, last, last]
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn vertical_groups_round_only_their_ends() {
85        assert_eq!(list_corners(0, 1), [true; 4]);
86        assert_eq!(list_corners(0, 3), [true, true, false, false]);
87        assert_eq!(list_corners(1, 3), [false; 4]);
88        assert_eq!(list_corners(2, 3), [false, false, true, true]);
89    }
90
91    #[test]
92    fn corner_scale_matches_m3() {
93        let scale = [
94            (RADIUS_NONE, 0.),
95            (RADIUS_EXTRA_SMALL, 4.),
96            (RADIUS_SMALL, 8.),
97            (RADIUS_MEDIUM, 12.),
98            (RADIUS_LARGE, 16.),
99            (RADIUS_LARGE_INCREASED, 20.),
100            (RADIUS_EXTRA_LARGE, 28.),
101            (RADIUS_EXTRA_LARGE_INCREASED, 32.),
102            (RADIUS_EXTRA_EXTRA_LARGE, 48.),
103        ];
104        for (radius, expected) in scale {
105            assert_eq!(radius, px(expected));
106        }
107        let fixture: serde_json::Value =
108            serde_json::from_str(include_str!("../../tests/fixtures/figma-sys-tokens.json"))
109                .unwrap();
110        let reference = &fixture["shape_corner_px"];
111        for (radius, name) in [
112            (RADIUS_NONE, "none"),
113            (RADIUS_EXTRA_SMALL, "extra-small"),
114            (RADIUS_SMALL, "small"),
115            (RADIUS_MEDIUM, "medium"),
116            (RADIUS_LARGE, "large"),
117            (RADIUS_LARGE_INCREASED, "large-increased"),
118            (RADIUS_EXTRA_LARGE, "extra-large"),
119            (RADIUS_EXTRA_LARGE_INCREASED, "extra-large-increased"),
120            (RADIUS_EXTRA_EXTRA_LARGE, "extra-extra-large"),
121            (RADIUS_FULL, "full"),
122        ] {
123            assert_eq!(
124                radius,
125                px(reference[name].as_f64().unwrap() as f32),
126                "{name}"
127            );
128        }
129        assert_eq!(FIELD_RADIUS, px(4.), "M3 text fields use extra-small");
130        assert_eq!(RADIUS_CARD, px(12.), "M3 cards use medium");
131    }
132}