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