gpui_component/text/
text_view.rs

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    AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
10    EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
11    KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
12    Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer, Window, div, px,
13};
14use smol::stream::StreamExt;
15
16use crate::highlighter::HighlightTheme;
17use crate::scroll::ScrollableElement;
18use crate::text::node::CodeBlock;
19use crate::{ActiveTheme, StyledExt, v_flex};
20use crate::{
21    global_state::GlobalState,
22    input::{self},
23    text::{
24        TextViewStyle,
25        node::{self, NodeContext},
26    },
27};
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/// Type for code block actions generator function.
71pub(crate) type CodeBlockActionsFn =
72    dyn Fn(&CodeBlock, &mut Window, &mut App) -> AnyElement + Send + Sync;
73
74/// A text view that can render Markdown or HTML.
75///
76/// ## Goals
77///
78/// - Provide a rich text rendering component for such as Markdown or HTML,
79/// used to display rich text in GPUI application (e.g., Help messages, Release notes)
80/// - Support Markdown GFM and HTML (Simple HTML like Safari Reader Mode) for showing most common used markups.
81/// - Support Heading, Paragraph, Bold, Italic, StrikeThrough, Code, Link, Image, Blockquote, List, Table, HorizontalRule, CodeBlock ...
82///
83/// ## Not Goals
84///
85/// - Customization of the complex style (some simple styles will be supported)
86/// - As a Markdown editor or viewer (If you want to like this, you must fork your version).
87/// - As a HTML viewer, we not support CSS, we only support basic HTML tags for used to as a content reader.
88///
89/// See also [`MarkdownElement`], [`HtmlElement`]
90#[derive(Clone)]
91pub struct TextView {
92    id: ElementId,
93    init_state: Option<InitState>,
94    raw: SharedString,
95    state: Entity<TextViewState>,
96    style: StyleRefinement,
97    selectable: bool,
98    scrollable: bool,
99    code_block_actions: Option<Arc<CodeBlockActionsFn>>,
100}
101
102#[derive(PartialEq)]
103pub(crate) struct ParsedContent {
104    pub(crate) root_node: node::Node,
105    pub(crate) node_cx: node::NodeContext,
106}
107
108/// The type of the text view.
109#[derive(Clone, Copy, PartialEq, Eq)]
110enum TextViewType {
111    /// Markdown view
112    Markdown,
113    /// HTML view
114    Html,
115}
116
117enum Update {
118    Text(SharedString),
119    Style(Box<TextViewStyle>),
120}
121
122struct UpdateFuture {
123    type_: TextViewType,
124    highlight_theme: Arc<HighlightTheme>,
125    current_style: TextViewStyle,
126    current_text: SharedString,
127    timer: Timer,
128    rx: Pin<Box<smol::channel::Receiver<Update>>>,
129    tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
130    delay: Duration,
131    code_block_actions: Option<Arc<CodeBlockActionsFn>>,
132}
133
134impl UpdateFuture {
135    #[allow(clippy::too_many_arguments)]
136    fn new(
137        type_: TextViewType,
138        style: TextViewStyle,
139        text: SharedString,
140        highlight_theme: Arc<HighlightTheme>,
141        rx: smol::channel::Receiver<Update>,
142        tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
143        delay: Duration,
144        code_block_actions: Option<Arc<CodeBlockActionsFn>>,
145    ) -> Self {
146        Self {
147            type_,
148            highlight_theme,
149            current_style: style,
150            current_text: text,
151            timer: Timer::never(),
152            rx: Box::pin(rx),
153            tx_result,
154            delay,
155            code_block_actions,
156        }
157    }
158}
159
160impl Future for UpdateFuture {
161    type Output = ();
162
163    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
164        loop {
165            match self.rx.poll_next(cx) {
166                Poll::Ready(Some(update)) => {
167                    let changed = match update {
168                        Update::Text(text) if self.current_text != text => {
169                            self.current_text = text;
170                            true
171                        }
172                        Update::Style(style) if self.current_style != *style => {
173                            self.current_style = *style;
174                            true
175                        }
176                        _ => false,
177                    };
178                    if changed {
179                        let delay = self.delay;
180                        self.timer.set_after(delay);
181                    }
182                    continue;
183                }
184                Poll::Ready(None) => return Poll::Ready(()),
185                Poll::Pending => {}
186            }
187
188            match self.timer.poll_next(cx) {
189                Poll::Ready(Some(_)) => {
190                    let res = parse_content(
191                        self.type_,
192                        &self.current_text,
193                        self.current_style.clone(),
194                        &self.highlight_theme,
195                        &self.code_block_actions.clone(),
196                    );
197                    _ = self.tx_result.try_send(res);
198                    continue;
199                }
200                Poll::Ready(None) | Poll::Pending => return Poll::Pending,
201            }
202        }
203    }
204}
205
206#[derive(Clone)]
207enum InitState {
208    Initializing {
209        type_: TextViewType,
210        text: SharedString,
211        style: Box<TextViewStyle>,
212        highlight_theme: Arc<HighlightTheme>,
213    },
214    Initialized {
215        tx: smol::channel::Sender<Update>,
216    },
217}
218
219pub(crate) struct TextViewState {
220    parent_entity: Option<EntityId>,
221    tx: Option<smol::channel::Sender<Update>>,
222    parsed_result: Option<Result<ParsedContent, SharedString>>,
223    focus_handle: Option<FocusHandle>,
224    /// The bounds of the text view
225    bounds: Bounds<Pixels>,
226    /// The local (in TextView) position of the selection.
227    selection_positions: (Option<Point<Pixels>>, Option<Point<Pixels>>),
228    /// Is current in selection.
229    is_selecting: bool,
230    is_selectable: bool,
231    list_state: ListState,
232}
233
234impl TextViewState {
235    fn new(cx: &mut Context<TextViewState>) -> Self {
236        let focus_handle = cx.focus_handle();
237        Self {
238            parent_entity: None,
239            tx: None,
240            parsed_result: None,
241            focus_handle: Some(focus_handle),
242            bounds: Bounds::default(),
243            selection_positions: (None, None),
244            is_selecting: false,
245            is_selectable: false,
246            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
247        }
248    }
249}
250
251impl TextViewState {
252    /// Save bounds and unselect if bounds changed.
253    fn update_bounds(&mut self, bounds: Bounds<Pixels>) {
254        if self.bounds.size != bounds.size {
255            self.clear_selection();
256        }
257        self.bounds = bounds;
258    }
259
260    fn clear_selection(&mut self) {
261        self.selection_positions = (None, None);
262        self.is_selecting = false;
263    }
264
265    fn start_selection(&mut self, pos: Point<Pixels>) {
266        let pos = pos - self.bounds.origin;
267        self.selection_positions = (Some(pos), Some(pos));
268        self.is_selecting = true;
269    }
270
271    fn update_selection(&mut self, pos: Point<Pixels>) {
272        let pos = pos - self.bounds.origin;
273        if let (Some(start), Some(_)) = self.selection_positions {
274            self.selection_positions = (Some(start), Some(pos))
275        }
276    }
277
278    fn end_selection(&mut self) {
279        self.is_selecting = false;
280    }
281
282    pub(crate) fn has_selection(&self) -> bool {
283        if let (Some(start), Some(end)) = self.selection_positions {
284            start != end
285        } else {
286            false
287        }
288    }
289
290    pub(crate) fn is_selectable(&self) -> bool {
291        self.is_selectable
292    }
293
294    /// Return the bounds of the selection in window coordinates.
295    pub(crate) fn selection_bounds(&self) -> Bounds<Pixels> {
296        selection_bounds(
297            self.selection_positions.0,
298            self.selection_positions.1,
299            self.bounds,
300        )
301    }
302
303    fn selection_text(&self) -> Option<String> {
304        Some(
305            self.parsed_result
306                .as_ref()?
307                .as_ref()
308                .ok()?
309                .root_node
310                .selected_text(),
311        )
312    }
313}
314
315#[derive(IntoElement, Clone)]
316pub enum Text {
317    String(SharedString),
318    TextView(Box<TextView>),
319}
320
321impl From<SharedString> for Text {
322    fn from(s: SharedString) -> Self {
323        Self::String(s)
324    }
325}
326
327impl From<&str> for Text {
328    fn from(s: &str) -> Self {
329        Self::String(SharedString::from(s.to_string()))
330    }
331}
332
333impl From<String> for Text {
334    fn from(s: String) -> Self {
335        Self::String(s.into())
336    }
337}
338
339impl From<TextView> for Text {
340    fn from(e: TextView) -> Self {
341        Self::TextView(Box::new(e))
342    }
343}
344
345impl Text {
346    /// Set the style for [`TextView`].
347    ///
348    /// Do nothing if this is `String`.
349    pub fn style(self, style: TextViewStyle) -> Self {
350        match self {
351            Self::String(s) => Self::String(s),
352            Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
353        }
354    }
355
356    /// Get the str
357    pub fn as_str(&self) -> &str {
358        match self {
359            Self::String(s) => s.as_str(),
360            Self::TextView(view) => view.raw.as_str(),
361        }
362    }
363}
364
365impl RenderOnce for Text {
366    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
367        match self {
368            Self::String(s) => s.into_any_element(),
369            Self::TextView(e) => e.into_any_element(),
370        }
371    }
372}
373
374impl Styled for TextView {
375    fn style(&mut self) -> &mut StyleRefinement {
376        &mut self.style
377    }
378}
379
380impl TextView {
381    fn create_init_state(
382        type_: TextViewType,
383        text: &SharedString,
384        highlight_theme: &Arc<HighlightTheme>,
385        state: &Entity<TextViewState>,
386        cx: &mut App,
387    ) -> InitState {
388        let state = state.read(cx);
389        if let Some(tx) = &state.tx {
390            InitState::Initialized { tx: tx.clone() }
391        } else {
392            InitState::Initializing {
393                type_,
394                text: text.clone(),
395                style: Default::default(),
396                highlight_theme: highlight_theme.clone(),
397            }
398        }
399    }
400
401    /// Create a new markdown text view.
402    pub fn markdown(
403        id: impl Into<ElementId>,
404        markdown: impl Into<SharedString>,
405        window: &mut Window,
406        cx: &mut App,
407    ) -> Self {
408        let id: ElementId = id.into();
409        let markdown = markdown.into();
410        let highlight_theme = cx.theme().highlight_theme.clone();
411        let state =
412            window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
413                TextViewState::new(cx)
414            });
415        let init_state = Self::create_init_state(
416            TextViewType::Markdown,
417            &markdown,
418            &highlight_theme,
419            &state,
420            cx,
421        );
422        if let Some(tx) = &state.read(cx).tx {
423            let _ = tx.try_send(Update::Text(markdown.clone()));
424        }
425        Self {
426            id,
427            init_state: Some(init_state),
428            raw: markdown.clone(),
429            style: StyleRefinement::default(),
430            state,
431            selectable: false,
432            scrollable: false,
433            code_block_actions: None,
434        }
435    }
436
437    /// Create a new html text view.
438    pub fn html(
439        id: impl Into<ElementId>,
440        html: impl Into<SharedString>,
441        window: &mut Window,
442        cx: &mut App,
443    ) -> Self {
444        let id: ElementId = id.into();
445        let html = html.into();
446        let highlight_theme = cx.theme().highlight_theme.clone();
447        let state =
448            window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
449                TextViewState::new(cx)
450            });
451        let init_state =
452            Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
453        if let Some(tx) = &state.read(cx).tx {
454            let _ = tx.try_send(Update::Text(html.clone()));
455        }
456        Self {
457            id,
458            init_state: Some(init_state),
459            style: StyleRefinement::default(),
460            state,
461            raw: html,
462            selectable: false,
463            scrollable: false,
464            code_block_actions: None,
465        }
466    }
467
468    /// Set the source text of the text view.
469    pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
470        let raw: SharedString = raw.into();
471        if let Some(init_state) = &mut self.init_state {
472            match init_state {
473                InitState::Initializing { text, .. } => *text = raw.clone(),
474                InitState::Initialized { tx } => {
475                    let _ = tx.try_send(Update::Text(raw.clone()));
476                }
477            }
478        }
479        self.raw = raw;
480        self
481    }
482
483    /// Set [`TextViewStyle`].
484    pub fn style(mut self, style: TextViewStyle) -> Self {
485        if let Some(init_state) = &mut self.init_state {
486            match init_state {
487                InitState::Initializing { style: s, .. } => *s = Box::new(style),
488                InitState::Initialized { tx } => {
489                    let _ = tx.try_send(Update::Style(Box::new(style)));
490                }
491            }
492        }
493        self
494    }
495
496    /// Set the text view to be selectable, default is false.
497    pub fn selectable(mut self, selectable: bool) -> Self {
498        self.selectable = selectable;
499        self
500    }
501
502    /// Set the text view to be scrollable, default is false.
503    ///
504    /// ## If true for `scrollable`
505    ///
506    /// The `scrollable` mode used for large content,
507    /// will show scrollbar, but requires the parent to have a fixed height,
508    /// and use [`gpui::list`] to render the content in a virtualized way.
509    ///
510    /// ## If false to fit content
511    ///
512    /// The TextView will expand to fit all content, no scrollbar.
513    /// This mode is suitable for small content, such as a few lines of text, a label, etc.
514    pub fn scrollable(mut self, scrollable: bool) -> Self {
515        self.scrollable = scrollable;
516        self
517    }
518
519    fn on_action_copy(state: &Entity<TextViewState>, cx: &mut App) {
520        let Some(selected_text) = state.read(cx).selection_text() else {
521            return;
522        };
523
524        cx.write_to_clipboard(ClipboardItem::new_string(selected_text.trim().to_string()));
525    }
526
527    /// Set custom block actions for code blocks.
528    ///
529    /// The closure receives the [`CodeBlock`],
530    /// and returns an element to display.
531    pub fn code_block_actions<F, E>(mut self, f: F) -> Self
532    where
533        F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static,
534        E: IntoElement,
535    {
536        self.code_block_actions = Some(Arc::new(move |code_block, window, cx| {
537            f(&code_block, window, cx).into_any_element()
538        }));
539        self
540    }
541}
542
543impl IntoElement for TextView {
544    type Element = Self;
545
546    fn into_element(self) -> Self::Element {
547        self
548    }
549}
550
551impl Element for TextView {
552    type RequestLayoutState = AnyElement;
553    type PrepaintState = ();
554
555    fn id(&self) -> Option<ElementId> {
556        Some(self.id.clone())
557    }
558
559    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
560        None
561    }
562
563    fn request_layout(
564        &mut self,
565        _: Option<&GlobalElementId>,
566        _: Option<&InspectorElementId>,
567        window: &mut Window,
568        cx: &mut App,
569    ) -> (LayoutId, Self::RequestLayoutState) {
570        if let Some(InitState::Initializing {
571            type_,
572            text,
573            style,
574            highlight_theme,
575        }) = self.init_state.take()
576        {
577            let style = *style;
578            let highlight_theme = highlight_theme.clone();
579            let code_block_actions = self.code_block_actions.clone();
580            let (tx, rx) = smol::channel::unbounded::<Update>();
581            let (tx_result, rx_result) =
582                smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
583            let parsed_result = parse_content(
584                type_,
585                &text,
586                style.clone(),
587                &highlight_theme,
588                &code_block_actions,
589            );
590
591            self.state.update(cx, {
592                let tx = tx.clone();
593                |state, _| {
594                    state.parsed_result = Some(parsed_result);
595                    state.tx = Some(tx);
596                }
597            });
598
599            cx.spawn({
600                let state = self.state.downgrade();
601                async move |cx| {
602                    while let Ok(parsed_result) = rx_result.recv().await {
603                        if let Some(state) = state.upgrade() {
604                            _ = state.update(cx, |state, cx| {
605                                state.parsed_result = Some(parsed_result);
606                                if let Some(parent_entity) = state.parent_entity {
607                                    let app = &mut **cx;
608                                    app.notify(parent_entity);
609                                }
610                                state.clear_selection();
611                            });
612                        } else {
613                            // state released, stopping processing
614                            break;
615                        }
616                    }
617                }
618            })
619            .detach();
620
621            cx.background_spawn(UpdateFuture::new(
622                type_,
623                style,
624                text,
625                highlight_theme,
626                rx,
627                tx_result,
628                Duration::from_millis(200),
629                code_block_actions,
630            ))
631            .detach();
632
633            self.init_state = Some(InitState::Initialized { tx });
634        }
635
636        let list_state = &self.state.read(cx).list_state;
637
638        let focus_handle = self
639            .state
640            .read(cx)
641            .focus_handle
642            .as_ref()
643            .expect("focus_handle should init by TextViewState::new");
644
645        let mut el = div()
646            .key_context(CONTEXT)
647            .track_focus(focus_handle)
648            .size_full()
649            .relative()
650            .on_action({
651                let state = self.state.clone();
652                move |_: &input::Copy, _, cx| {
653                    Self::on_action_copy(&state, cx);
654                }
655            })
656            .child(TextViewElement {
657                list_state: if self.scrollable {
658                    Some(list_state.clone())
659                } else {
660                    None
661                },
662                state: self.state.clone(),
663            })
664            .refine_style(&self.style)
665            .vertical_scrollbar(list_state)
666            .into_any_element();
667        let layout_id = el.request_layout(window, cx);
668        (layout_id, el)
669    }
670
671    fn prepaint(
672        &mut self,
673        _: Option<&GlobalElementId>,
674        _: Option<&InspectorElementId>,
675        _: Bounds<Pixels>,
676        request_layout: &mut Self::RequestLayoutState,
677        window: &mut Window,
678        cx: &mut App,
679    ) -> Self::PrepaintState {
680        request_layout.prepaint(window, cx);
681    }
682
683    fn paint(
684        &mut self,
685        _: Option<&GlobalElementId>,
686        _: Option<&InspectorElementId>,
687        bounds: Bounds<Pixels>,
688        request_layout: &mut Self::RequestLayoutState,
689        _: &mut Self::PrepaintState,
690        window: &mut Window,
691        cx: &mut App,
692    ) {
693        let entity_id = window.current_view();
694        let is_selectable = self.selectable;
695
696        self.state.update(cx, |state, _| {
697            state.parent_entity = Some(entity_id);
698            state.update_bounds(bounds);
699            state.is_selectable = is_selectable;
700        });
701
702        GlobalState::global_mut(cx)
703            .text_view_state_stack
704            .push(self.state.clone());
705        request_layout.paint(window, cx);
706        GlobalState::global_mut(cx).text_view_state_stack.pop();
707
708        if self.selectable {
709            let is_selecting = self.state.read(cx).is_selecting;
710            let has_selection = self.state.read(cx).has_selection();
711
712            window.on_mouse_event({
713                let state = self.state.clone();
714                move |event: &MouseDownEvent, phase, _, cx| {
715                    if !bounds.contains(&event.position) || !phase.bubble() {
716                        return;
717                    }
718
719                    state.update(cx, |state, _| {
720                        state.start_selection(event.position);
721                    });
722                    cx.notify(entity_id);
723                }
724            });
725
726            if is_selecting {
727                // move to update end position.
728                window.on_mouse_event({
729                    let state = self.state.clone();
730                    move |event: &MouseMoveEvent, phase, _, cx| {
731                        if !phase.bubble() {
732                            return;
733                        }
734
735                        state.update(cx, |state, _| {
736                            state.update_selection(event.position);
737                        });
738                        cx.notify(entity_id);
739                    }
740                });
741
742                // up to end selection
743                window.on_mouse_event({
744                    let state = self.state.clone();
745                    move |_: &MouseUpEvent, phase, _, cx| {
746                        if !phase.bubble() {
747                            return;
748                        }
749
750                        state.update(cx, |state, _| {
751                            state.end_selection();
752                        });
753                        cx.notify(entity_id);
754                    }
755                });
756            }
757
758            if has_selection {
759                // down outside to clear selection
760                window.on_mouse_event({
761                    let state = self.state.clone();
762                    move |event: &MouseDownEvent, _, _, cx| {
763                        if bounds.contains(&event.position) {
764                            return;
765                        }
766
767                        state.update(cx, |state, _| {
768                            state.clear_selection();
769                        });
770                        cx.notify(entity_id);
771                    }
772                });
773            }
774        }
775    }
776}
777
778fn parse_content(
779    type_: TextViewType,
780    text: &str,
781    style: TextViewStyle,
782    highlight_theme: &HighlightTheme,
783    code_block_actions: &Option<Arc<CodeBlockActionsFn>>,
784) -> Result<ParsedContent, SharedString> {
785    let mut node_cx = NodeContext {
786        style: style.clone(),
787        code_block_actions: code_block_actions.clone(),
788        ..NodeContext::default()
789    };
790
791    let res = match type_ {
792        TextViewType::Markdown => {
793            super::format::markdown::parse(text, &style, &mut node_cx, highlight_theme)
794        }
795        TextViewType::Html => super::format::html::parse(text, &mut node_cx),
796    };
797    res.map(move |root_node| ParsedContent { root_node, node_cx })
798}
799
800fn selection_bounds(
801    start: Option<Point<Pixels>>,
802    end: Option<Point<Pixels>>,
803    bounds: Bounds<Pixels>,
804) -> Bounds<Pixels> {
805    if let (Some(start), Some(end)) = (start, end) {
806        let start = start + bounds.origin;
807        let end = end + bounds.origin;
808
809        let origin = Point {
810            x: start.x.min(end.x),
811            y: start.y.min(end.y),
812        };
813        let size = Size {
814            width: (start.x - end.x).abs(),
815            height: (start.y - end.y).abs(),
816        };
817
818        return Bounds { origin, size };
819    }
820
821    Bounds::default()
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827    use gpui::{Bounds, point, px, size};
828
829    #[test]
830    fn test_text_view_state_selection_bounds() {
831        assert_eq!(
832            selection_bounds(None, None, Default::default()),
833            Bounds::default()
834        );
835        assert_eq!(
836            selection_bounds(None, Some(point(px(10.), px(20.))), Default::default()),
837            Bounds::default()
838        );
839        assert_eq!(
840            selection_bounds(Some(point(px(10.), px(20.))), None, Default::default()),
841            Bounds::default()
842        );
843
844        // 10,10 start
845        //   |------|
846        //   |      |
847        //   |------|
848        //         50,50
849        assert_eq!(
850            selection_bounds(
851                Some(point(px(10.), px(10.))),
852                Some(point(px(50.), px(50.))),
853                Default::default()
854            ),
855            Bounds {
856                origin: point(px(10.), px(10.)),
857                size: size(px(40.), px(40.))
858            }
859        );
860        // 10,10
861        //   |------|
862        //   |      |
863        //   |------|
864        //         50,50 start
865        assert_eq!(
866            selection_bounds(
867                Some(point(px(50.), px(50.))),
868                Some(point(px(10.), px(10.))),
869                Default::default()
870            ),
871            Bounds {
872                origin: point(px(10.), px(10.)),
873                size: size(px(40.), px(40.))
874            }
875        );
876        //        50,10 start
877        //   |------|
878        //   |      |
879        //   |------|
880        // 10,50
881        assert_eq!(
882            selection_bounds(
883                Some(point(px(50.), px(10.))),
884                Some(point(px(10.), px(50.))),
885                Default::default()
886            ),
887            Bounds {
888                origin: point(px(10.), px(10.)),
889                size: size(px(40.), px(40.))
890            }
891        );
892        //        50,10
893        //   |------|
894        //   |      |
895        //   |------|
896        // 10,50 start
897        assert_eq!(
898            selection_bounds(
899                Some(point(px(10.), px(50.))),
900                Some(point(px(50.), px(10.))),
901                Default::default()
902            ),
903            Bounds {
904                origin: point(px(10.), px(10.)),
905                size: size(px(40.), px(40.))
906            }
907        );
908    }
909}