1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::Poll;
5use std::time::Duration;
6
7use gpui::prelude::FluentBuilder;
8use gpui::{
9 div, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
10 EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
11 KeyBinding, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels,
12 Point, RenderOnce, SharedString, Size, Styled, Timer, Window,
13};
14use smol::stream::StreamExt;
15
16use crate::highlighter::HighlightTheme;
17use crate::{
18 global_state::GlobalState,
19 input::{self},
20 text::{
21 node::{self, NodeContext},
22 TextViewStyle,
23 },
24};
25use crate::{v_flex, ActiveTheme};
26
27const CONTEXT: &'static str = "TextView";
28
29pub(crate) fn init(cx: &mut App) {
30 cx.bind_keys(vec![
31 #[cfg(target_os = "macos")]
32 KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
33 #[cfg(not(target_os = "macos"))]
34 KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
35 ]);
36}
37
38#[derive(IntoElement, Clone)]
39struct TextViewElement {
40 state: Entity<TextViewState>,
41}
42
43impl RenderOnce for TextViewElement {
44 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
45 self.state.update(cx, |state, cx| {
46 div().map(|this| match &mut state.parsed_result {
47 Some(Ok(content)) => this.child(content.root_node.render(
48 None,
49 true,
50 true,
51 &content.node_cx,
52 window,
53 cx,
54 )),
55 Some(Err(err)) => this.child(
56 v_flex()
57 .gap_1()
58 .child("Failed to parse content")
59 .child(err.to_string()),
60 ),
61 None => this,
62 })
63 })
64 }
65}
66
67#[derive(Clone)]
84pub struct TextView {
85 id: ElementId,
86 init_state: Option<InitState>,
87 state: Entity<TextViewState>,
88 selectable: bool,
89}
90
91#[derive(PartialEq)]
92pub(crate) struct ParsedContent {
93 pub(crate) root_node: node::Node,
94 pub(crate) node_cx: node::NodeContext,
95}
96
97#[derive(Clone, Copy, PartialEq, Eq)]
99enum TextViewType {
100 Markdown,
102 Html,
104}
105
106enum Update {
107 Text(SharedString),
108 Style(Box<TextViewStyle>),
109}
110
111struct UpdateFuture {
112 type_: TextViewType,
113 highlight_theme: Arc<HighlightTheme>,
114 current_style: TextViewStyle,
115 current_text: SharedString,
116 timer: Timer,
117 rx: smol::channel::Receiver<Update>,
118 tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
119 delay: Duration,
120}
121
122impl UpdateFuture {
123 fn new(
124 type_: TextViewType,
125 style: TextViewStyle,
126 text: SharedString,
127 highlight_theme: Arc<HighlightTheme>,
128 rx: smol::channel::Receiver<Update>,
129 tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
130 delay: Duration,
131 ) -> Self {
132 Self {
133 type_,
134 highlight_theme,
135 current_style: style,
136 current_text: text,
137 timer: Timer::never(),
138 rx,
139 tx_result,
140 delay,
141 }
142 }
143}
144
145impl Future for UpdateFuture {
146 type Output = ();
147
148 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
149 loop {
150 match self.rx.poll_next(cx) {
151 Poll::Ready(Some(update)) => {
152 let changed = match update {
153 Update::Text(text) if self.current_text != text => {
154 self.current_text = text;
155 true
156 }
157 Update::Style(style) if self.current_style != *style => {
158 self.current_style = *style;
159 true
160 }
161 _ => false,
162 };
163 if changed {
164 let delay = self.delay;
165 self.timer.set_after(delay);
166 }
167 continue;
168 }
169 Poll::Ready(None) => return Poll::Ready(()),
170 Poll::Pending => {}
171 }
172
173 match self.timer.poll_next(cx) {
174 Poll::Ready(Some(_)) => {
175 let res = parse_content(
176 self.type_,
177 &self.current_text,
178 self.current_style.clone(),
179 &self.highlight_theme,
180 );
181 _ = self.tx_result.try_send(res);
182 continue;
183 }
184 Poll::Ready(None) | Poll::Pending => return Poll::Pending,
185 }
186 }
187 }
188}
189
190#[derive(Clone)]
191enum InitState {
192 Initializing {
193 type_: TextViewType,
194 text: SharedString,
195 style: Box<TextViewStyle>,
196 highlight_theme: Arc<HighlightTheme>,
197 },
198 Initialized {
199 tx: smol::channel::Sender<Update>,
200 },
201}
202
203pub(crate) struct TextViewState {
204 parent_entity: Option<EntityId>,
205 tx: Option<smol::channel::Sender<Update>>,
206 parsed_result: Option<Result<ParsedContent, SharedString>>,
207 focus_handle: Option<FocusHandle>,
208
209 bounds: Bounds<Pixels>,
211 selection_positions: (Option<Point<Pixels>>, Option<Point<Pixels>>),
213 is_selecting: bool,
215 is_selectable: bool,
216}
217
218impl TextViewState {
219 fn new(cx: &mut Context<TextViewState>) -> Self {
220 let focus_handle = cx.focus_handle();
221 Self {
222 parent_entity: None,
223 tx: None,
224 parsed_result: None,
225 focus_handle: Some(focus_handle),
226 bounds: Bounds::default(),
227 selection_positions: (None, None),
228 is_selecting: false,
229 is_selectable: false,
230 }
231 }
232}
233
234impl TextViewState {
235 fn update_bounds(&mut self, bounds: Bounds<Pixels>) {
237 if self.bounds.size != bounds.size {
238 self.clear_selection();
239 }
240 self.bounds = bounds;
241 }
242
243 fn clear_selection(&mut self) {
244 self.selection_positions = (None, None);
245 self.is_selecting = false;
246 }
247
248 fn start_selection(&mut self, pos: Point<Pixels>) {
249 let pos = pos - self.bounds.origin;
250 self.selection_positions = (Some(pos), Some(pos));
251 self.is_selecting = true;
252 }
253
254 fn update_selection(&mut self, pos: Point<Pixels>) {
255 let pos = pos - self.bounds.origin;
256 if let (Some(start), Some(_)) = self.selection_positions {
257 self.selection_positions = (Some(start), Some(pos))
258 }
259 }
260
261 fn end_selection(&mut self) {
262 self.is_selecting = false;
263 }
264
265 pub(crate) fn has_selection(&self) -> bool {
266 if let (Some(start), Some(end)) = self.selection_positions {
267 start != end
268 } else {
269 false
270 }
271 }
272
273 pub(crate) fn is_selectable(&self) -> bool {
274 self.is_selectable
275 }
276
277 pub(crate) fn selection_bounds(&self) -> Bounds<Pixels> {
279 selection_bounds(
280 self.selection_positions.0,
281 self.selection_positions.1,
282 self.bounds,
283 )
284 }
285
286 fn selection_text(&self) -> Option<String> {
287 Some(
288 self.parsed_result
289 .as_ref()?
290 .as_ref()
291 .ok()?
292 .root_node
293 .selected_text(),
294 )
295 }
296}
297
298#[derive(IntoElement, Clone)]
299pub enum Text {
300 String(SharedString),
301 TextView(Box<TextView>),
302}
303
304impl From<SharedString> for Text {
305 fn from(s: SharedString) -> Self {
306 Self::String(s)
307 }
308}
309
310impl From<&str> for Text {
311 fn from(s: &str) -> Self {
312 Self::String(SharedString::from(s.to_string()))
313 }
314}
315
316impl From<String> for Text {
317 fn from(s: String) -> Self {
318 Self::String(s.into())
319 }
320}
321
322impl From<TextView> for Text {
323 fn from(e: TextView) -> Self {
324 Self::TextView(Box::new(e))
325 }
326}
327
328impl Text {
329 pub fn style(self, style: TextViewStyle) -> Self {
333 match self {
334 Self::String(s) => Self::String(s),
335 Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
336 }
337 }
338}
339
340impl RenderOnce for Text {
341 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
342 match self {
343 Self::String(s) => s.into_any_element(),
344 Self::TextView(e) => e.into_any_element(),
345 }
346 }
347}
348
349impl TextView {
350 fn create_init_state(
351 type_: TextViewType,
352 text: &SharedString,
353 highlight_theme: &Arc<HighlightTheme>,
354 state: &Entity<TextViewState>,
355 cx: &mut App,
356 ) -> InitState {
357 let state = state.read(cx);
358 if let Some(tx) = &state.tx {
359 InitState::Initialized { tx: tx.clone() }
360 } else {
361 InitState::Initializing {
362 type_,
363 text: text.clone(),
364 style: Default::default(),
365 highlight_theme: highlight_theme.clone(),
366 }
367 }
368 }
369
370 pub fn markdown(
372 id: impl Into<ElementId>,
373 markdown: impl Into<SharedString>,
374 window: &mut Window,
375 cx: &mut App,
376 ) -> Self {
377 let id: ElementId = id.into();
378 let markdown = markdown.into();
379 let highlight_theme = cx.theme().highlight_theme.clone();
380 let state =
381 window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
382 TextViewState::new(cx)
383 });
384 let init_state = Self::create_init_state(
385 TextViewType::Markdown,
386 &markdown,
387 &highlight_theme,
388 &state,
389 cx,
390 );
391 if let Some(tx) = &state.read(cx).tx {
392 let _ = tx.try_send(Update::Text(markdown));
393 }
394 Self {
395 id,
396 init_state: Some(init_state),
397 state,
398 selectable: false,
399 }
400 }
401
402 pub fn html(
404 id: impl Into<ElementId>,
405 html: impl Into<SharedString>,
406 window: &mut Window,
407 cx: &mut App,
408 ) -> Self {
409 let id: ElementId = id.into();
410 let html = html.into();
411 let highlight_theme = cx.theme().highlight_theme.clone();
412 let state =
413 window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
414 TextViewState::new(cx)
415 });
416 let init_state =
417 Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
418 if let Some(tx) = &state.read(cx).tx {
419 let _ = tx.try_send(Update::Text(html));
420 }
421 Self {
422 id,
423 init_state: Some(init_state),
424 state,
425 selectable: false,
426 }
427 }
428
429 pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
431 if let Some(init_state) = &mut self.init_state {
432 match init_state {
433 InitState::Initializing { text, .. } => *text = raw.into(),
434 InitState::Initialized { tx } => {
435 let _ = tx.try_send(Update::Text(raw.into()));
436 }
437 }
438 }
439
440 self
441 }
442
443 pub fn style(mut self, style: TextViewStyle) -> Self {
445 if let Some(init_state) = &mut self.init_state {
446 match init_state {
447 InitState::Initializing { style: s, .. } => *s = Box::new(style),
448 InitState::Initialized { tx } => {
449 let _ = tx.try_send(Update::Style(Box::new(style)));
450 }
451 }
452 }
453 self
454 }
455
456 pub fn selectable(mut self) -> Self {
458 self.selectable = true;
459 self
460 }
461
462 fn on_action_copy(state: &Entity<TextViewState>, cx: &mut App) {
463 let Some(selected_text) = state.read(cx).selection_text() else {
464 return;
465 };
466
467 cx.write_to_clipboard(ClipboardItem::new_string(selected_text.trim().to_string()));
468 }
469}
470
471impl IntoElement for TextView {
472 type Element = Self;
473
474 fn into_element(self) -> Self::Element {
475 self
476 }
477}
478
479impl Element for TextView {
480 type RequestLayoutState = AnyElement;
481 type PrepaintState = ();
482
483 fn id(&self) -> Option<ElementId> {
484 Some(self.id.clone())
485 }
486
487 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
488 None
489 }
490
491 fn request_layout(
492 &mut self,
493 _: Option<&GlobalElementId>,
494 _: Option<&InspectorElementId>,
495 window: &mut Window,
496 cx: &mut App,
497 ) -> (LayoutId, Self::RequestLayoutState) {
498 if let Some(InitState::Initializing {
499 type_,
500 text,
501 style,
502 highlight_theme,
503 }) = self.init_state.take()
504 {
505 let style = *style;
506 let highlight_theme = highlight_theme.clone();
507 let (tx, rx) = smol::channel::unbounded::<Update>();
508 let (tx_result, rx_result) =
509 smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
510 let parsed_result = parse_content(type_, &text, style.clone(), &highlight_theme);
511
512 self.state.update(cx, {
513 let tx = tx.clone();
514 |state, _| {
515 state.parsed_result = Some(parsed_result);
516 state.tx = Some(tx);
517 }
518 });
519
520 cx.spawn({
521 let state = self.state.downgrade();
522 async move |cx| {
523 while let Ok(parsed_result) = rx_result.recv().await {
524 if let Some(state) = state.upgrade() {
525 _ = state.update(cx, |state, cx| {
526 state.parsed_result = Some(parsed_result);
527 if let Some(parent_entity) = state.parent_entity {
528 let app = &mut **cx;
529 app.notify(parent_entity);
530 }
531 state.clear_selection();
532 });
533 } else {
534 break;
536 }
537 }
538 }
539 })
540 .detach();
541
542 cx.background_spawn(UpdateFuture::new(
543 type_,
544 style,
545 text,
546 highlight_theme,
547 rx,
548 tx_result,
549 Duration::from_millis(200),
550 ))
551 .detach();
552
553 self.init_state = Some(InitState::Initialized { tx });
554 }
555
556 let focus_handle = self
557 .state
558 .read(cx)
559 .focus_handle
560 .as_ref()
561 .expect("focus_handle should init by TextViewState::new");
562
563 let mut el = div()
564 .key_context(CONTEXT)
565 .track_focus(focus_handle)
566 .on_action({
567 let state = self.state.clone();
568 move |_: &input::Copy, _, cx| {
569 Self::on_action_copy(&state, cx);
570 }
571 })
572 .child(TextViewElement {
573 state: self.state.clone(),
574 })
575 .into_any_element();
576 let layout_id = el.request_layout(window, cx);
577 (layout_id, el)
578 }
579
580 fn prepaint(
581 &mut self,
582 _: Option<&GlobalElementId>,
583 _: Option<&InspectorElementId>,
584 _: Bounds<Pixels>,
585 request_layout: &mut Self::RequestLayoutState,
586 window: &mut Window,
587 cx: &mut App,
588 ) -> Self::PrepaintState {
589 request_layout.prepaint(window, cx);
590 }
591
592 fn paint(
593 &mut self,
594 _: Option<&GlobalElementId>,
595 _: Option<&InspectorElementId>,
596 bounds: Bounds<Pixels>,
597 request_layout: &mut Self::RequestLayoutState,
598 _: &mut Self::PrepaintState,
599 window: &mut Window,
600 cx: &mut App,
601 ) {
602 let entity_id = window.current_view();
603 let is_selectable = self.selectable;
604
605 self.state.update(cx, |state, _| {
606 state.parent_entity = Some(entity_id);
607 state.update_bounds(bounds);
608 state.is_selectable = is_selectable;
609 });
610
611 GlobalState::global_mut(cx)
612 .text_view_state_stack
613 .push(self.state.clone());
614 request_layout.paint(window, cx);
615 GlobalState::global_mut(cx).text_view_state_stack.pop();
616
617 if self.selectable {
618 let is_selecting = self.state.read(cx).is_selecting;
619 let has_selection = self.state.read(cx).has_selection();
620
621 window.on_mouse_event({
622 let state = self.state.clone();
623 move |event: &MouseDownEvent, phase, _, cx| {
624 if !bounds.contains(&event.position) || !phase.bubble() {
625 return;
626 }
627
628 state.update(cx, |state, _| {
629 state.start_selection(event.position);
630 });
631 cx.notify(entity_id);
632 }
633 });
634
635 if is_selecting {
636 window.on_mouse_event({
638 let state = self.state.clone();
639 move |event: &MouseMoveEvent, _, _, cx| {
640 state.update(cx, |state, _| {
641 state.update_selection(event.position);
642 });
643 cx.notify(entity_id);
644 }
645 });
646
647 window.on_mouse_event({
649 let state = self.state.clone();
650 move |_: &MouseUpEvent, _, _, cx| {
651 state.update(cx, |state, _| {
652 state.end_selection();
653 });
654 cx.notify(entity_id);
655 }
656 });
657 }
658
659 if has_selection {
660 window.on_mouse_event({
662 let state = self.state.clone();
663 move |event: &MouseDownEvent, _, _, cx| {
664 if bounds.contains(&event.position) {
665 return;
666 }
667
668 state.update(cx, |state, _| {
669 state.clear_selection();
670 });
671 cx.notify(entity_id);
672 }
673 });
674 }
675 }
676 }
677}
678
679fn parse_content(
680 type_: TextViewType,
681 text: &str,
682 style: TextViewStyle,
683 highlight_theme: &HighlightTheme,
684) -> Result<ParsedContent, SharedString> {
685 let mut node_cx = NodeContext {
686 style: style.clone(),
687 ..NodeContext::default()
688 };
689
690 let res = match type_ {
691 TextViewType::Markdown => {
692 super::format::markdown::parse(text, &style, &mut node_cx, highlight_theme)
693 }
694 TextViewType::Html => super::format::html::parse(text, &mut node_cx),
695 };
696 res.map(move |root_node| ParsedContent { root_node, node_cx })
697}
698
699fn selection_bounds(
700 start: Option<Point<Pixels>>,
701 end: Option<Point<Pixels>>,
702 bounds: Bounds<Pixels>,
703) -> Bounds<Pixels> {
704 if let (Some(start), Some(end)) = (start, end) {
705 let start = start + bounds.origin;
706 let end = end + bounds.origin;
707
708 let origin = Point {
709 x: start.x.min(end.x),
710 y: start.y.min(end.y),
711 };
712 let size = Size {
713 width: (start.x - end.x).abs(),
714 height: (start.y - end.y).abs(),
715 };
716
717 return Bounds { origin, size };
718 }
719
720 Bounds::default()
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use gpui::{point, px, size, Bounds};
727
728 #[test]
729 fn test_text_view_state_selection_bounds() {
730 assert_eq!(
731 selection_bounds(None, None, Default::default()),
732 Bounds::default()
733 );
734 assert_eq!(
735 selection_bounds(None, Some(point(px(10.), px(20.))), Default::default()),
736 Bounds::default()
737 );
738 assert_eq!(
739 selection_bounds(Some(point(px(10.), px(20.))), None, Default::default()),
740 Bounds::default()
741 );
742
743 assert_eq!(
749 selection_bounds(
750 Some(point(px(10.), px(10.))),
751 Some(point(px(50.), px(50.))),
752 Default::default()
753 ),
754 Bounds {
755 origin: point(px(10.), px(10.)),
756 size: size(px(40.), px(40.))
757 }
758 );
759 assert_eq!(
765 selection_bounds(
766 Some(point(px(50.), px(50.))),
767 Some(point(px(10.), px(10.))),
768 Default::default()
769 ),
770 Bounds {
771 origin: point(px(10.), px(10.)),
772 size: size(px(40.), px(40.))
773 }
774 );
775 assert_eq!(
781 selection_bounds(
782 Some(point(px(50.), px(10.))),
783 Some(point(px(10.), px(50.))),
784 Default::default()
785 ),
786 Bounds {
787 origin: point(px(10.), px(10.)),
788 size: size(px(40.), px(40.))
789 }
790 );
791 assert_eq!(
797 selection_bounds(
798 Some(point(px(10.), px(50.))),
799 Some(point(px(50.), px(10.))),
800 Default::default()
801 ),
802 Bounds {
803 origin: point(px(10.), px(10.)),
804 size: size(px(40.), px(40.))
805 }
806 );
807 }
808}