freya_hooks/
use_editable.rs

1use std::rc::Rc;
2
3use dioxus_clipboard::prelude::{
4    use_clipboard,
5    UseClipboard,
6};
7use dioxus_core::{
8    prelude::spawn,
9    use_hook,
10    AttributeValue,
11};
12use dioxus_signals::{
13    Readable,
14    Signal,
15    Writable,
16};
17use freya_core::{
18    custom_attributes::{
19        CursorLayoutResponse,
20        CursorReference,
21        CustomAttributeValues,
22    },
23    event_loop_messages::{
24        EventLoopMessage,
25        TextGroupMeasurement,
26    },
27};
28use freya_elements::{
29    events::{
30        Code,
31        KeyboardData,
32        MouseData,
33    },
34    MouseButton,
35};
36use tokio::sync::mpsc::unbounded_channel;
37use torin::geometry::CursorPoint;
38use uuid::Uuid;
39
40use crate::{
41    use_platform,
42    EditorHistory,
43    RopeEditor,
44    TextCursor,
45    TextEditor,
46    TextEvent,
47    UsePlatform,
48};
49
50/// Events emitted to the [`UseEditable`].
51pub enum EditableEvent {
52    Click,
53    MouseMove(Rc<MouseData>, usize),
54    MouseDown(Rc<MouseData>, usize),
55    KeyDown(Rc<KeyboardData>),
56    KeyUp(Rc<KeyboardData>),
57}
58
59/// How the editable content must behave.
60#[derive(PartialEq, Eq, Clone, Copy)]
61pub enum EditableMode {
62    /// Multiple editors of only one line.
63    ///
64    /// Useful for textarea-like editors that need more customization than a simple paragraph for example.
65    SingleLineMultipleEditors,
66    /// One editor of multiple lines.
67    ///
68    /// A paragraph for example.
69    MultipleLinesSingleEditor,
70}
71
72impl Default for EditableMode {
73    fn default() -> Self {
74        Self::MultipleLinesSingleEditor
75    }
76}
77
78/// Indicates the type of text dragging being done.
79#[derive(Debug, PartialEq, Clone)]
80pub enum TextDragging {
81    None,
82    FromPointToPoint {
83        src: CursorPoint,
84    },
85    FromCursorToPoint {
86        shift: bool,
87        clicked: bool,
88        cursor: usize,
89        dist: Option<CursorPoint>,
90    },
91}
92
93impl TextDragging {
94    pub fn has_cursor_coords(&self) -> bool {
95        match self {
96            Self::None => false,
97            Self::FromPointToPoint { .. } => true,
98            Self::FromCursorToPoint { dist, .. } => dist.is_some(),
99        }
100    }
101
102    pub fn set_cursor_coords(&mut self, cursor: CursorPoint) {
103        match self {
104            Self::FromPointToPoint { src } => *src = cursor,
105            Self::FromCursorToPoint {
106                dist, shift: true, ..
107            } => *dist = Some(cursor),
108            _ => *self = Self::FromPointToPoint { src: cursor },
109        }
110    }
111
112    pub fn get_cursor_coords(&self) -> Option<CursorPoint> {
113        match self {
114            Self::None => None,
115            Self::FromPointToPoint { src } => Some(*src),
116            Self::FromCursorToPoint { dist, clicked, .. } => {
117                if *clicked {
118                    *dist
119                } else {
120                    None
121                }
122            }
123        }
124    }
125}
126
127/// Manage an editable text.
128#[derive(Clone, Copy, PartialEq)]
129pub struct UseEditable {
130    pub(crate) editor: Signal<RopeEditor>,
131    pub(crate) cursor_reference: Signal<CursorReference>,
132    pub(crate) dragging: Signal<TextDragging>,
133    pub(crate) platform: UsePlatform,
134    pub(crate) allow_tabs: bool,
135    pub(crate) allow_changes: bool,
136    pub(crate) allow_clipboard: bool,
137}
138
139impl UseEditable {
140    /// Manually create an editable content instead of using [use_editable].
141    pub fn new_in_hook(
142        clipboard: UseClipboard,
143        platform: UsePlatform,
144        config: EditableConfig,
145        mode: EditableMode,
146    ) -> Self {
147        let text_id = Uuid::new_v4();
148        let mut editor = Signal::new(RopeEditor::new(
149            config.content,
150            config.cursor,
151            config.identation,
152            mode,
153            clipboard,
154            EditorHistory::new(),
155        ));
156        let dragging = Signal::new(TextDragging::None);
157        let (cursor_sender, mut cursor_receiver) = unbounded_channel::<CursorLayoutResponse>();
158        let cursor_reference = CursorReference {
159            text_id,
160            cursor_sender,
161        };
162
163        spawn(async move {
164            while let Some(message) = cursor_receiver.recv().await {
165                match message {
166                    // Update the cursor position calculated by the layout
167                    CursorLayoutResponse::CursorPosition { position, id } => {
168                        let mut text_editor = editor.write();
169                        let new_cursor = text_editor.measure_new_cursor(position, id);
170
171                        // Only update and clear the selection if the cursor has changed
172                        if *text_editor.cursor() != new_cursor {
173                            *text_editor.cursor_mut() = new_cursor;
174                            if let TextDragging::FromCursorToPoint { cursor: from, .. } =
175                                &*dragging.read()
176                            {
177                                let to = text_editor.cursor_pos();
178                                text_editor.set_selection((*from, to));
179                            } else {
180                                text_editor.clear_selection();
181                            }
182                        }
183                    }
184                    // Update the text selections calculated by the layout
185                    CursorLayoutResponse::TextSelection { from, to, id } => {
186                        let current_cursor = editor.peek().cursor().clone();
187                        let current_selection = editor.peek().get_selection();
188
189                        let maybe_new_cursor = editor.peek().measure_new_cursor(to, id);
190                        let maybe_new_selection = editor.peek().measure_new_selection(from, to, id);
191
192                        // Update the text selection if it has changed
193                        if let Some(current_selection) = current_selection {
194                            if current_selection != maybe_new_selection {
195                                let mut text_editor = editor.write();
196                                text_editor.set_selection(maybe_new_selection);
197                            }
198                        } else {
199                            let mut text_editor = editor.write();
200                            text_editor.set_selection(maybe_new_selection);
201                        }
202
203                        // Update the cursor if it has changed
204                        if current_cursor != maybe_new_cursor {
205                            let mut text_editor = editor.write();
206                            *text_editor.cursor_mut() = maybe_new_cursor;
207                        }
208                    }
209                }
210            }
211        });
212
213        UseEditable {
214            editor,
215            cursor_reference: Signal::new(cursor_reference.clone()),
216            dragging,
217            platform,
218            allow_tabs: config.allow_tabs,
219            allow_changes: config.allow_changes,
220            allow_clipboard: config.allow_clipboard,
221        }
222    }
223
224    /// Reference to the editor.
225    pub fn editor(&self) -> &Signal<RopeEditor> {
226        &self.editor
227    }
228
229    /// Mutable reference to the editor.
230    pub fn editor_mut(&mut self) -> &mut Signal<RopeEditor> {
231        &mut self.editor
232    }
233
234    /// Create a cursor attribute.
235    pub fn cursor_attr(&self) -> AttributeValue {
236        AttributeValue::any_value(CustomAttributeValues::CursorReference(
237            self.cursor_reference.peek().clone(),
238        ))
239    }
240
241    /// Create a highlights attribute.
242    pub fn highlights_attr(&self, editor_id: usize) -> AttributeValue {
243        AttributeValue::any_value(CustomAttributeValues::TextHighlights(
244            self.editor
245                .read()
246                .get_visible_selection(editor_id)
247                .map(|v| vec![v])
248                .unwrap_or_default(),
249        ))
250    }
251
252    /// Process a [`EditableEvent`] event.
253    pub fn process_event(&mut self, edit_event: &EditableEvent) {
254        let res = match edit_event {
255            EditableEvent::MouseDown(e, id)
256                if e.get_trigger_button() == Some(MouseButton::Left) =>
257            {
258                let coords = e.get_element_coordinates();
259
260                self.dragging.write().set_cursor_coords(coords);
261                self.editor.write().clear_selection();
262
263                Some((*id, Some(coords), None))
264            }
265            EditableEvent::MouseMove(e, id) => {
266                if let Some(src) = self.dragging.peek().get_cursor_coords() {
267                    let new_dist = e.get_element_coordinates();
268
269                    Some((*id, None, Some((src, new_dist))))
270                } else {
271                    None
272                }
273            }
274            EditableEvent::Click => {
275                let dragging = &mut *self.dragging.write();
276                match dragging {
277                    TextDragging::FromCursorToPoint { shift, clicked, .. } if *shift => {
278                        *clicked = false;
279                    }
280                    _ => {
281                        *dragging = TextDragging::None;
282                    }
283                }
284                None
285            }
286            EditableEvent::KeyDown(e) => {
287                match e.code {
288                    // Handle dragging
289                    Code::ShiftLeft => {
290                        let dragging = &mut *self.dragging.write();
291                        match dragging {
292                            TextDragging::FromCursorToPoint {
293                                shift: shift_pressed,
294                                ..
295                            } => {
296                                *shift_pressed = true;
297                            }
298                            TextDragging::None => {
299                                *dragging = TextDragging::FromCursorToPoint {
300                                    shift: true,
301                                    clicked: false,
302                                    cursor: self.editor.peek().cursor_pos(),
303                                    dist: None,
304                                }
305                            }
306                            _ => {}
307                        }
308                    }
309                    // Handle editing
310                    _ => {
311                        let event = self.editor.write().process_key(
312                            &e.key,
313                            &e.code,
314                            &e.modifiers,
315                            self.allow_tabs,
316                            self.allow_changes,
317                            self.allow_clipboard,
318                        );
319                        if event.contains(TextEvent::TEXT_CHANGED) {
320                            *self.dragging.write() = TextDragging::None;
321                        }
322                    }
323                }
324
325                None
326            }
327            EditableEvent::KeyUp(e) => {
328                if e.code == Code::ShiftLeft {
329                    if let TextDragging::FromCursorToPoint { shift, .. } =
330                        &mut *self.dragging.write()
331                    {
332                        *shift = false;
333                    }
334                } else {
335                    *self.dragging.write() = TextDragging::None;
336                }
337
338                None
339            }
340            _ => None,
341        };
342
343        if let Some((cursor_id, cursor_position, cursor_selection)) = res {
344            if self.dragging.peek().has_cursor_coords() {
345                self.platform
346                    .send(EventLoopMessage::RemeasureTextGroup(TextGroupMeasurement {
347                        text_id: self.cursor_reference.peek().text_id,
348                        cursor_id,
349                        cursor_position,
350                        cursor_selection,
351                    }))
352                    .unwrap()
353            }
354        }
355    }
356}
357
358/// Create a configuration for a [`UseEditable`].
359pub struct EditableConfig {
360    pub(crate) content: String,
361    pub(crate) cursor: TextCursor,
362    pub(crate) identation: u8,
363    pub(crate) allow_tabs: bool,
364    pub(crate) allow_changes: bool,
365    pub(crate) allow_clipboard: bool,
366}
367
368impl EditableConfig {
369    /// Create a [`EditableConfig`].
370    pub fn new(content: String) -> Self {
371        Self {
372            content,
373            cursor: TextCursor::default(),
374            identation: 4,
375            allow_tabs: false,
376            allow_changes: true,
377            allow_clipboard: true,
378        }
379    }
380
381    /// Specify a custom initial cursor position.
382    pub fn with_cursor(mut self, pos: usize) -> Self {
383        self.cursor = TextCursor::new(pos);
384        self
385    }
386
387    /// Specify a custom identation
388    pub fn with_identation(mut self, identation: u8) -> Self {
389        self.identation = identation;
390        self
391    }
392
393    /// Specify whether you want to allow tabs to be inserted
394    pub fn with_allow_tabs(mut self, allow_tabs: bool) -> Self {
395        self.allow_tabs = allow_tabs;
396        self
397    }
398
399    /// Allow changes through keyboard events or not
400    pub fn with_allow_changes(mut self, allow_changes: bool) -> Self {
401        self.allow_changes = allow_changes;
402        self
403    }
404
405    /// Allow clipboard keyboard events
406    pub fn with_allow_clipboard(mut self, allow_clipboard: bool) -> Self {
407        self.allow_clipboard = allow_clipboard;
408        self
409    }
410}
411
412/// Hook to create an editable text.
413///
414/// For manual creation use [UseEditable::new_in_hook].
415///
416/// **This is a low level hook and is not expected to be used by the common user, in fact,
417/// you might be looking for something like the `Input` component instead.**
418pub fn use_editable(
419    initializer: impl FnOnce() -> EditableConfig,
420    mode: EditableMode,
421) -> UseEditable {
422    let platform = use_platform();
423    let clipboard = use_clipboard();
424
425    use_hook(|| UseEditable::new_in_hook(clipboard, platform, initializer(), mode))
426}