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 pub fn wrap(mut self) -> Self {
105 self.wrap = true;
106 self.fill_width_on_wrap = true;
107 self
108 }
109
110 pub fn auto_wrap(self) -> Self {
112 self.wrap()
113 }
114
115 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 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 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}