Skip to main content

liora_components/
text.rs

1use crate::{SelectableText, SelectableTextOptions, SelectableTextWrap};
2use gpui::{
3    App, Component, ElementId, FontStyle, FontWeight, Hsla, IntoElement, Pixels, RenderOnce,
4    SharedString, StrikethroughStyle, TextRun, TextStyle, UnderlineStyle, Window, div, prelude::*,
5    px,
6};
7use liora_core::Config;
8
9#[derive(Clone)]
10pub struct Text {
11    pub(crate) content: SharedString,
12    pub(crate) color: Option<Hsla>,
13    pub(crate) bg: Option<Hsla>,
14    pub(crate) size: Option<Pixels>,
15    pub(crate) weight: Option<FontWeight>,
16    pub(crate) style: Option<FontStyle>,
17    pub(crate) underline: bool,
18    pub(crate) strikethrough: bool,
19    pub(crate) font_family: Option<SharedString>,
20    pub(crate) wrap: bool,
21    pub(crate) fill_width_on_wrap: bool,
22    pub(crate) selectable: bool,
23    pub(crate) id: SharedString,
24}
25
26impl Text {
27    pub fn new(content: impl Into<SharedString>) -> Self {
28        Self {
29            content: content.into(),
30            color: None,
31            bg: None,
32            size: None,
33            weight: None,
34            style: None,
35            underline: false,
36            strikethrough: false,
37            font_family: None,
38            wrap: true,
39            fill_width_on_wrap: false,
40            selectable: true,
41            id: liora_core::unique_id("text"),
42        }
43    }
44
45    pub fn text_color(mut self, color: Hsla) -> Self {
46        self.color = Some(color);
47        self
48    }
49
50    pub fn bg(mut self, bg: Hsla) -> Self {
51        self.bg = Some(bg);
52        self
53    }
54
55    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
56        self.size = Some(size.into());
57        self
58    }
59
60    pub fn xs(self) -> Self {
61        self.size(px(12.0))
62    }
63
64    pub fn sm(self) -> Self {
65        self.size(px(14.0))
66    }
67
68    pub fn weight(mut self, weight: FontWeight) -> Self {
69        self.weight = Some(weight);
70        self
71    }
72
73    pub fn bold(mut self) -> Self {
74        self.weight = Some(FontWeight::BOLD);
75        self
76    }
77
78    pub fn font_style(mut self, style: FontStyle) -> Self {
79        self.style = Some(style);
80        self
81    }
82
83    pub fn italic(mut self) -> Self {
84        self.style = Some(FontStyle::Italic);
85        self
86    }
87
88    pub fn underline(mut self) -> Self {
89        self.underline = true;
90        self
91    }
92
93    pub fn strikethrough(mut self) -> Self {
94        self.strikethrough = true;
95        self
96    }
97
98    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
99        self.font_family = Some(family.into());
100        self
101    }
102
103    /// Enable normal whitespace wrapping and let the text take the parent width.
104    pub fn wrap(mut self) -> Self {
105        self.wrap = true;
106        self.fill_width_on_wrap = true;
107        self
108    }
109
110    /// Alias for [`Text::wrap`].
111    pub fn auto_wrap(self) -> Self {
112        self.wrap()
113    }
114
115    /// Keep the text on a single line.
116    pub fn nowrap(mut self) -> Self {
117        self.wrap = false;
118        self.fill_width_on_wrap = false;
119        self
120    }
121
122    pub fn selectable(mut self, selectable: bool) -> Self {
123        self.selectable = selectable;
124        self
125    }
126
127    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
128        self.id = id.into();
129        self
130    }
131
132    /// Convenience for "code" style
133    pub fn code_style(mut self, theme: &liora_theme::Theme) -> Self {
134        self.font_family = Some("Monospace".into());
135        self.bg = Some(theme.neutral.hover);
136        self.text_color(theme.danger.base)
137    }
138
139    pub(crate) fn apply_to_text_style(&self, mut style: TextStyle) -> TextStyle {
140        if let Some(color) = self.color {
141            style.color = color;
142        }
143
144        if let Some(bg) = self.bg {
145            style.background_color = Some(bg);
146        }
147
148        if let Some(weight) = self.weight {
149            style.font_weight = weight;
150        }
151
152        if let Some(font_style) = self.style {
153            style.font_style = font_style;
154        }
155
156        if let Some(family) = self.font_family.clone() {
157            style.font_family = family;
158        }
159
160        if self.underline {
161            style.underline = Some(UnderlineStyle {
162                thickness: px(1.0),
163                color: self.color,
164                ..Default::default()
165            });
166        }
167
168        if self.strikethrough {
169            style.strikethrough = Some(StrikethroughStyle {
170                thickness: px(1.0),
171                color: self.color,
172            });
173        }
174
175        style
176    }
177
178    pub(crate) fn to_text_run(&self, default_style: &TextStyle) -> TextRun {
179        self.apply_to_text_style(default_style.clone())
180            .to_run(self.content.len())
181    }
182
183    pub fn register_key_bindings(cx: &mut App) {
184        SelectableText::register_key_bindings(cx);
185    }
186}
187
188impl RenderOnce for Text {
189    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
190        let theme = &cx.global::<Config>().theme;
191
192        let font_size = self.size.unwrap_or_else(|| px(theme.font_size.md));
193        let line_height = font_size * 1.6;
194        let text_color = self.color.unwrap_or(theme.neutral.text_2);
195
196        if self.selectable {
197            let mut base_style = TextStyle::default();
198            base_style.color = text_color;
199            base_style.font_size = font_size.into();
200            base_style.line_height = line_height.into();
201            base_style.white_space = if self.wrap {
202                gpui::WhiteSpace::Normal
203            } else {
204                gpui::WhiteSpace::Nowrap
205            };
206            let run = self.to_text_run(&base_style);
207            return SelectableText::view(
208                SelectableTextOptions {
209                    id: ElementId::from(self.id.clone()),
210                    text: self.content.clone(),
211                    runs: vec![run],
212                    font_size,
213                    line_height,
214                    text_color,
215                    wrap: if self.wrap {
216                        SelectableTextWrap::Normal
217                    } else {
218                        SelectableTextWrap::NoWrap
219                    },
220                    key_context: "SelectableText",
221                    fill_width: self.fill_width_on_wrap,
222                },
223                _window,
224                cx,
225            );
226        }
227
228        let mut el = div()
229            .child(self.content.clone())
230            .text_size(font_size)
231            .line_height(line_height)
232            .text_color(text_color);
233
234        if self.wrap {
235            el = el.whitespace_normal();
236            if self.fill_width_on_wrap {
237                el = el.w_full().flex_shrink();
238            }
239        } else {
240            el = el.whitespace_nowrap();
241        }
242
243        if let Some(bg) = self.bg {
244            el = el.bg(bg).px_1().rounded(px(2.0));
245        }
246
247        if let Some(weight) = self.weight {
248            el = el.font_weight(weight);
249        }
250
251        if let Some(style) = self.style {
252            // In some GPUI versions, it's .italic(), in others it's .font_style(style)
253            // If .font_style failed, let's try matching on style
254            if style == FontStyle::Italic {
255                el = el.italic();
256            }
257        }
258
259        if self.underline {
260            el = el.underline();
261        }
262
263        if self.strikethrough {
264            el = el.line_through();
265        }
266
267        if let Some(family) = self.font_family {
268            el = el.font_family(family);
269        }
270
271        el.into_any_element()
272    }
273}
274
275impl IntoElement for Text {
276    type Element = Component<Self>;
277    fn into_element(self) -> Self::Element {
278        Component::new(self)
279    }
280}