1use gpui::{
2 AnyElement, App, Bounds, ClickEvent, Element, ElementId, Entity, GlobalElementId,
3 HighlightStyle, InspectorElementId, IntoElement, LayoutId, Pixels, Refineable as _, RenderOnce,
4 SharedString, StyleRefinement, Styled, Window,
5};
6
7use super::{
8 MarkdownExtensions, MarkdownNode, MarkdownParseContext, MarkdownPlugin, SelectionFormat,
9 TableData, TextViewState, TextViewStyle,
10};
11use gpui_base::text::CodeBlock;
12
13#[derive(Clone)]
20pub struct TextView {
21 id: ElementId,
22 inner: gpui_base::TextView,
23 text_style: Option<TextViewStyle>,
24}
25
26impl Styled for TextView {
27 fn style(&mut self) -> &mut StyleRefinement {
28 gpui::Styled::style(&mut self.inner)
29 }
30}
31
32impl TextView {
33 pub fn new(state: &Entity<TextViewState>) -> Self {
35 Self {
36 id: ElementId::Name(state.entity_id().to_string().into()),
37 inner: gpui_base::TextView::new(state),
38 text_style: None,
39 }
40 }
41 pub fn markdown(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
43 let id = id.into();
44 Self {
45 id: id.clone(),
46 inner: gpui_base::TextView::markdown(id, text),
47 text_style: None,
48 }
49 }
50 pub fn html(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
52 let id = id.into();
53 Self {
54 id: id.clone(),
55 inner: gpui_base::TextView::html(id, text),
56 text_style: None,
57 }
58 }
59 pub fn style(mut self, style: TextViewStyle) -> Self {
61 self.text_style = Some(style);
62 self
63 }
64 pub fn selectable(mut self, value: bool) -> Self {
66 self.inner = self.inner.selectable(value);
67 self
68 }
69 pub fn selection_format(mut self, value: SelectionFormat) -> Self {
71 self.inner = self.inner.selection_format(value);
72 self
73 }
74 pub fn scrollable(mut self, value: bool) -> Self {
76 self.inner = self.inner.scrollable(value);
77 self
78 }
79 pub fn max_lines(mut self, value: usize) -> Self {
81 self.inner = self.inner.max_lines(value);
82 self
83 }
84 pub fn code_block_actions<F, E>(mut self, f: F) -> Self
86 where
87 F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static,
88 E: IntoElement,
89 {
90 self.inner = self.inner.code_block_actions(f);
91 self
92 }
93 pub fn table_actions<F, E>(mut self, f: F) -> Self
95 where
96 F: Fn(&TableData, &mut Window, &mut App) -> E + Send + Sync + 'static,
97 E: IntoElement,
98 {
99 self.inner = self.inner.table_actions(f);
100 self
101 }
102 pub fn on_link_click<F>(mut self, f: F) -> Self
104 where
105 F: Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync + 'static,
106 {
107 self.inner = self.inner.on_link_click(f);
108 self
109 }
110 pub fn markdown_extensions(mut self, value: MarkdownExtensions) -> Self {
112 self.inner = self.inner.markdown_extensions(value);
113 self
114 }
115 pub fn markdown_mdx(mut self) -> Self {
117 self.inner = self.inner.markdown_mdx();
118 self
119 }
120 pub fn markdown_block_parser<F>(mut self, parser: F) -> Self
122 where
123 F: for<'a> Fn(&markdown::mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
124 + Send
125 + Sync
126 + 'static,
127 {
128 self.inner = self.inner.markdown_block_parser(parser);
129 self
130 }
131 pub fn markdown_block_renderer<F, E>(
133 mut self,
134 name: impl Into<SharedString>,
135 renderer: F,
136 ) -> Self
137 where
138 F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
139 E: IntoElement,
140 {
141 self.inner = self.inner.markdown_block_renderer(name, renderer);
142 self
143 }
144 pub fn plugin<P>(self, plugin: P) -> Self
146 where
147 P: TextViewPlugin,
148 {
149 plugin.setup(self)
150 }
151}
152
153impl IntoElement for TextView {
154 type Element = Self;
155
156 fn into_element(self) -> Self::Element {
157 self
158 }
159}
160
161pub struct TextViewLayoutState {
163 element: AnyElement,
164}
165
166pub struct TextViewPrepaintState;
168
169impl Element for TextView {
170 type RequestLayoutState = TextViewLayoutState;
171 type PrepaintState = TextViewPrepaintState;
172
173 fn id(&self) -> Option<ElementId> {
174 Some(self.id.clone())
175 }
176
177 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
178 None
179 }
180
181 fn request_layout(
182 &mut self,
183 _: Option<&GlobalElementId>,
184 _: Option<&InspectorElementId>,
185 window: &mut Window,
186 cx: &mut App,
187 ) -> (LayoutId, Self::RequestLayoutState) {
188 let mut inner = self.inner.clone();
189 if let Some(style) = self.text_style.clone() {
190 #[cfg(feature = "tree-sitter")]
195 if !std::sync::Arc::ptr_eq(
196 &style.highlight_theme,
197 &crate::highlighter::HighlightTheme::default_light(),
198 ) {
199 inner = inner.code_block_highlighter(super::component_code_block_highlighter(
200 style.highlight_theme.clone(),
201 ));
202 }
203 inner = inner.style(resolve_component_style(
204 crate::ActiveTheme::theme(cx),
205 style,
206 ));
207 }
208 let mut element = inner.into_any_element();
209 let layout_id = element.request_layout(window, cx);
210 (layout_id, TextViewLayoutState { element })
211 }
212
213 fn prepaint(
214 &mut self,
215 _: Option<&GlobalElementId>,
216 _: Option<&InspectorElementId>,
217 _: Bounds<Pixels>,
218 element: &mut Self::RequestLayoutState,
219 window: &mut Window,
220 cx: &mut App,
221 ) -> Self::PrepaintState {
222 element.element.prepaint(window, cx);
223 TextViewPrepaintState
224 }
225
226 fn paint(
227 &mut self,
228 _: Option<&GlobalElementId>,
229 _: Option<&InspectorElementId>,
230 _: Bounds<Pixels>,
231 element: &mut Self::RequestLayoutState,
232 _: &mut Self::PrepaintState,
233 window: &mut Window,
234 cx: &mut App,
235 ) {
236 element.element.paint(window, cx);
237 }
238}
239
240pub(super) fn resolve_component_style(
247 theme: &crate::Theme,
248 legacy: TextViewStyle,
249) -> gpui_base::TextViewStyle {
250 let themed = super::base_text_view_style(theme);
251
252 let refined = |mut base: gpui::StyleRefinement, overlay: &StyleRefinement| {
253 base.refine(overlay);
254 base
255 };
256 let code_block = refined(themed.code_block().clone(), &legacy.code_block);
257 let table = refined(themed.table().clone(), &legacy.table);
258 let table_head = refined(themed.table_head().clone(), &legacy.table_head);
259 let table_cell = refined(themed.table_cell().clone(), &legacy.table_cell);
260
261 let mut inline_code = themed.inline_code();
262 refine_highlight_style(&mut inline_code, legacy.inline_code);
263
264 let is_dark = themed.is_dark() || legacy.is_dark;
268
269 let mut style = themed
270 .with_paragraph_gap(legacy.paragraph_gap)
271 .with_heading_base_font_size(legacy.heading_base_font_size)
272 .with_code_block(code_block)
273 .with_table(table)
274 .with_table_head(table_head)
275 .with_table_cell(table_cell)
276 .with_inline_code(inline_code)
277 .with_dark(is_dark);
278 if let Some(heading_font_size) = legacy.heading_font_size {
279 style = style.with_heading_font_size(move |level, base| heading_font_size(level, base));
280 }
281 style
282}
283
284fn refine_highlight_style(style: &mut HighlightStyle, refinement: HighlightStyle) {
285 if refinement.color.is_some() {
286 style.color = refinement.color;
287 }
288 if refinement.font_weight.is_some() {
289 style.font_weight = refinement.font_weight;
290 }
291 if refinement.font_style.is_some() {
292 style.font_style = refinement.font_style;
293 }
294 if refinement.background_color.is_some() {
295 style.background_color = refinement.background_color;
296 }
297 if refinement.underline.is_some() {
298 style.underline = refinement.underline;
299 }
300 if refinement.strikethrough.is_some() {
301 style.strikethrough = refinement.strikethrough;
302 }
303 if refinement.fade_out.is_some() {
304 style.fade_out = refinement.fade_out;
305 }
306}
307
308pub trait TextViewPlugin {
310 fn setup(self, text_view: TextView) -> TextView;
312}
313impl<P> TextViewPlugin for P
314where
315 P: MarkdownPlugin,
316{
317 fn setup(self, mut text_view: TextView) -> TextView {
318 text_view.inner = text_view.inner.plugin(self);
319 text_view
320 }
321}
322
323#[derive(IntoElement, Clone)]
325pub enum Text {
326 String(SharedString),
327 TextView(Box<TextView>),
328}
329impl From<SharedString> for Text {
330 fn from(value: SharedString) -> Self {
331 Self::String(value)
332 }
333}
334impl From<String> for Text {
335 fn from(value: String) -> Self {
336 Self::String(value.into())
337 }
338}
339impl From<&str> for Text {
340 fn from(value: &str) -> Self {
341 Self::String(value.to_string().into())
342 }
343}
344impl From<TextView> for Text {
345 fn from(value: TextView) -> Self {
346 Self::TextView(Box::new(value))
347 }
348}
349impl Text {
350 pub fn style(self, style: TextViewStyle) -> Self {
352 match self {
353 Self::String(value) => Self::String(value),
354 Self::TextView(view) => Self::TextView(Box::new(view.style(style))),
355 }
356 }
357 pub(crate) fn get_text(&self, cx: &App) -> SharedString {
358 match self {
359 Self::String(value) => value.clone(),
360 Self::TextView(view) => gpui_base::Text::from(view.inner.clone()).get_text(cx),
361 }
362 }
363}
364impl RenderOnce for Text {
365 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
366 match self {
367 Self::String(value) => value.into_any_element(),
368 Self::TextView(view) => view.into_any_element(),
369 }
370 }
371}
372
373#[track_caller]
375pub fn markdown(source: impl Into<SharedString>) -> TextView {
376 TextView::markdown(
377 ElementId::CodeLocation(*std::panic::Location::caller()),
378 source,
379 )
380}
381#[track_caller]
383pub fn html(source: impl Into<SharedString>) -> TextView {
384 TextView::html(
385 ElementId::CodeLocation(*std::panic::Location::caller()),
386 source,
387 )
388}
389
390#[cfg(test)]
391mod tests {
392 use std::sync::{
393 Arc,
394 atomic::{AtomicUsize, Ordering},
395 };
396
397 use gpui::{
398 Context, IntoElement, ParentElement as _, Render, TestAppContext, VisualTestContext, div,
399 };
400
401 struct StatelessMarkdown {
402 renders: Arc<AtomicUsize>,
403 }
404
405 impl Render for StatelessMarkdown {
406 fn render(&mut self, _: &mut gpui::Window, _: &mut Context<Self>) -> impl IntoElement {
407 self.renders.fetch_add(1, Ordering::Relaxed);
408 div().child(
409 super::markdown(include_str!("../../../story/examples/fixtures/test.md"))
410 .markdown_block_parser(|_, _| None),
411 )
412 }
413 }
414
415 #[gpui::test]
416 fn stateless_markdown_facade_settles_after_parsing(cx: &mut TestAppContext) {
417 cx.update(crate::init);
418 let renders = Arc::new(AtomicUsize::new(0));
419 let (_, cx) = cx.add_window_view({
420 let renders = renders.clone();
421 move |_, _| StatelessMarkdown { renders }
422 });
423 let cx: &mut VisualTestContext = cx;
424
425 cx.run_until_parked();
426 assert!(
427 renders.load(Ordering::Relaxed) <= 2,
428 "an unchanged compatibility TextView must settle after its parse, but rendered {} times",
429 renders.load(Ordering::Relaxed),
430 );
431 }
432}