gpui_ui_kit/
text.rs

1//! Text component
2//!
3//! Typography and text styling utilities.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Text size variants
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum TextSize {
11    /// Extra small
12    Xs,
13    /// Small
14    Sm,
15    /// Medium (default)
16    #[default]
17    Md,
18    /// Large
19    Lg,
20    /// Extra large
21    Xl,
22    /// 2X large
23    Xxl,
24}
25
26/// Text weight
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum TextWeight {
29    /// Light
30    Light,
31    /// Normal (default)
32    #[default]
33    Normal,
34    /// Medium
35    Medium,
36    /// Semibold
37    Semibold,
38    /// Bold
39    Bold,
40}
41
42impl TextWeight {
43    fn to_font_weight(&self) -> FontWeight {
44        match self {
45            TextWeight::Light => FontWeight::LIGHT,
46            TextWeight::Normal => FontWeight::NORMAL,
47            TextWeight::Medium => FontWeight::MEDIUM,
48            TextWeight::Semibold => FontWeight::SEMIBOLD,
49            TextWeight::Bold => FontWeight::BOLD,
50        }
51    }
52}
53
54/// A styled text component
55pub struct Text {
56    content: SharedString,
57    size: TextSize,
58    weight: TextWeight,
59    color: Option<Rgba>,
60    muted: bool,
61    truncate: bool,
62}
63
64impl Text {
65    /// Create new text
66    pub fn new(content: impl Into<SharedString>) -> Self {
67        Self {
68            content: content.into(),
69            size: TextSize::default(),
70            weight: TextWeight::default(),
71            color: None,
72            muted: false,
73            truncate: false,
74        }
75    }
76
77    /// Set size
78    pub fn size(mut self, size: TextSize) -> Self {
79        self.size = size;
80        self
81    }
82
83    /// Set weight
84    pub fn weight(mut self, weight: TextWeight) -> Self {
85        self.weight = weight;
86        self
87    }
88
89    /// Set custom color
90    pub fn color(mut self, color: Rgba) -> Self {
91        self.color = Some(color);
92        self
93    }
94
95    /// Make text muted (secondary color)
96    pub fn muted(mut self, muted: bool) -> Self {
97        self.muted = muted;
98        self
99    }
100
101    /// Truncate with ellipsis
102    pub fn truncate(mut self, truncate: bool) -> Self {
103        self.truncate = truncate;
104        self
105    }
106
107    /// Build into element
108    pub fn build(self) -> Div {
109        let text_color = if let Some(color) = self.color {
110            color
111        } else if self.muted {
112            rgb(0x888888)
113        } else {
114            rgb(0xffffff)
115        };
116
117        let mut text = div()
118            .text_color(text_color)
119            .font_weight(self.weight.to_font_weight());
120
121        // Apply size
122        text = match self.size {
123            TextSize::Xs => text.text_xs(),
124            TextSize::Sm => text.text_sm(),
125            TextSize::Md => text.text_sm(),
126            TextSize::Lg => text.text_lg(),
127            TextSize::Xl => text.text_xl(),
128            TextSize::Xxl => text.text_2xl(),
129        };
130
131        if self.truncate {
132            text = text.overflow_hidden().whitespace_nowrap();
133        }
134
135        text.child(self.content)
136    }
137}
138
139impl IntoElement for Text {
140    type Element = Div;
141
142    fn into_element(self) -> Self::Element {
143        self.build()
144    }
145}
146
147/// A heading component
148pub struct Heading {
149    content: SharedString,
150    level: u8,
151}
152
153impl Heading {
154    /// Create a new heading
155    pub fn new(content: impl Into<SharedString>) -> Self {
156        Self {
157            content: content.into(),
158            level: 1,
159        }
160    }
161
162    /// Set heading level (1-6)
163    pub fn level(mut self, level: u8) -> Self {
164        self.level = level.clamp(1, 6);
165        self
166    }
167
168    /// Create h1
169    pub fn h1(content: impl Into<SharedString>) -> Self {
170        Self::new(content).level(1)
171    }
172
173    /// Create h2
174    pub fn h2(content: impl Into<SharedString>) -> Self {
175        Self::new(content).level(2)
176    }
177
178    /// Create h3
179    pub fn h3(content: impl Into<SharedString>) -> Self {
180        Self::new(content).level(3)
181    }
182
183    /// Create h4
184    pub fn h4(content: impl Into<SharedString>) -> Self {
185        Self::new(content).level(4)
186    }
187
188    /// Build into element
189    pub fn build(self) -> Div {
190        let mut heading = div()
191            .font_weight(FontWeight::BOLD)
192            .text_color(rgb(0xffffff));
193
194        heading = match self.level {
195            1 => heading.text_2xl(),
196            2 => heading.text_xl(),
197            3 => heading.text_lg(),
198            4 => heading,
199            5 => heading.text_sm(),
200            _ => heading.text_xs(),
201        };
202
203        heading.child(self.content)
204    }
205}
206
207impl IntoElement for Heading {
208    type Element = Div;
209
210    fn into_element(self) -> Self::Element {
211        self.build()
212    }
213}
214
215/// A code/monospace text component
216pub struct Code {
217    content: SharedString,
218    inline: bool,
219}
220
221impl Code {
222    /// Create inline code
223    pub fn new(content: impl Into<SharedString>) -> Self {
224        Self {
225            content: content.into(),
226            inline: true,
227        }
228    }
229
230    /// Create code block
231    pub fn block(content: impl Into<SharedString>) -> Self {
232        Self {
233            content: content.into(),
234            inline: false,
235        }
236    }
237
238    /// Build into element
239    pub fn build(self) -> Div {
240        if self.inline {
241            div()
242                .px_1()
243                .py(px(1.0))
244                .bg(rgb(0x2a2a2a))
245                .rounded(px(3.0))
246                .text_xs()
247                .text_color(rgb(0xe06c75))
248                .child(self.content)
249        } else {
250            div()
251                .p_3()
252                .bg(rgb(0x1a1a1a))
253                .rounded_md()
254                .text_sm()
255                .text_color(rgb(0xcccccc))
256                .overflow_hidden()
257                .child(self.content)
258        }
259    }
260}
261
262impl IntoElement for Code {
263    type Element = Div;
264
265    fn into_element(self) -> Self::Element {
266        self.build()
267    }
268}
269
270/// A link component
271pub struct Link {
272    id: ElementId,
273    content: SharedString,
274    href: Option<SharedString>,
275    external: bool,
276    on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
277}
278
279impl Link {
280    /// Create a new link
281    pub fn new(id: impl Into<ElementId>, content: impl Into<SharedString>) -> Self {
282        Self {
283            id: id.into(),
284            content: content.into(),
285            href: None,
286            external: false,
287            on_click: None,
288        }
289    }
290
291    /// Set href
292    pub fn href(mut self, href: impl Into<SharedString>) -> Self {
293        self.href = Some(href.into());
294        self
295    }
296
297    /// Mark as external link
298    pub fn external(mut self, external: bool) -> Self {
299        self.external = external;
300        self
301    }
302
303    /// Set click handler
304    pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
305        self.on_click = Some(Box::new(handler));
306        self
307    }
308
309    /// Build into element
310    pub fn build(self) -> Stateful<Div> {
311        let mut link = div()
312            .id(self.id)
313            .text_color(rgb(0x007acc))
314            .cursor_pointer()
315            .hover(|s| s.text_color(rgb(0x0098ff)));
316
317        if let Some(handler) = self.on_click {
318            link = link.on_mouse_up(MouseButton::Left, move |_event, window, cx| {
319                handler(window, cx);
320            });
321        }
322
323        link = link.child(self.content);
324
325        if self.external {
326            link = link.child(div().text_xs().ml_1().child("↗"));
327        }
328
329        link
330    }
331}
332
333impl IntoElement for Link {
334    type Element = Stateful<Div>;
335
336    fn into_element(self) -> Self::Element {
337        self.build()
338    }
339}