text.rs

  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}