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
50pub 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#[derive(PartialEq, Eq, Clone, Copy)]
61pub enum EditableMode {
62 SingleLineMultipleEditors,
66 MultipleLinesSingleEditor,
70}
71
72impl Default for EditableMode {
73 fn default() -> Self {
74 Self::MultipleLinesSingleEditor
75 }
76}
77
78#[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#[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 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 CursorLayoutResponse::CursorPosition { position, id } => {
168 let mut text_editor = editor.write();
169 let new_cursor = text_editor.measure_new_cursor(position, id);
170
171 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 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 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 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 pub fn editor(&self) -> &Signal<RopeEditor> {
226 &self.editor
227 }
228
229 pub fn editor_mut(&mut self) -> &mut Signal<RopeEditor> {
231 &mut self.editor
232 }
233
234 pub fn cursor_attr(&self) -> AttributeValue {
236 AttributeValue::any_value(CustomAttributeValues::CursorReference(
237 self.cursor_reference.peek().clone(),
238 ))
239 }
240
241 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 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 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 _ => {
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
358pub 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 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 pub fn with_cursor(mut self, pos: usize) -> Self {
383 self.cursor = TextCursor::new(pos);
384 self
385 }
386
387 pub fn with_identation(mut self, identation: u8) -> Self {
389 self.identation = identation;
390 self
391 }
392
393 pub fn with_allow_tabs(mut self, allow_tabs: bool) -> Self {
395 self.allow_tabs = allow_tabs;
396 self
397 }
398
399 pub fn with_allow_changes(mut self, allow_changes: bool) -> Self {
401 self.allow_changes = allow_changes;
402 self
403 }
404
405 pub fn with_allow_clipboard(mut self, allow_clipboard: bool) -> Self {
407 self.allow_clipboard = allow_clipboard;
408 self
409 }
410}
411
412pub 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}