freya_components/
selectable_text.rs

1use std::rc::Rc;
2
3use dioxus::prelude::*;
4use freya_core::platform::CursorIcon;
5use freya_elements::{
6    self as dioxus_elements,
7    events::KeyboardEvent,
8    MouseEvent,
9};
10use freya_hooks::{
11    use_editable,
12    use_focus,
13    use_platform,
14    EditableConfig,
15    EditableEvent,
16    EditableMode,
17    TextEditor,
18};
19
20/// Current status of the SelectableText.
21#[derive(Debug, Default, PartialEq, Clone, Copy)]
22pub enum SelectableTextStatus {
23    /// Default state.
24    #[default]
25    Idle,
26    /// Mouse is hovering the text.
27    Hovering,
28}
29
30/// Text that can be selected with a mouse or keyboard.
31///
32/// # Example
33///
34/// ```rust
35/// # use freya::prelude::*;
36/// fn app() -> Element {
37///     rsx!(SelectableText {
38///         value: "You can select this looooooooooong text"
39///     })
40/// }
41/// ```
42#[component]
43pub fn SelectableText(value: ReadOnlySignal<String>) -> Element {
44    let platform = use_platform();
45    let mut editable = use_editable(
46        move || EditableConfig::new(value()).with_allow_changes(false),
47        EditableMode::MultipleLinesSingleEditor,
48    );
49    let mut status = use_signal(SelectableTextStatus::default);
50    let mut focus = use_focus();
51    let mut drag_origin = use_signal(|| None);
52
53    if &*value.read() != editable.editor().read().rope() {
54        editable.editor_mut().write().set(&value.read());
55        editable.editor_mut().write().editor_history().clear();
56    }
57
58    use_drop(move || {
59        if *status.peek() == SelectableTextStatus::Hovering {
60            platform.set_cursor(CursorIcon::default());
61        }
62    });
63
64    let a11y_id = focus.attribute();
65    let cursor_reference = editable.cursor_attr();
66    let highlights = editable.highlights_attr(0);
67
68    let onmousedown = move |e: MouseEvent| {
69        e.stop_propagation();
70        drag_origin.set(Some(e.get_screen_coordinates() - e.element_coordinates));
71        editable.process_event(&EditableEvent::MouseDown(e.data, 0));
72        focus.request_focus();
73    };
74
75    let onglobalmousemove = move |mut e: MouseEvent| {
76        if focus.is_focused() {
77            if let Some(drag_origin) = drag_origin() {
78                let data = Rc::get_mut(&mut e.data).unwrap();
79                data.element_coordinates.x -= drag_origin.x;
80                data.element_coordinates.y -= drag_origin.y;
81                editable.process_event(&EditableEvent::MouseMove(e.data, 0));
82            }
83        }
84    };
85
86    let onmouseenter = move |_| {
87        platform.set_cursor(CursorIcon::Text);
88        *status.write() = SelectableTextStatus::Hovering;
89    };
90
91    let onmouseleave = move |_| {
92        platform.set_cursor(CursorIcon::default());
93        *status.write() = SelectableTextStatus::default();
94    };
95
96    let onclick = move |_: MouseEvent| {
97        editable.process_event(&EditableEvent::Click);
98    };
99
100    let onkeydown = move |e: KeyboardEvent| {
101        editable.process_event(&EditableEvent::KeyDown(e.data));
102    };
103
104    let onkeyup = move |e: KeyboardEvent| {
105        editable.process_event(&EditableEvent::KeyUp(e.data));
106    };
107
108    let onglobalclick = move |_| {
109        match *status.read() {
110            SelectableTextStatus::Idle if focus.is_focused() => {
111                editable.process_event(&EditableEvent::Click);
112            }
113            SelectableTextStatus::Hovering => {
114                editable.process_event(&EditableEvent::Click);
115            }
116            _ => {}
117        };
118
119        // Unfocus text when this:
120        // + is focused
121        // + it has not just being dragged
122        // + a global click happened
123        if focus.is_focused() {
124            if drag_origin.read().is_some() {
125                drag_origin.set(None);
126            } else {
127                editable.editor_mut().write().clear_selection();
128                focus.request_unfocus();
129            }
130        }
131    };
132
133    rsx!(
134        paragraph {
135            a11y_focusable: "true",
136            a11y_id,
137            cursor_id: "0",
138            cursor_mode: "editable",
139            cursor_color: "black",
140            highlights,
141            cursor_reference,
142            onclick,
143            onglobalmousemove,
144            onmousedown,
145            onmouseenter,
146            onmouseleave,
147            onglobalclick,
148            onkeydown,
149            onkeyup,
150            text {
151                "{editable.editor()}"
152            }
153        }
154    )
155}
156
157#[cfg(test)]
158mod test {
159    use freya::prelude::*;
160    use freya_testing::prelude::*;
161
162    #[tokio::test]
163    pub async fn selectable_text() {
164        fn selectable_text_app() -> Element {
165            rsx!(SelectableText {
166                value: "Hello, World!"
167            })
168        }
169
170        let mut utils = launch_test(selectable_text_app);
171
172        // Initial state
173        let root = utils.root().get(0);
174        assert_eq!(root.get(0).get(0).text(), Some("Hello, World!"));
175        utils.wait_for_update().await;
176        utils.wait_for_update().await;
177
178        utils.push_event(TestEvent::Mouse {
179            name: EventName::MouseDown,
180            cursor: (3.0, 3.0).into(),
181            button: Some(MouseButton::Left),
182        });
183        utils.wait_for_update().await;
184        utils.wait_for_update().await;
185        utils.push_event(TestEvent::Mouse {
186            name: EventName::MouseMove,
187            cursor: (55.0, 3.0).into(),
188            button: Some(MouseButton::Left),
189        });
190        utils.wait_for_update().await;
191        utils.wait_for_update().await;
192
193        let root = utils.root().get(0);
194        let highlights = root.state().cursor.highlights.clone();
195        #[cfg(not(target_os = "macos"))]
196        assert_eq!(highlights, Some(vec![(0, 8)]));
197
198        #[cfg(target_os = "macos")]
199        assert_eq!(highlights, Some(vec![(0, 7)]));
200    }
201}