1use gpui::{
16 div, px, AnyElement, App, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, Styled,
17 Window,
18};
19use herogpui_theme::ActiveTheme;
20
21use crate::util::MONO_FONT;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum TypographyType {
32 H1,
34 H2,
36 H3,
38 H4,
40 H5,
42 H6,
44 #[default]
46 Body,
47 BodySm,
49 BodyXs,
51 Code,
53}
54
55impl TypographyType {
56 pub fn metrics(self) -> (Pixels, Pixels) {
58 match self {
59 Self::H1 => (px(36.), px(40.)),
60 Self::H2 => (px(30.), px(36.)),
61 Self::H3 => (px(24.), px(32.)),
62 Self::H4 => (px(20.), px(28.)),
63 Self::H5 => (px(18.), px(28.)),
64 Self::H6 => (px(16.), px(24.)),
65 Self::Body => (px(16.), px(28.)),
66 Self::BodySm => (px(14.), px(24.)),
67 Self::BodyXs => (px(12.), px(20.)),
68 Self::Code => (px(14.), px(20.)),
69 }
70 }
71
72 pub fn default_weight(self) -> FontWeight {
74 match self {
75 Self::H1 | Self::H2 | Self::H3 | Self::H4 | Self::H5 | Self::H6 => FontWeight::Semibold,
76 _ => FontWeight::Normal,
77 }
78 }
79
80 pub fn is_mono(self) -> bool {
82 matches!(self, Self::Code)
83 }
84
85 pub fn heading(level: u8) -> Self {
87 match level {
88 1 => Self::H1,
89 2 => Self::H2,
90 3 => Self::H3,
91 4 => Self::H4,
92 5 => Self::H5,
93 _ => Self::H6,
94 }
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub enum TextAlign {
101 #[default]
103 Start,
104 Center,
106 End,
108 Justify,
110}
111
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub enum TextColor {
115 #[default]
117 Default,
118 Muted,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum FontWeight {
125 Normal,
127 Medium,
129 Semibold,
131 Bold,
133}
134
135impl FontWeight {
136 fn to_gpui(self) -> gpui::FontWeight {
137 match self {
138 Self::Normal => gpui::FontWeight::NORMAL,
139 Self::Medium => gpui::FontWeight::MEDIUM,
140 Self::Semibold => gpui::FontWeight::SEMIBOLD,
141 Self::Bold => gpui::FontWeight::BOLD,
142 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
148pub enum ParagraphSize {
149 #[default]
151 Base,
152 Sm,
154 Xs,
156}
157
158#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
160#[derive(IntoElement)]
161pub struct Typography {
162 kind: TypographyType,
163 align: TextAlign,
164 color: TextColor,
165 weight: Option<FontWeight>,
166 truncate: bool,
167 font_family: Option<SharedString>,
170 radius: Option<Pixels>,
174 text: Option<SharedString>,
175 children: Vec<AnyElement>,
176 sx: Option<Box<gpui::StyleRefinement>>,
178}
179
180impl Typography {
181 pub fn new(text: impl Into<SharedString>) -> Self {
183 Self {
184 kind: TypographyType::default(),
185 align: TextAlign::default(),
186 color: TextColor::default(),
187 weight: None,
188 truncate: false,
189 font_family: None,
190 radius: None,
191 text: Some(text.into()),
192 children: Vec::new(),
193 sx: None,
194 }
195 }
196
197 pub fn heading(level: u8, text: impl Into<SharedString>) -> Self {
199 Self::new(text).kind(TypographyType::heading(level))
200 }
201
202 pub fn paragraph(size: ParagraphSize, text: impl Into<SharedString>) -> Self {
204 Self::new(text).kind(match size {
205 ParagraphSize::Base => TypographyType::Body,
206 ParagraphSize::Sm => TypographyType::BodySm,
207 ParagraphSize::Xs => TypographyType::BodyXs,
208 })
209 }
210
211 pub fn code(text: impl Into<SharedString>) -> Self {
213 Self::new(text).kind(TypographyType::Code)
214 }
215
216 pub fn kind(mut self, kind: TypographyType) -> Self {
218 self.kind = kind;
219 self
220 }
221
222 pub fn align(mut self, align: TextAlign) -> Self {
224 self.align = align;
225 self
226 }
227
228 pub fn color(mut self, color: TextColor) -> Self {
230 self.color = color;
231 self
232 }
233
234 pub fn weight(mut self, weight: FontWeight) -> Self {
236 self.weight = Some(weight);
237 self
238 }
239
240 pub fn truncate(mut self, v: bool) -> Self {
242 self.truncate = v;
243 self
244 }
245
246 pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
249 self.font_family = Some(family.into());
250 self
251 }
252
253 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
258 self.radius = Some(radius.into());
259 self
260 }
261
262 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
267 crate::util::refine_sx(&mut self.sx, style);
268 self
269 }
270}
271
272impl ParentElement for Typography {
273 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
274 self.children.extend(elements);
275 }
276}
277
278impl RenderOnce for Typography {
279 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
280 let colors = cx.colors();
281 let (size, line_height) = self.kind.metrics();
282 let weight = self.weight.unwrap_or_else(|| self.kind.default_weight());
283
284 let mut el = div()
285 .text_size(size)
286 .line_height(line_height)
287 .font_weight(weight.to_gpui())
288 .text_color(match self.color {
289 TextColor::Default => colors.foreground,
290 TextColor::Muted => colors.muted,
291 });
292
293 if self.kind.is_mono() {
296 el = el
297 .font_family(MONO_FONT)
298 .bg(colors.default.color)
299 .rounded(self.radius.unwrap_or_else(|| crate::util::mark_radius(cx)))
300 .px(px(6.))
301 .py(px(2.));
302 }
303 if let Some(family) = self.font_family.clone() {
304 el = el.font_family(family);
305 }
306
307 el = match self.align {
309 TextAlign::Start | TextAlign::Justify => el.text_left(),
310 TextAlign::Center => el.text_center(),
311 TextAlign::End => el.text_right(),
312 };
313
314 if self.truncate {
315 el = el.truncate();
316 }
317
318 if let Some(text) = self.text {
319 el = el.child(text.to_string());
320 }
321 el = el.children(self.children);
322 el = crate::util::apply_sx(el, &self.sx);
323 el
324 }
325}
326
327#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
334#[derive(IntoElement)]
335pub struct Prose {
336 children: Vec<AnyElement>,
337 sx: Option<Box<gpui::StyleRefinement>>,
339}
340
341impl Prose {
342 pub fn new() -> Self {
344 Self {
345 children: Vec::new(),
346 sx: None,
347 }
348 }
349
350 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
355 crate::util::refine_sx(&mut self.sx, style);
356 self
357 }
358}
359
360impl Default for Prose {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366impl ParentElement for Prose {
367 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
368 self.children.extend(elements);
369 }
370}
371
372impl RenderOnce for Prose {
373 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
374 let el = div()
375 .text_color(cx.colors().foreground)
376 .children(self.children);
377 crate::util::apply_sx(el, &self.sx)
378 }
379}
380
381crate::util::impl_component_styled!(Typography, Prose);