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::{ActiveTheme, StyledExt, v_flex};
19use crate::{
20    global_state::GlobalState,
21    input::{self},
22    text::{
23        TextViewStyle,
24        node::{self, NodeContext},
25    },
26};
27
28const CONTEXT: &'static str = "TextView";
29
30pub(crate) fn init(cx: &mut App) {
31    cx.bind_keys(vec![
32        #[cfg(target_os = "macos")]
33        KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
34        #[cfg(not(target_os = "macos"))]
35        KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
36    ]);
37}
38
39#[derive(IntoElement, Clone)]
40struct TextViewElement {
41    list_state: Option<ListState>,
42    state: Entity<TextViewState>,
43}
44
45impl RenderOnce for TextViewElement {
46    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
47        self.state.update(cx, |state, cx| {
48            v_flex()
49                .size_full()
50                .map(|this| match &mut state.parsed_result {
51                    Some(Ok(content)) => this.child(content.root_node.render_root(
52                        self.list_state.clone(),
53                        &content.node_cx,
54                        window,
55                        cx,
56                    )),
57                    Some(Err(err)) => this.child(
58                        v_flex()
59                            .gap_1()
60                            .child("Failed to parse content")
61                            .child(err.to_string()),
62                    ),
63                    None => this,
64                })
65        })
66    }
67}
68
69/// A text view that can render Markdown or HTML.
70///
71/// ## Goals
72///
73/// - Provide a rich text rendering component for such as Markdown or HTML,
74/// used to display rich text in GPUI application (e.g., Help messages, Release notes)
75/// - Support Markdown GFM and HTML (Simple HTML like Safari Reader Mode) for showing most common used markups.
76/// - Support Heading, Paragraph, Bold, Italic, StrikeThrough, Code, Link, Image, Blockquote, List, Table, HorizontalRule, CodeBlock ...
77///
78/// ## Not Goals
79///
80/// - Customization of the complex style (some simple styles will be supported)
81/// - As a Markdown editor or viewer (If you want to like this, you must fork your version).
82/// - As a HTML viewer, we not support CSS, we only support basic HTML tags for used to as a content reader.
83///
84/// See also [`MarkdownElement`], [`HtmlElement`]
85#[derive(Clone)]
86pub struct TextView {
87    id: ElementId,
88    init_state: Option<InitState>,
89    raw: SharedString,
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/// The type of the text view.
103#[derive(Clone, Copy, PartialEq, Eq)]
104enum TextViewType {
105    /// Markdown view
106    Markdown,
107    /// HTML view
108    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    /// The bounds of the text view
214    bounds: Bounds<Pixels>,
215    /// The local (in TextView) position of the selection.
216    selection_positions: (Option<Point<Pixels>>, Option<Point<Pixels>>),
217    /// Is current in selection.
218    is_selecting: bool,
219    is_selectable: bool,
220    list_state: ListState,
221}
222
223impl TextViewState {
224    fn new(cx: &mut Context<TextViewState>) -> Self {
225        let focus_handle = cx.focus_handle();
226        Self {
227            parent_entity: None,
228            tx: None,
229            parsed_result: None,
230            focus_handle: Some(focus_handle),
231            bounds: Bounds::default(),
232            selection_positions: (None, None),
233            is_selecting: false,
234            is_selectable: false,
235            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
236        }
237    }
238}
239
240impl TextViewState {
241    /// Save bounds and unselect if bounds changed.
242    fn update_bounds(&mut self, bounds: Bounds<Pixels>) {
243        if self.bounds.size != bounds.size {
244            self.clear_selection();
245        }
246        self.bounds = bounds;
247    }
248
249    fn clear_selection(&mut self) {
250        self.selection_positions = (None, None);
251        self.is_selecting = false;
252    }
253
254    fn start_selection(&mut self, pos: Point<Pixels>) {
255        let pos = pos - self.bounds.origin;
256        self.selection_positions = (Some(pos), Some(pos));
257        self.is_selecting = true;
258    }
259
260    fn update_selection(&mut self, pos: Point<Pixels>) {
261        let pos = pos - self.bounds.origin;
262        if let (Some(start), Some(_)) = self.selection_positions {
263            self.selection_positions = (Some(start), Some(pos))
264        }
265    }
266
267    fn end_selection(&mut self) {
268        self.is_selecting = false;
269    }
270
271    pub(crate) fn has_selection(&self) -> bool {
272        if let (Some(start), Some(end)) = self.selection_positions {
273            start != end
274        } else {
275            false
276        }
277    }
278
279    pub(crate) fn is_selectable(&self) -> bool {
280        self.is_selectable
281    }
282
283    /// Return the bounds of the selection in window coordinates.
284    pub(crate) fn selection_bounds(&self) -> Bounds<Pixels> {
285        selection_bounds(
286            self.selection_positions.0,
287            self.selection_positions.1,
288            self.bounds,
289        )
290    }
291
292    fn selection_text(&self) -> Option<String> {
293        Some(
294            self.parsed_result
295                .as_ref()?
296                .as_ref()
297                .ok()?
298                .root_node
299                .selected_text(),
300        )
301    }
302}
303
304#[derive(IntoElement, Clone)]
305pub enum Text {
306    String(SharedString),
307    TextView(Box<TextView>),
308}
309
310impl From<SharedString> for Text {
311    fn from(s: SharedString) -> Self {
312        Self::String(s)
313    }
314}
315
316impl From<&str> for Text {
317    fn from(s: &str) -> Self {
318        Self::String(SharedString::from(s.to_string()))
319    }
320}
321
322impl From<String> for Text {
323    fn from(s: String) -> Self {
324        Self::String(s.into())
325    }
326}
327
328impl From<TextView> for Text {
329    fn from(e: TextView) -> Self {
330        Self::TextView(Box::new(e))
331    }
332}
333
334impl Text {
335    /// Set the style for [`TextView`].
336    ///
337    /// Do nothing if this is `String`.
338    pub fn style(self, style: TextViewStyle) -> Self {
339        match self {
340            Self::String(s) => Self::String(s),
341            Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
342        }
343    }
344
345    /// Get the str
346    pub fn as_str(&self) -> &str {
347        match self {
348            Self::String(s) => s.as_str(),
349            Self::TextView(view) => view.raw.as_str(),
350        }
351    }
352}
353
354impl RenderOnce for Text {
355    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
356        match self {
357            Self::String(s) => s.into_any_element(),
358            Self::TextView(e) => e.into_any_element(),
359        }
360    }
361}
362
363impl Styled for TextView {
364    fn style(&mut self) -> &mut StyleRefinement {
365        &mut self.style
366    }
367}
368
369impl TextView {
370    fn create_init_state(
371        type_: TextViewType,
372        text: &SharedString,
373        highlight_theme: &Arc<HighlightTheme>,
374        state: &Entity<TextViewState>,
375        cx: &mut App,
376    ) -> InitState {
377        let state = state.read(cx);
378        if let Some(tx) = &state.tx {
379            InitState::Initialized { tx: tx.clone() }
380        } else {
381            InitState::Initializing {
382                type_,
383                text: text.clone(),
384                style: Default::default(),
385                highlight_theme: highlight_theme.clone(),
386            }
387        }
388    }
389
390    /// Create a new markdown text view.
391    pub fn markdown(
392        id: impl Into<ElementId>,
393        markdown: impl Into<SharedString>,
394        window: &mut Window,
395        cx: &mut App,
396    ) -> Self {
397        let id: ElementId = id.into();
398        let markdown = markdown.into();
399        let highlight_theme = cx.theme().highlight_theme.clone();
400        let state =
401            window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
402                TextViewState::new(cx)
403            });
404        let init_state = Self::create_init_state(
405            TextViewType::Markdown,
406            &markdown,
407            &highlight_theme,
408            &state,
409            cx,
410        );
411        if let Some(tx) = &state.read(cx).tx {
412            let _ = tx.try_send(Update::Text(markdown.clone()));
413        }
414        Self {
415            id,
416            init_state: Some(init_state),
417            raw: markdown.clone(),
418            style: StyleRefinement::default(),
419            state,
420            selectable: false,
421            scrollable: false,
422        }
423    }
424
425    /// Create a new html text view.
426    pub fn html(
427        id: impl Into<ElementId>,
428        html: impl Into<SharedString>,
429        window: &mut Window,
430        cx: &mut App,
431    ) -> Self {
432        let id: ElementId = id.into();
433        let html = html.into();
434        let highlight_theme = cx.theme().highlight_theme.clone();
435        let state =
436            window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
437                TextViewState::new(cx)
438            });
439        let init_state =
440            Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
441        if let Some(tx) = &state.read(cx).tx {
442            let _ = tx.try_send(Update::Text(html.clone()));
443        }
444        Self {
445            id,
446            init_state: Some(init_state),
447            style: StyleRefinement::default(),
448            state,
449            raw: html,
450            selectable: false,
451            scrollable: false,
452        }
453    }
454
455    /// Set the source text of the text view.
456    pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
457        let raw: SharedString = raw.into();
458        if let Some(init_state) = &mut self.init_state {
459            match init_state {
460                InitState::Initializing { text, .. } => *text = raw.clone(),
461                InitState::Initialized { tx } => {
462                    let _ = tx.try_send(Update::Text(raw.clone()));
463                }
464            }
465        }
466        self.raw = raw;
467        self
468    }
469
470    /// Set [`TextViewStyle`].
471    pub fn style(mut self, style: TextViewStyle) -> Self {
472        if let Some(init_state) = &mut self.init_state {
473            match init_state {
474                InitState::Initializing { style: s, .. } => *s = Box::new(style),
475                InitState::Initialized { tx } => {
476                    let _ = tx.try_send(Update::Style(Box::new(style)));
477                }
478            }
479        }
480        self
481    }
482
483    /// Set the text view to be selectable, default is false.
484    pub fn selectable(mut self, selectable: bool) -> Self {
485        self.selectable = selectable;
486        self
487    }
488
489    /// Set the text view to be scrollable, default is false.
490    ///
491    /// ## If true for `scrollable`
492    ///
493    /// The `scrollable` mode used for large content,
494    /// will show scrollbar, but requires the parent to have a fixed height,
495    /// and use [`gpui::list`] to render the content in a virtualized way.
496    ///
497    /// ## If false to fit content
498    ///
499    /// The TextView will expand to fit all content, no scrollbar.
500    /// This mode is suitable for small content, such as a few lines of text, a label, etc.
501    pub fn scrollable(mut self, scrollable: bool) -> Self {
502        self.scrollable = scrollable;
503        self
504    }
505
506    fn on_action_copy(state: &Entity<TextViewState>, cx: &mut App) {
507        let Some(selected_text) = state.read(cx).selection_text() else {
508            return;
509        };
510
511        cx.write_to_clipboard(ClipboardItem::new_string(selected_text.trim().to_string()));
512    }
513}
514
515impl IntoElement for TextView {
516    type Element = Self;
517
518    fn into_element(self) -> Self::Element {
519        self
520    }
521}
522
523impl Element for TextView {
524    type RequestLayoutState = AnyElement;
525    type PrepaintState = ();
526
527    fn id(&self) -> Option<ElementId> {
528        Some(self.id.clone())
529    }
530
531    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
532        None
533    }
534
535    fn request_layout(
536        &mut self,
537        _: Option<&GlobalElementId>,
538        _: Option<&InspectorElementId>,
539        window: &mut Window,
540        cx: &mut App,
541    ) -> (LayoutId, Self::RequestLayoutState) {
542        if let Some(InitState::Initializing {
543            type_,
544            text,
545            style,
546            highlight_theme,
547        }) = self.init_state.take()
548        {
549            let style = *style;
550            let highlight_theme = highlight_theme.clone();
551            let (tx, rx) = smol::channel::unbounded::<Update>();
552            let (tx_result, rx_result) =
553                smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
554            let parsed_result = parse_content(type_, &text, style.clone(), &highlight_theme);
555
556            self.state.update(cx, {
557                let tx = tx.clone();
558                |state, _| {
559                    state.parsed_result = Some(parsed_result);
560                    state.tx = Some(tx);
561                }
562            });
563
564            cx.spawn({
565                let state = self.state.downgrade();
566                async move |cx| {
567                    while let Ok(parsed_result) = rx_result.recv().await {
568                        if let Some(state) = state.upgrade() {
569                            _ = state.update(cx, |state, cx| {
570                                state.parsed_result = Some(parsed_result);
571                                if let Some(parent_entity) = state.parent_entity {
572                                    let app = &mut **cx;
573                                    app.notify(parent_entity);
574                                }
575                                state.clear_selection();
576                            });
577                        } else {
578                            // state released, stopping processing
579                            break;
580                        }
581                    }
582                }
583            })
584            .detach();
585
586            cx.background_spawn(UpdateFuture::new(
587                type_,
588                style,
589                text,
590                highlight_theme,
591                rx,
592                tx_result,
593                Duration::from_millis(200),
594            ))
595            .detach();
596
597            self.init_state = Some(InitState::Initialized { tx });
598        }
599
600        let list_state = &self.state.read(cx).list_state;
601
602        let focus_handle = self
603            .state
604            .read(cx)
605            .focus_handle
606            .as_ref()
607            .expect("focus_handle should init by TextViewState::new");
608
609        let mut el = div()
610            .key_context(CONTEXT)
611            .track_focus(focus_handle)
612            .size_full()
613            .relative()
614            .on_action({
615                let state = self.state.clone();
616                move |_: &input::Copy, _, cx| {
617                    Self::on_action_copy(&state, cx);
618                }
619            })
620            .child(TextViewElement {
621                list_state: if self.scrollable {
622                    Some(list_state.clone())
623                } else {
624                    None
625                },
626                state: self.state.clone(),
627            })
628            .refine_style(&self.style)
629            .vertical_scrollbar(list_state)
630            .into_any_element();
631        let layout_id = el.request_layout(window, cx);
632        (layout_id, el)
633    }
634
635    fn prepaint(
636        &mut self,
637        _: Option<&GlobalElementId>,
638        _: Option<&InspectorElementId>,
639        _: Bounds<Pixels>,
640        request_layout: &mut Self::RequestLayoutState,
641        window: &mut Window,
642        cx: &mut App,
643    ) -> Self::PrepaintState {
644        request_layout.prepaint(window, cx);
645    }
646
647    fn paint(
648        &mut self,
649        _: Option<&GlobalElementId>,
650        _: Option<&InspectorElementId>,
651        bounds: Bounds<Pixels>,
652        request_layout: &mut Self::RequestLayoutState,
653        _: &mut Self::PrepaintState,
654        window: &mut Window,
655        cx: &mut App,
656    ) {
657        let entity_id = window.current_view();
658        let is_selectable = self.selectable;
659
660        self.state.update(cx, |state, _| {
661            state.parent_entity = Some(entity_id);
662            state.update_bounds(bounds);
663            state.is_selectable = is_selectable;
664        });
665
666        GlobalState::global_mut(cx)
667            .text_view_state_stack
668            .push(self.state.clone());
669        request_layout.paint(window, cx);
670        GlobalState::global_mut(cx).text_view_state_stack.pop();
671
672        if self.selectable {
673            let is_selecting = self.state.read(cx).is_selecting;
674            let has_selection = self.state.read(cx).has_selection();
675
676            window.on_mouse_event({
677                let state = self.state.clone();
678                move |event: &MouseDownEvent, phase, _, cx| {
679                    if !bounds.contains(&event.position) || !phase.bubble() {
680                        return;
681                    }
682
683                    state.update(cx, |state, _| {
684                        state.start_selection(event.position);
685                    });
686                    cx.notify(entity_id);
687                }
688            });
689
690            if is_selecting {
691                // move to update end position.
692                window.on_mouse_event({
693                    let state = self.state.clone();
694                    move |event: &MouseMoveEvent, phase, _, cx| {
695                        if !phase.bubble() {
696                            return;
697                        }
698
699                        state.update(cx, |state, _| {
700                            state.update_selection(event.position);
701                        });
702                        cx.notify(entity_id);
703                    }
704                });
705
706                // up to end selection
707                window.on_mouse_event({
708                    let state = self.state.clone();
709                    move |_: &MouseUpEvent, phase, _, cx| {
710                        if !phase.bubble() {
711                            return;
712                        }
713
714                        state.update(cx, |state, _| {
715                            state.end_selection();
716                        });
717                        cx.notify(entity_id);
718                    }
719                });
720            }
721
722            if has_selection {
723                // down outside to clear selection
724                window.on_mouse_event({
725                    let state = self.state.clone();
726                    move |event: &MouseDownEvent, _, _, cx| {
727                        if bounds.contains(&event.position) {
728                            return;
729                        }
730
731                        state.update(cx, |state, _| {
732                            state.clear_selection();
733                        });
734                        cx.notify(entity_id);
735                    }
736                });
737            }
738        }
739    }
740}
741
742fn parse_content(
743    type_: TextViewType,
744    text: &str,
745    style: TextViewStyle,
746    highlight_theme: &HighlightTheme,
747) -> Result<ParsedContent, SharedString> {
748    let mut node_cx = NodeContext {
749        style: style.clone(),
750        ..NodeContext::default()
751    };
752
753    let res = match type_ {
754        TextViewType::Markdown => {
755            super::format::markdown::parse(text, &style, &mut node_cx, highlight_theme)
756        }
757        TextViewType::Html => super::format::html::parse(text, &mut node_cx),
758    };
759    res.map(move |root_node| ParsedContent { root_node, node_cx })
760}
761
762fn selection_bounds(
763    start: Option<Point<Pixels>>,
764    end: Option<Point<Pixels>>,
765    bounds: Bounds<Pixels>,
766) -> Bounds<Pixels> {
767    if let (Some(start), Some(end)) = (start, end) {
768        let start = start + bounds.origin;
769        let end = end + bounds.origin;
770
771        let origin = Point {
772            x: start.x.min(end.x),
773            y: start.y.min(end.y),
774        };
775        let size = Size {
776            width: (start.x - end.x).abs(),
777            height: (start.y - end.y).abs(),
778        };
779
780        return Bounds { origin, size };
781    }
782
783    Bounds::default()
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use gpui::{Bounds, point, px, size};
790
791    #[test]
792    fn test_text_view_state_selection_bounds() {
793        assert_eq!(
794            selection_bounds(None, None, Default::default()),
795            Bounds::default()
796        );
797        assert_eq!(
798            selection_bounds(None, Some(point(px(10.), px(20.))), Default::default()),
799            Bounds::default()
800        );
801        assert_eq!(
802            selection_bounds(Some(point(px(10.), px(20.))), None, Default::default()),
803            Bounds::default()
804        );
805
806        // 10,10 start
807        //   |------|
808        //   |      |
809        //   |------|
810        //         50,50
811        assert_eq!(
812            selection_bounds(
813                Some(point(px(10.), px(10.))),
814                Some(point(px(50.), px(50.))),
815                Default::default()
816            ),
817            Bounds {
818                origin: point(px(10.), px(10.)),
819                size: size(px(40.), px(40.))
820            }
821        );
822        // 10,10
823        //   |------|
824        //   |      |
825        //   |------|
826        //         50,50 start
827        assert_eq!(
828            selection_bounds(
829                Some(point(px(50.), px(50.))),
830                Some(point(px(10.), px(10.))),
831                Default::default()
832            ),
833            Bounds {
834                origin: point(px(10.), px(10.)),
835                size: size(px(40.), px(40.))
836            }
837        );
838        //        50,10 start
839        //   |------|
840        //   |      |
841        //   |------|
842        // 10,50
843        assert_eq!(
844            selection_bounds(
845                Some(point(px(50.), px(10.))),
846                Some(point(px(10.), px(50.))),
847                Default::default()
848            ),
849            Bounds {
850                origin: point(px(10.), px(10.)),
851                size: size(px(40.), px(40.))
852            }
853        );
854        //        50,10
855        //   |------|
856        //   |      |
857        //   |------|
858        // 10,50 start
859        assert_eq!(
860            selection_bounds(
861                Some(point(px(10.), px(50.))),
862                Some(point(px(50.), px(10.))),
863                Default::default()
864            ),
865            Bounds {
866                origin: point(px(10.), px(10.)),
867                size: size(px(40.), px(40.))
868            }
869        );
870    }
871}