ultron_core/
base_editor.rs

1pub use crate::Selection;
2use crate::{BaseOptions, SelectionMode, TextBuffer, TextEdit};
3use nalgebra::Point2;
4use std::marker::PhantomData;
5use std::sync::Arc;
6pub use ultron_syntaxes_themes::{Style, TextHighlighter};
7
8/// An editor with core functionality platform specific UI
9pub struct BaseEditor<XMSG> {
10    options: BaseOptions,
11    text_edit: TextEdit,
12    /// Other components can listen to the an event.
13    /// When the content of the text editor changes, the change listener will be emitted
14    #[cfg(feature = "callback")]
15    change_listeners: Vec<Callback<String, XMSG>>,
16    /// a cheaper listener which doesn't need to assemble the text content
17    /// of the text editor everytime
18    #[cfg(feature = "callback")]
19    change_notify_listeners: Vec<Callback<(), XMSG>>,
20    _phantom: PhantomData<XMSG>,
21}
22
23impl<XMSG> AsRef<TextEdit> for BaseEditor<XMSG> {
24    fn as_ref(&self) -> &TextEdit {
25        &self.text_edit
26    }
27}
28
29impl<XMSG> AsMut<TextEdit> for BaseEditor<XMSG> {
30    fn as_mut(&mut self) -> &mut TextEdit {
31        &mut self.text_edit
32    }
33}
34
35impl<XMSG> Default for BaseEditor<XMSG> {
36    fn default() -> Self {
37        Self {
38            options: BaseOptions::default(),
39            text_edit: TextEdit::default(),
40            #[cfg(feature = "callback")]
41            change_listeners: vec![],
42            #[cfg(feature = "callback")]
43            change_notify_listeners: vec![],
44            _phantom: PhantomData,
45        }
46    }
47}
48
49impl<XMSG> Clone for BaseEditor<XMSG> {
50    fn clone(&self) -> Self {
51        Self {
52            options: self.options.clone(),
53            text_edit: self.text_edit.clone(),
54            #[cfg(feature = "callback")]
55            change_listeners: self.change_listeners.clone(),
56            #[cfg(feature = "callback")]
57            change_notify_listeners: self.change_notify_listeners.clone(),
58            _phantom: self._phantom,
59        }
60    }
61}
62
63#[derive(Debug)]
64pub enum Command {
65    IndentForward,
66    IndentBackward,
67    BreakLine,
68    DeleteBack,
69    DeleteForward,
70    MoveUp,
71    MoveDown,
72    MoveLeft,
73    MoveLeftStart,
74    MoveRight,
75    MoveRightEnd,
76    InsertChar(char),
77    ReplaceChar(char),
78    InsertText(String),
79    PasteTextBlock(String),
80    MergeText(String),
81    /// set a new content to the editor, resetting to a new history for undo/redo
82    SetContent(String),
83    Undo,
84    Redo,
85    BumpHistory,
86    SetSelection(Point2<i32>, Point2<i32>),
87    SelectAll,
88    ClearSelection,
89    SetPosition(Point2<i32>),
90}
91
92pub struct Callback<IN, OUT> {
93    func: Arc<dyn Fn(IN) -> OUT>,
94}
95
96impl<IN, F, OUT> From<F> for Callback<IN, OUT>
97where
98    F: Fn(IN) -> OUT + 'static,
99{
100    fn from(func: F) -> Self {
101        Self {
102            func: Arc::new(func),
103        }
104    }
105}
106
107impl<IN, OUT> Clone for Callback<IN, OUT> {
108    fn clone(&self) -> Self {
109        Self {
110            func: Arc::clone(&self.func),
111        }
112    }
113}
114
115impl<IN, OUT> Callback<IN, OUT> {
116    /// This method calls the actual callback.
117    pub fn emit(&self, input: IN) -> OUT {
118        (self.func)(input)
119    }
120}
121
122impl<XMSG> BaseEditor<XMSG> {
123    pub fn from_str(options: &BaseOptions, content: &str) -> Self {
124        let text_edit = TextEdit::new_from_str(content);
125
126        BaseEditor {
127            options: options.clone(),
128            text_edit,
129            #[cfg(feature = "callback")]
130            change_listeners: vec![],
131            #[cfg(feature = "callback")]
132            change_notify_listeners: vec![],
133            _phantom: PhantomData,
134        }
135    }
136
137    pub fn text_buffer(&self) -> &TextBuffer {
138        self.text_edit.text_buffer()
139    }
140
141    pub fn set_selection(&mut self, start: Point2<i32>, end: Point2<i32>) {
142        self.text_edit.set_selection(start, end);
143    }
144
145    pub fn selection(&self) -> &Selection {
146        self.text_edit.selection()
147    }
148
149    pub fn selected_text(&self) -> Option<String> {
150        match self.options.selection_mode {
151            SelectionMode::Linear => self.text_edit.selected_text_in_linear_mode(),
152            SelectionMode::Block => self.text_edit.selected_text_in_block_mode(),
153        }
154    }
155
156    pub fn cut_selected_text(&mut self) -> Option<String> {
157        match self.options.selection_mode {
158            SelectionMode::Linear => self.text_edit.cut_selected_text_in_linear_mode(),
159            SelectionMode::Block => self.text_edit.cut_selected_text_in_block_mode(),
160        }
161    }
162
163    pub fn is_selected(&self, loc: Point2<i32>) -> bool {
164        match self.options.selection_mode {
165            SelectionMode::Linear => self.text_edit.is_selected_in_linear_mode(loc),
166            SelectionMode::Block => self.text_edit.is_selected_in_block_mode(loc),
167        }
168    }
169
170    pub fn clear_selection(&mut self) {
171        self.text_edit.clear_selection()
172    }
173
174    pub fn set_selection_start(&mut self, start: Point2<i32>) {
175        self.text_edit.set_selection_start(start);
176    }
177
178    pub fn set_selection_end(&mut self, end: Point2<i32>) {
179        self.text_edit.set_selection_end(end);
180    }
181
182    pub fn get_char(&self, loc: Point2<usize>) -> Option<char> {
183        self.text_edit.get_char(loc)
184    }
185
186    pub fn get_position(&self) -> Point2<usize> {
187        self.text_edit.get_position()
188    }
189
190    pub fn get_content(&self) -> String {
191        self.text_edit.get_content()
192    }
193
194    pub fn total_lines(&self) -> usize {
195        self.text_edit.total_lines()
196    }
197}
198
199impl<XMSG> BaseEditor<XMSG> {
200    #[cfg(feature = "callback")]
201    pub fn process_commands(&mut self, commands: impl IntoIterator<Item = Command>) -> Vec<XMSG> {
202        let results: Vec<bool> = commands
203            .into_iter()
204            .map(|command| self.process_command(command))
205            .collect();
206
207        if results.into_iter().any(|v| v) {
208            self.emit_on_change_listeners()
209        } else {
210            vec![]
211        }
212    }
213
214    #[cfg(not(feature = "callback"))]
215    pub fn process_commands(&mut self, _commands: impl IntoIterator<Item = Command>) -> Vec<XMSG> {
216        vec![]
217    }
218
219    /// process the supplied command to text_edit
220    pub fn process_command(&mut self, command: Command) -> bool {
221        match command {
222            Command::IndentForward => {
223                let indent = "    ";
224                self.text_edit.command_insert_text(indent);
225                true
226            }
227            Command::IndentBackward => true,
228            Command::BreakLine => {
229                self.text_edit.command_break_line();
230                true
231            }
232            Command::DeleteBack => {
233                self.text_edit.command_delete_back();
234                true
235            }
236            Command::DeleteForward => {
237                self.text_edit.command_delete_forward();
238                true
239            }
240            Command::MoveUp => {
241                self.command_move_up();
242                false
243            }
244            Command::MoveDown => {
245                self.command_move_down();
246                false
247            }
248            Command::PasteTextBlock(text) => {
249                self.text_edit.paste_text_in_block_mode(text);
250                true
251            }
252            Command::MergeText(text) => {
253                self.text_edit.command_merge_text(text);
254                true
255            }
256            Command::MoveLeft => {
257                self.command_move_left();
258                false
259            }
260            Command::MoveLeftStart => {
261                self.text_edit.command_move_left_start();
262                false
263            }
264            Command::MoveRightEnd => {
265                self.text_edit.command_move_right_end();
266                false
267            }
268            Command::MoveRight => {
269                self.command_move_right();
270                false
271            }
272            Command::InsertChar(c) => {
273                self.text_edit.command_insert_char(c);
274                true
275            }
276            Command::ReplaceChar(c) => {
277                self.text_edit.command_replace_char(c);
278                true
279            }
280            Command::InsertText(text) => {
281                self.text_edit.command_insert_text(&text);
282                true
283            }
284            Command::SetContent(content) => {
285                self.text_edit = TextEdit::new_from_str(&content);
286                true
287            }
288            Command::Undo => {
289                self.text_edit.command_undo();
290                true
291            }
292            Command::Redo => {
293                self.text_edit.command_redo();
294                true
295            }
296            Command::BumpHistory => {
297                self.text_edit.bump_history();
298                false
299            }
300            Command::SetSelection(start, end) => {
301                self.text_edit.command_set_selection(start, end);
302                false
303            }
304            Command::SelectAll => {
305                self.text_edit.command_select_all();
306                false
307            }
308            Command::ClearSelection => {
309                self.text_edit.clear_selection();
310                false
311            }
312            Command::SetPosition(pos) => {
313                self.command_set_position(pos);
314                false
315            }
316        }
317    }
318
319    fn command_move_up(&mut self) {
320        if self.options.use_virtual_edit {
321            self.text_edit.command_move_up();
322        } else {
323            self.text_edit.command_move_up_clamped();
324        }
325    }
326
327    fn command_move_down(&mut self) {
328        if self.options.use_virtual_edit {
329            self.text_edit.command_move_down();
330        } else {
331            self.text_edit.command_move_down_clamped();
332        }
333    }
334
335    fn command_move_left(&mut self) {
336        self.text_edit.command_move_left();
337    }
338
339    fn command_move_right(&mut self) {
340        if self.options.use_virtual_edit {
341            self.text_edit.command_move_right();
342        } else {
343            self.text_edit.command_move_right_clamped();
344        }
345    }
346
347    fn command_set_position(&mut self, loc: Point2<i32>) {
348        let cursor = Point2::new(loc.x as usize, loc.y as usize);
349        if self.options.use_virtual_edit {
350            self.text_edit.command_set_position(cursor);
351        } else {
352            self.text_edit.command_set_position_clamped(cursor);
353        }
354    }
355
356    pub fn clear(&mut self) {
357        self.text_edit.clear();
358    }
359
360    /// Attach a callback to this editor where it is invoked when the content is changed.
361    ///
362    /// Note:The content is extracted into string and used as a parameter to the function.
363    /// This may be a costly operation when the editor has lot of text on it.
364    #[cfg(feature = "callback")]
365    pub fn on_change<F>(mut self, f: F) -> Self
366    where
367        F: Fn(String) -> XMSG + 'static,
368    {
369        let cb = Callback::from(f);
370        self.change_listeners.push(cb);
371        self
372    }
373
374    #[cfg(feature = "callback")]
375    pub fn add_on_change_listener<F>(&mut self, f: F)
376    where
377        F: Fn(String) -> XMSG + 'static,
378    {
379        let cb = Callback::from(f);
380        self.change_listeners.push(cb);
381    }
382
383    /// Attach an callback to this editor where it is invoked when the content is changed.
384    /// The callback function just notifies the parent component that uses the BaseEditor component.
385    /// It will be up to the parent component to extract the content of the editor manually.
386    ///
387    /// This is intended to be used in a debounced or throttled functionality where the component
388    /// decides when to do an expensive operation based on time and recency.
389    ///
390    ///
391    #[cfg(feature = "callback")]
392    pub fn on_change_notify<F>(mut self, f: F) -> Self
393    where
394        F: Fn(()) -> XMSG + 'static,
395    {
396        let cb = Callback::from(f);
397        self.change_notify_listeners.push(cb);
398        self
399    }
400
401    #[cfg(feature = "callback")]
402    pub fn add_on_change_notify<F>(&mut self, f: F)
403    where
404        F: Fn(()) -> XMSG + 'static,
405    {
406        let cb = Callback::from(f);
407        self.change_notify_listeners.push(cb);
408    }
409
410    #[cfg(feature = "callback")]
411    pub fn emit_on_change_listeners(&self) -> Vec<XMSG> {
412        let mut extern_msgs: Vec<XMSG> = vec![];
413        if !self.change_listeners.is_empty() {
414            let content = self.text_edit.get_content();
415            let xmsgs: Vec<XMSG> = self
416                .change_listeners
417                .iter()
418                .map(|listener| listener.emit(content.clone()))
419                .collect();
420            extern_msgs.extend(xmsgs);
421        }
422
423        if !self.change_notify_listeners.is_empty() {
424            let xmsgs: Vec<XMSG> = self
425                .change_notify_listeners
426                .iter()
427                .map(|notify| notify.emit(()))
428                .collect();
429            extern_msgs.extend(xmsgs);
430        }
431
432        extern_msgs
433    }
434
435    pub fn numberline_wide(&self) -> usize {
436        self.text_edit.numberline_wide()
437    }
438}