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 items: vec![],
354 }]);
355
356 let fonts = [include_bytes!(
357 "../../../assets/fonts/lilex/Lilex-Regular.ttf"
358 )]
359 .iter()
360 .map(|b| Cow::Borrowed(&b[..]))
361 .collect();
362
363 _ = cx.text_system().add_fonts(fonts);
364
365 cx.init_colors();
366 cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
367
368 let window = cx
369 .open_window(
370 WindowOptions {
371 titlebar: Some(TitlebarOptions {
372 title: Some("GPUI Typography".into()),
373 ..Default::default()
374 }),
375 window_bounds: Some(WindowBounds::Windowed(bounds(
376 point(px(0.0), px(0.0)),
377 size(px(920.), px(720.)),
378 ))),
379 ..Default::default()
380 },
381 |_window, cx| {
382 cx.new(|_cx| TextExample {
383 next_id: 0,
384 font_family: ".ZedMono".into(),
385 })
386 },
387 )
388 .unwrap();
389
390 window
391 .update(cx, |_view, _window, cx| {
392 cx.activate(true);
393 })
394 .unwrap();
395 });
396}
397
398#[cfg(not(target_family = "wasm"))]
399fn main() {
400 run_example();
401}
402
403#[cfg(target_family = "wasm")]
404#[wasm_bindgen::prelude::wasm_bindgen(start)]
405pub fn start() {
406 gpui_platform::web_init();
407 run_example();
408}