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