1use gpui::{Rems, rems};
23
24pub const BASE_PX: f32 = 14.;
26
27pub const fn rem(px: f32) -> Rems {
29 rems(px / BASE_PX)
30}
31
32pub const TEXT_AUX: Rems = TypeRole::BodySmall.scale().font_size;
38pub const TEXT_BODY: Rems = TypeRole::BodyMedium.scale().font_size;
40pub const TEXT_READER: Rems = TypeRole::BodyLarge.scale().font_size;
42pub const TEXT_TITLE: Rems = TypeRole::TitleLarge.scale().font_size;
44pub const TEXT_SECTION: Rems = TypeRole::TitleLarge.scale().font_size;
46pub const TEXT_HEADLINE: Rems = TypeRole::HeadlineSmall.scale().font_size;
48pub const HEADLINE_LINE_HEIGHT: Rems = TypeRole::HeadlineSmall.scale().line_height;
49pub const TEXT_SETTINGS_BODY: Rems = TypeRole::BodyLarge.scale().font_size;
53pub const TEXT_SETTINGS_AUX: Rems = TEXT_BODY;
56
57pub const LEGACY_ROLES: [(&str, TypeRole); 8] = [
59 ("TEXT_AUX", TypeRole::BodySmall),
60 ("TEXT_BODY", TypeRole::BodyMedium),
61 ("TEXT_READER", TypeRole::BodyLarge),
62 ("TEXT_TITLE", TypeRole::TitleLarge),
63 ("TEXT_SECTION", TypeRole::TitleLarge),
64 ("TEXT_HEADLINE", TypeRole::HeadlineSmall),
65 ("TEXT_SETTINGS_BODY", TypeRole::BodyLarge),
66 ("TEXT_SETTINGS_AUX", TypeRole::BodyMedium),
67];
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum TypeRole {
72 DisplayLarge,
73 DisplayMedium,
74 DisplaySmall,
75 HeadlineLarge,
76 HeadlineMedium,
77 HeadlineSmall,
78 TitleLarge,
79 TitleMedium,
80 TitleSmall,
81 BodyLarge,
82 BodyMedium,
83 BodySmall,
84 LabelLarge,
85 LabelMedium,
86 LabelSmall,
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum TypefaceRole {
92 Brand,
93 Plain,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct TypeScale {
103 pub typeface: TypefaceRole,
104 pub font_family: &'static str,
106 pub font_size: Rems,
107 pub line_height: Rems,
108 pub letter_spacing: Rems,
109 pub font_weight: u16,
110}
111
112impl TypeRole {
113 pub const ALL: [Self; 15] = [
114 Self::DisplayLarge,
115 Self::DisplayMedium,
116 Self::DisplaySmall,
117 Self::HeadlineLarge,
118 Self::HeadlineMedium,
119 Self::HeadlineSmall,
120 Self::TitleLarge,
121 Self::TitleMedium,
122 Self::TitleSmall,
123 Self::BodyLarge,
124 Self::BodyMedium,
125 Self::BodySmall,
126 Self::LabelLarge,
127 Self::LabelMedium,
128 Self::LabelSmall,
129 ];
130
131 pub const fn name(self) -> &'static str {
132 match self {
133 Self::DisplayLarge => "display-large",
134 Self::DisplayMedium => "display-medium",
135 Self::DisplaySmall => "display-small",
136 Self::HeadlineLarge => "headline-large",
137 Self::HeadlineMedium => "headline-medium",
138 Self::HeadlineSmall => "headline-small",
139 Self::TitleLarge => "title-large",
140 Self::TitleMedium => "title-medium",
141 Self::TitleSmall => "title-small",
142 Self::BodyLarge => "body-large",
143 Self::BodyMedium => "body-medium",
144 Self::BodySmall => "body-small",
145 Self::LabelLarge => "label-large",
146 Self::LabelMedium => "label-medium",
147 Self::LabelSmall => "label-small",
148 }
149 }
150
151 pub const fn typeface(self) -> TypefaceRole {
155 match self {
156 Self::TitleMedium
157 | Self::TitleSmall
158 | Self::BodyLarge
159 | Self::BodyMedium
160 | Self::BodySmall
161 | Self::LabelLarge
162 | Self::LabelMedium
163 | Self::LabelSmall => TypefaceRole::Plain,
164 _ => TypefaceRole::Brand,
165 }
166 }
167
168 pub const fn scale(self) -> TypeScale {
170 let (size, height, tracking, weight) = match self {
171 Self::DisplayLarge => (57.0, 64.0, -0.25, 400),
172 Self::DisplayMedium => (45.0, 52.0, 0.0, 400),
173 Self::DisplaySmall => (36.0, 44.0, 0.0, 400),
174 Self::HeadlineLarge => (32.0, 40.0, 0.0, 400),
175 Self::HeadlineMedium => (28.0, 36.0, 0.0, 400),
176 Self::HeadlineSmall => (24.0, 32.0, 0.0, 400),
177 Self::TitleLarge => (22.0, 28.0, 0.0, 400),
178 Self::TitleMedium => (16.0, 24.0, 0.15, 500),
179 Self::TitleSmall => (14.0, 20.0, 0.1, 500),
180 Self::BodyLarge => (16.0, 24.0, 0.5, 400),
181 Self::BodyMedium => (14.0, 20.0, 0.25, 400),
182 Self::BodySmall => (12.0, 16.0, 0.4, 400),
183 Self::LabelLarge => (14.0, 20.0, 0.1, 500),
184 Self::LabelMedium => (12.0, 16.0, 0.5, 500),
185 Self::LabelSmall => (11.0, 16.0, 0.5, 500),
186 };
187 self.metrics(size, height, tracking, weight)
188 }
189
190 pub const fn emphasized(self) -> TypeScale {
195 let base = self.scale();
196 TypeScale {
197 font_weight: if base.font_weight == 400 { 500 } else { 600 },
198 ..base
199 }
200 }
201
202 const fn metrics(self, size: f32, height: f32, tracking: f32, weight: u16) -> TypeScale {
203 TypeScale {
204 typeface: self.typeface(),
205 font_family: "Roboto",
206 font_size: rem(size),
207 line_height: rem(height),
208 letter_spacing: rem(tracking),
209 font_weight: weight,
210 }
211 }
212}
213
214#[cfg(all(test, not(target_family = "wasm")))]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn legacy_sizes_are_their_mapped_roles() {
220 let sizes = [
221 TEXT_AUX,
222 TEXT_BODY,
223 TEXT_READER,
224 TEXT_TITLE,
225 TEXT_SECTION,
226 TEXT_HEADLINE,
227 TEXT_SETTINGS_BODY,
228 TEXT_SETTINGS_AUX,
229 ];
230 for (size, (name, role)) in sizes.into_iter().zip(LEGACY_ROLES) {
231 assert_eq!(size, role.scale().font_size, "{name}");
232 }
233 }
234
235 #[test]
236 fn all_roles_match_the_figma_kit_text_styles() {
237 let fixture: serde_json::Value =
238 serde_json::from_str(include_str!("../../tests/fixtures/figma-typography.json"))
239 .unwrap();
240 assert_eq!(fixture["font_theme"]["brand"], "Roboto");
241 assert_eq!(fixture["font_theme"]["plain"], "Roboto");
242 for (key, style) in [
243 ("roles", TypeRole::scale as fn(TypeRole) -> TypeScale),
244 ("emphasized", TypeRole::emphasized),
245 ] {
246 let roles = fixture[key].as_array().unwrap();
247 assert_eq!(roles.len(), TypeRole::ALL.len());
248 for role in TypeRole::ALL {
249 let expected = roles.iter().find(|row| row["name"] == role.name()).unwrap();
250 assert_matches(style(role), expected, role.name());
251 }
252 }
253 }
254
255 fn assert_matches(scale: TypeScale, expected: &serde_json::Value, name: &str) {
256 assert_eq!(
257 scale.typeface,
258 if expected["typeface"] == "brand" {
259 TypefaceRole::Brand
260 } else {
261 TypefaceRole::Plain
262 },
263 "{name} typeface"
264 );
265 assert_eq!(
266 scale.font_family,
267 expected["font_family"].as_str().unwrap(),
268 "{name} family"
269 );
270 assert_eq!(
271 scale.font_weight,
272 expected["font_weight"].as_u64().unwrap() as u16
273 );
274 for (actual, key) in [
275 (scale.font_size, "font_size"),
276 (scale.line_height, "line_height"),
277 (scale.letter_spacing, "letter_spacing"),
278 ] {
279 assert_eq!(
280 actual,
281 rem(expected[key].as_f64().unwrap() as f32),
282 "{name} {key}"
283 );
284 }
285 }
286}