Skip to main content

freya_components/
selectable_text.rs

1use freya_core::prelude::*;
2use freya_edit::*;
3
4/// Current status of the SelectableText.
5#[derive(Debug, Default, PartialEq, Clone, Copy)]
6pub enum SelectableTextStatus {
7    /// Default state.
8    #[default]
9    Idle,
10    /// Mouse is hovering the text.
11    Hovering,
12}
13
14/// A piece of a [SelectableText]: styled text or an inline element flowing between the text.
15#[derive(Clone, PartialEq)]
16enum SelectableContent {
17    Span(Span<'static>),
18    Child(Element),
19}
20
21#[derive(Clone, PartialEq)]
22pub struct SelectableText {
23    contents: Vec<SelectableContent>,
24    layout: LayoutData,
25    accessibility: AccessibilityData,
26    text_style_data: TextStyleData,
27    max_lines: Option<usize>,
28    line_height: Option<f32>,
29    key: DiffKey,
30}
31
32impl KeyExt for SelectableText {
33    fn write_key(&mut self) -> &mut DiffKey {
34        &mut self.key
35    }
36}
37
38impl LayoutExt for SelectableText {
39    fn get_layout(&mut self) -> &mut LayoutData {
40        &mut self.layout
41    }
42}
43
44impl ContainerExt for SelectableText {}
45
46impl AccessibilityExt for SelectableText {
47    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
48        &mut self.accessibility
49    }
50}
51
52impl TextStyleExt for SelectableText {
53    fn get_text_style_data(&mut self) -> &mut TextStyleData {
54        &mut self.text_style_data
55    }
56}
57
58impl Default for SelectableText {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl SelectableText {
65    pub fn new() -> Self {
66        Self {
67            contents: Vec::new(),
68            layout: LayoutData::default(),
69            accessibility: AccessibilityData::default(),
70            text_style_data: TextStyleData::default(),
71            max_lines: None,
72            line_height: None,
73            key: DiffKey::None,
74        }
75    }
76
77    /// Append a styled [Span] to the text.
78    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
79        self.contents.push(SelectableContent::Span(span.into()));
80        self
81    }
82
83    /// Append an inline element that flows between the text.
84    pub fn child(mut self, child: impl IntoElement) -> Self {
85        self.contents
86            .push(SelectableContent::Child(child.into_element()));
87        self
88    }
89
90    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
91        self.max_lines = max_lines.into();
92        self
93    }
94
95    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
96        self.line_height = line_height.into();
97        self
98    }
99
100    /// The text the editor selects over. Each inline element becomes a single space
101    /// so selection offsets stay aligned with the paragraph's placeholders.
102    fn editor_text(&self) -> String {
103        self.contents
104            .iter()
105            .map(|content| match content {
106                SelectableContent::Span(span) => span.text.as_ref(),
107                SelectableContent::Child(_) => " ",
108            })
109            .collect()
110    }
111}
112
113impl Component for SelectableText {
114    fn render(&self) -> impl IntoElement {
115        let value = self.editor_text();
116        let holder = use_state(ParagraphHolder::default);
117        let mut editable = use_editable(
118            || self.editor_text(),
119            move || EditableConfig::new().with_allow_changes(false),
120        );
121        let mut status = use_state(SelectableTextStatus::default);
122        let a11y_id = use_a11y();
123        let mut drag_origin = use_state(|| None);
124
125        if value.as_str() != editable.editor().read().rope() {
126            editable.editor_mut().write().set(value.as_str());
127            editable.editor_mut().write().editor_history().clear();
128        }
129
130        let highlights = editable
131            .editor()
132            .read()
133            .get_visible_selection(EditorLine::SingleParagraph);
134
135        let on_pointer_down = move |e: Event<PointerEventData>| {
136            if !e.data().is_primary() {
137                return;
138            }
139            e.stop_propagation();
140            drag_origin.set(Some(e.global_location() - e.element_location()));
141            editable.process_event(EditableEvent::Down {
142                location: e.element_location(),
143                editor_line: EditorLine::SingleParagraph,
144                holder: &holder.read(),
145            });
146            a11y_id.request_focus();
147        };
148
149        let on_global_pointer_move = move |e: Event<PointerEventData>| {
150            if a11y_id.is_focused()
151                && let Some(drag_origin) = drag_origin()
152            {
153                let mut element_location = e.element_location();
154                element_location.x -= drag_origin.x;
155                element_location.y -= drag_origin.y;
156                editable.process_event(EditableEvent::Move {
157                    location: element_location,
158                    editor_line: EditorLine::SingleParagraph,
159                    holder: &holder.read(),
160                });
161            }
162        };
163
164        let on_global_pointer_down = move |_: Event<PointerEventData>| {
165            if *status.read() == SelectableTextStatus::Idle {
166                editable.editor_mut().write().clear_selection();
167            }
168        };
169
170        let on_pointer_enter = move |_| {
171            *status.write() = SelectableTextStatus::Hovering;
172        };
173
174        let on_pointer_leave = move |_| {
175            *status.write() = SelectableTextStatus::default();
176        };
177
178        let on_mouse_up = move |_| {
179            editable.process_event(EditableEvent::Release);
180        };
181
182        let on_key_down = move |e: Event<KeyboardEventData>| {
183            editable.process_event(EditableEvent::KeyDown {
184                key: &e.key,
185                modifiers: e.modifiers,
186            });
187        };
188
189        let on_key_up = move |e: Event<KeyboardEventData>| {
190            editable.process_event(EditableEvent::KeyUp { key: &e.key });
191        };
192
193        let on_global_pointer_press = move |_: Event<PointerEventData>| {
194            match *status.read() {
195                SelectableTextStatus::Idle if a11y_id.is_focused() => {
196                    editable.process_event(EditableEvent::Release);
197                }
198                SelectableTextStatus::Hovering => {
199                    editable.process_event(EditableEvent::Release);
200                }
201                _ => {}
202            };
203
204            if drag_origin.read().is_some() {
205                drag_origin.set(None);
206            } else if a11y_id.is_focused() {
207                a11y_id.request_unfocus();
208            }
209        };
210
211        let mut paragraph = paragraph()
212            .layout(self.layout.clone())
213            .accessibility(self.accessibility.clone())
214            .text_style(self.text_style_data.clone())
215            .max_lines(self.max_lines)
216            .line_height(self.line_height)
217            .a11y_id(a11y_id)
218            .a11y_focusable(true)
219            .holder(holder.read().clone())
220            .cursor_color(Color::BLACK)
221            .highlights(highlights.map(|h| vec![h]))
222            .on_mouse_up(on_mouse_up)
223            .on_global_pointer_move(on_global_pointer_move)
224            .on_global_pointer_down(on_global_pointer_down)
225            .on_pointer_down(on_pointer_down)
226            .on_pointer_enter(on_pointer_enter)
227            .on_pointer_leave(on_pointer_leave)
228            .on_global_pointer_press(on_global_pointer_press)
229            .on_key_down(on_key_down)
230            .on_key_up(on_key_up);
231
232        for content in &self.contents {
233            paragraph = match content {
234                SelectableContent::Span(span) => paragraph.span(span.clone()),
235                SelectableContent::Child(child) => paragraph.child(child.clone()),
236            };
237        }
238
239        paragraph
240    }
241
242    fn render_key(&self) -> DiffKey {
243        self.key.clone().or(self.default_key())
244    }
245}