1#![cfg_attr(target_family = "wasm", no_main)]
2
3use std::{
4 borrow::Cow,
5 ops::{Deref, DerefMut},
6 sync::Arc,
7};
8
9use gpui::{
10 AbsoluteLength, App, Context, DefiniteLength, ElementId, Global, Hsla, Menu, SharedString,
11 TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds, colors::DefaultColors,
12 div, point, prelude::*, px, relative, rgb, size,
13};
14use gpui_platform::application;
15use std::iter;
16
17#[derive(Clone, Debug)]
18pub struct TextContext {
19 font_size: f32,
20 line_height: f32,
21 type_scale: f32,
22}
23
24impl Default for TextContext {
25 fn default() -> Self {
26 TextContext {
27 font_size: 16.0,
28 line_height: 1.3,
29 type_scale: 1.33,
30 }
31 }
32}
33
34impl TextContext {
35 pub fn get_global(cx: &App) -> &Arc<TextContext> {
36 &cx.global::<GlobalTextContext>().0
37 }
38}
39
40#[derive(Clone, Debug)]
41pub struct GlobalTextContext(pub Arc<TextContext>);
42
43impl Deref for GlobalTextContext {
44 type Target = Arc<TextContext>;
45
46 fn deref(&self) -> &Self::Target {
47 &self.0
48 }
49}
50
51impl DerefMut for GlobalTextContext {
52 fn deref_mut(&mut self) -> &mut Self::Target {
53 &mut self.0
54 }
55}
56
57impl Global for GlobalTextContext {}
58
59pub trait ActiveTextContext {
60 fn text_context(&self) -> &Arc<TextContext>;
61}
62
63impl ActiveTextContext for App {
64 fn text_context(&self) -> &Arc<TextContext> {
65 &self.global::<GlobalTextContext>().0
66 }
67}
68
69#[derive(Clone, PartialEq)]
70pub struct SpecimenTheme {
71 pub bg: Hsla,
72 pub fg: Hsla,
73}
74
75impl Default for SpecimenTheme {
76 fn default() -> Self {
77 Self {
78 bg: gpui::white(),
79 fg: gpui::black(),
80 }
81 }
82}
83
84impl SpecimenTheme {
85 pub fn invert(&self) -> Self {
86 Self {
87 bg: self.fg,
88 fg: self.bg,
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, IntoElement)]
94struct Specimen {
95 id: ElementId,
96 scale: f32,
97 text_style: Option<TextStyle>,
98 string: SharedString,
99 invert: bool,
100}
101
102impl Specimen {
103 pub fn new(id: usize) -> Self {
104 let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
105 let id_string = format!("specimen-{}", id);
106 let id = ElementId::Name(id_string.into());
107 Self {
108 id,
109 scale: 1.0,
110 text_style: None,
111 string,
112 invert: false,
113 }
114 }
115
116 pub fn invert(mut self) -> Self {
117 self.invert = !self.invert;
118 self
119 }
120
121 pub fn scale(mut self, scale: f32) -> Self {
122 self.scale = scale;
123 self
124 }
125}
126
127impl RenderOnce for Specimen {
128 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
129 let rem_size = window.rem_size();
130 let scale = self.scale;
131 let global_style = cx.text_context();
132
133 let style_override = self.text_style;
134
135 let mut font_size = global_style.font_size;
136 let mut line_height = global_style.line_height;
137
138 if let Some(style_override) = style_override {
139 font_size = style_override.font_size.to_pixels(rem_size).into();
140 line_height = match style_override.line_height {
141 DefiniteLength::Absolute(absolute_len) => match absolute_len {
142 AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(),
143 AbsoluteLength::Pixels(absolute_len) => absolute_len.into(),
144 },
145 DefiniteLength::Fraction(value) => value,
146 };
147 }
148
149 let mut theme = SpecimenTheme::default();
150
151 if self.invert {
152 theme = theme.invert();
153 }
154
155 div()
156 .id(self.id)
157 .bg(theme.bg)
158 .text_color(theme.fg)
159 .text_size(px(font_size * scale))
160 .line_height(relative(line_height))
161 .p(px(10.0))
162 .child(self.string)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, IntoElement)]
167struct CharacterGrid {
168 scale: f32,
169 invert: bool,
170 text_style: Option<TextStyle>,
171}
172
173impl CharacterGrid {
174 pub fn new() -> Self {
175 Self {
176 scale: 1.0,
177 invert: false,
178 text_style: None,
179 }
180 }
181
182 pub fn scale(mut self, scale: f32) -> Self {
183 self.scale = scale;
184 self
185 }
186}
187
188impl RenderOnce for CharacterGrid {
189 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
190 let mut theme = SpecimenTheme::default();
191
192 if self.invert {
193 theme = theme.invert();
194 }
195
196 let characters = vec![
197 "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
198 "H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
199 "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
200 "r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
201 "Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
202 "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
203 "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
204 "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ",
205 "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
206 ];
207
208 let columns = 20;
209 let rows = characters.len().div_ceil(columns);
210
211 let grid_rows = (0..rows).map(|row_idx| {
212 let start_idx = row_idx * columns;
213 let end_idx = (start_idx + columns).min(characters.len());
214
215 div()
216 .w_full()
217 .flex()
218 .flex_row()
219 .children((start_idx..end_idx).map(|i| {
220 div()
221 .text_center()
222 .size(px(62.))
223 .bg(theme.bg)
224 .text_color(theme.fg)
225 .text_size(px(24.0))
226 .line_height(relative(1.0))
227 .child(characters[i])
228 }))
229 .when(end_idx - start_idx < columns, |d| {
230 d.children(
231 iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
232 )
233 })
234 });
235
236 div().p_4().gap_2().flex().flex_col().children(grid_rows)
237 }
238}
239
240struct TextExample {
241 next_id: usize,
242 font_family: SharedString,
243}
244
245impl TextExample {
246 fn next_id(&mut self) -> usize {
247 self.next_id += 1;
248 self.next_id
249 }
250
251 fn button(
252 text: &str,
253 cx: &mut Context<Self>,
254 on_click: impl Fn(&mut Self, &mut Context<Self>) + 'static,
255 ) -> impl IntoElement {
256 div()
257 .id(text.to_string())
258 .flex_none()
259 .child(text.to_string())
260 .bg(gpui::black())
261 .text_color(gpui::white())
262 .active(|this| this.opacity(0.8))
263 .px_3()
264 .py_1()
265 .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
266 }
267}
268
269const FONT_FAMILIES: [&str; 5] = [
270 ".ZedMono",
271 ".SystemUIFont",
272 "Menlo",
273 "Monaco",
274 "Courier New",
275];
276
277impl Render for TextExample {
278 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
279 let tcx = cx.text_context();
280 let colors = cx.default_colors().clone();
281
282 let type_scale = tcx.type_scale;
283
284 let step_down_2 = 1.0 / (type_scale * type_scale);
285 let step_down_1 = 1.0 / type_scale;
286 let base = 1.0;
287 let step_up_1 = base * type_scale;
288 let step_up_2 = step_up_1 * type_scale;
289 let step_up_3 = step_up_2 * type_scale;
290 let step_up_4 = step_up_3 * type_scale;
291 let step_up_5 = step_up_4 * type_scale;
292 let step_up_6 = step_up_5 * type_scale;
293
294 div()
295 .font_family(self.font_family.clone())
296 .size_full()
297 .child(
298 div()
299 .bg(gpui::white())
300 .border_b_1()
301 .border_color(gpui::black())
302 .p_3()
303 .flex()
304 .child(Self::button(&self.font_family, cx, |this, cx| {
305 let new_family = FONT_FAMILIES
306 .iter()
307 .position(|f| *f == this.font_family.as_str())
308 .map(|idx| FONT_FAMILIES[(idx + 1) % FONT_FAMILIES.len()])
309 .unwrap_or(FONT_FAMILIES[0]);
310
311 this.font_family = SharedString::new(new_family);
312 cx.notify();
313 })),
314 )
315 .child(
316 div()
317 .id("text-example")
318 .overflow_y_scroll()
319 .overflow_x_hidden()
320 .bg(rgb(0xffffff))
321 .size_full()
322 .child(div().child(CharacterGrid::new().scale(base)))
323 .child(
324 div()
325 .child(Specimen::new(self.next_id()).scale(step_down_2))
326 .child(Specimen::new(self.next_id()).scale(step_down_2).invert())
327 .child(Specimen::new(self.next_id()).scale(step_down_1))
328 .child(Specimen::new(self.next_id()).scale(step_down_1).invert())
329 .child(Specimen::new(self.next_id()).scale(base))
330 .child(Specimen::new(self.next_id()).scale(base).invert())
331 .child(Specimen::new(self.next_id()).scale(step_up_1))
332 .child(Specimen::new(self.next_id()).scale(step_up_1).invert())
333 .child(Specimen::new(self.next_id()).scale(step_up_2))
334 .child(Specimen::new(self.next_id()).scale(step_up_2).invert())
335 .child(Specimen::new(self.next_id()).scale(step_up_3))
336 .child(Specimen::new(self.next_id()).scale(step_up_3).invert())
337 .child(Specimen::new(self.next_id()).scale(step_up_4))
338 .child(Specimen::new(self.next_id()).scale(step_up_4).invert())
339 .child(Specimen::new(self.next_id()).scale(step_up_5))
340 .child(Specimen::new(self.next_id()).scale(step_up_5).invert())
341 .child(Specimen::new(self.next_id()).scale(step_up_6))
342 .child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
343 ),
344 )
345 .child(div().w(px(240.)).h_full().bg(colors.container))
346 }
347}
348
349fn run_example() {
350 application().run(|cx: &mut App| {
351 cx.set_menus(vec![Menu {
352 name: "GPUI Typography".into(),
353 disabled: false,
354 items: vec![],
355 }]);
356
357 let fonts = [include_bytes!(
358 "../../../assets/fonts/lilex/Lilex-Regular.ttf"
359 )]
360 .iter()
361 .map(|b| Cow::Borrowed(&b[..]))
362 .collect();
363
364 _ = cx.text_system().add_fonts(fonts);
365
366 cx.init_colors();
367 cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
368
369 let window = cx
370 .open_window(
371 WindowOptions {
372 titlebar: Some(TitlebarOptions {
373 title: Some("GPUI Typography".into()),
374 ..Default::default()
375 }),
376 window_bounds: Some(WindowBounds::Windowed(bounds(
377 point(px(0.0), px(0.0)),
378 size(px(920.), px(720.)),
379 ))),
380 ..Default::default()
381 },
382 |_window, cx| {
383 cx.new(|_cx| TextExample {
384 next_id: 0,
385 font_family: ".ZedMono".into(),
386 })
387 },
388 )
389 .unwrap();
390
391 window
392 .update(cx, |_view, _window, cx| {
393 cx.activate(true);
394 })
395 .unwrap();
396 });
397}
398
399#[cfg(not(target_family = "wasm"))]
400fn main() {
401 run_example();
402}
403
404#[cfg(target_family = "wasm")]
405#[wasm_bindgen::prelude::wasm_bindgen(start)]
406pub fn start() {
407 gpui_platform::web_init();
408 run_example();
409}