1use crate::context_menu::{self, Menu, MenuAction};
2use crate::util;
3use css_colors::{rgba, Color, RGBA};
4use sauron::{
5 dom::Measurements, html::attributes::*, html::events::*, html::*, jss_ns_pretty,
6 wasm_bindgen::JsCast, wasm_bindgen_futures::JsFuture, *,
7 web_sys::HtmlElement,
8};
9use std::cell::RefCell;
10use std::rc::Rc;
11pub use ultron_core;
12use ultron_core::{
13 editor, nalgebra::Point2, Ch, Editor, Options, SelectionMode, Style, TextBuffer, TextEdit,
14 TextHighlighter,
15};
16use selection::SelectionSplits;
17pub use mouse_cursor::MouseCursor;
18
19mod selection;
20mod mouse_cursor;
21pub mod custom_element;
22
23pub const COMPONENT_NAME: &str = "ultron";
24pub const CH_WIDTH: u32 = 7;
25pub const CH_HEIGHT: u32 = 16;
26
27#[derive(Debug, Clone)]
28pub enum Msg {
29 EditorMounted(MountEvent),
30 ChangeValue(String),
33 ChangeSyntax(String),
35 ChangeTheme(String),
37 CursorMounted(MountEvent),
38 Keydown(web_sys::KeyboardEvent),
39 Mouseup(web_sys::MouseEvent),
40 Click(web_sys::MouseEvent),
41 Mousedown(web_sys::MouseEvent),
42 Mousemove(web_sys::MouseEvent),
43 Measurements(Measurements),
44 Focused(web_sys::FocusEvent),
45 Blur(web_sys::FocusEvent),
46 ContextMenu(web_sys::MouseEvent),
47 ContextMenuMsg(context_menu::Msg),
48 ScrollCursorIntoView,
49 MenuAction(MenuAction),
50 SetFocus,
52 NoOp,
53}
54
55#[derive(Debug)]
56pub enum Command {
57 EditorCommand(editor::Command),
58 PasteTextBlock(String),
60 MergeText(String),
61 CopyText,
63 CutText,
65}
66
67pub struct WebEditor<XMSG> {
69 options: Options,
70 pub editor: Editor<XMSG>,
71 editor_element: Option<web_sys::Element>,
72 host_element: Option<web_sys::Element>,
74 cursor_element: Option<web_sys::Element>,
75 mouse_cursor: MouseCursor,
76 measure: Measure,
77 is_selecting: bool,
78 text_highlighter: Rc<RefCell<TextHighlighter>>,
79 highlighted_lines: Rc<RefCell<Vec<Vec<(Style, Vec<Ch>)>>>>,
81 animation_frame_handles: Vec<i32>,
82 background_task_handles: Vec<i32>,
83 pub is_focused: bool,
84 context_menu: Menu<Msg>,
85 show_context_menu: bool,
86}
87
88impl<XMSG> Default for WebEditor<XMSG>{
89 fn default() -> Self {
90 Self::from_str(Options::default(), "")
91 }
92}
93
94impl From<editor::Command> for Command {
95 fn from(ecommand: editor::Command) -> Self {
96 Self::EditorCommand(ecommand)
97 }
98}
99
100
101#[derive(Default)]
102struct Measure {
103 average_dispatch: Option<f64>,
104 last_dispatch: Option<f64>,
105}
106
107impl<XMSG> WebEditor<XMSG> {
108 pub fn from_str(options: Options, content: &str) -> Self {
109 let editor = Editor::from_str(options.clone(), content);
110 let mut text_highlighter = TextHighlighter::default();
111 if let Some(theme_name) = &options.theme_name {
112 text_highlighter.select_theme(theme_name);
113 }
114 text_highlighter.set_syntax_token(&options.syntax_token);
115 let highlighted_lines = Rc::new(RefCell::new(Self::highlight_lines(
116 &editor.text_edit,
117 &mut text_highlighter,
118 )));
119 WebEditor {
120 options,
121 editor,
122 editor_element: None,
123 host_element: None,
124 cursor_element: None,
125 mouse_cursor: MouseCursor::default(),
126 measure: Measure::default(),
127 is_selecting: false,
128 text_highlighter: Rc::new(RefCell::new(text_highlighter)),
129 highlighted_lines,
130 animation_frame_handles: vec![],
131 background_task_handles: vec![],
132 is_focused: false,
133 context_menu: Menu::new().on_activate(Msg::MenuAction),
134 show_context_menu: false,
135 }
136 }
137
138 pub fn set_syntax_token(&mut self, syntax_token: &str){
139 self.text_highlighter.borrow_mut().set_syntax_token(syntax_token);
140 self.rehighlight_all();
141 }
142
143 pub fn set_theme(&mut self, theme_name: &str) {
144 self.text_highlighter.borrow_mut().select_theme(theme_name);
145 self.rehighlight_all();
146 }
147
148 pub fn add_on_change_listener<F>(&mut self, f: F)
149 where
150 F: Fn(String) -> XMSG + 'static,
151 {
152 self.editor.add_on_change_listener(f);
153 }
154
155 pub fn add_on_change_notify<F>(&mut self, f: F)
156 where
157 F: Fn(()) -> XMSG + 'static,
158 {
159 self.editor.add_on_change_notify(f);
160 }
161
162 pub fn get_content(&self) -> String {
163 self.editor.get_content()
164 }
165}
166
167impl<XMSG> Component<Msg, XMSG> for WebEditor<XMSG> {
168
169 fn update(&mut self, msg: Msg) -> Effects<Msg, XMSG> {
170 match msg {
171 Msg::EditorMounted(mount_event) => {
172 log::info!("Web editor is mounted..");
173 let mount_element: web_sys::Element = mount_event.target_node.unchecked_into();
174 let root_node = mount_element.get_root_node();
175 if let Some(shadow_root) = root_node.dyn_ref::<web_sys::ShadowRoot>(){
176 let host_element = shadow_root.host();
177 self.host_element = Some(host_element);
178 }
179 self.editor_element = Some(mount_element);
180 Effects::none()
181 }
182 Msg::ChangeValue(content) => {
183 self.process_commands([editor::Command::SetContent(content).into()]);
184 Effects::none()
185 }
186 Msg::ChangeSyntax(syntax_token) => {
187 self.set_syntax_token(&syntax_token);
188 Effects::none()
189 }
190 Msg::ChangeTheme(theme_name) => {
191 self.set_theme(&theme_name);
192 Effects::none()
193 }
194 Msg::CursorMounted(mount_event) => {
195 let cursor_element: web_sys::Element = mount_event.target_node.unchecked_into();
196 self.cursor_element = Some(cursor_element);
197 Effects::none()
198 }
199 Msg::Click(me) => {
200 let client_x = me.client_x();
201 let client_y = me.client_y();
202 let cursor = self.client_to_grid_clamped(client_x, client_y);
203 let msgs = self.editor.process_commands([editor::Command::SetPosition(cursor)]);
204 Effects::new(vec![], msgs)
205 }
206 Msg::Mousedown(me) => {
207 log::info!("mouse down event in ultron..");
208 let client_x = me.client_x();
209 let client_y = me.client_y();
210 let is_primary_btn = me.button() == 0;
211 if is_primary_btn {
212 self.is_selecting = true;
214 let cursor = self.client_to_grid_clamped(client_x, client_y);
215 if self.is_selecting && !self.show_context_menu {
216 self.editor.set_selection_start(cursor);
217 }
218 let msgs = self
219 .editor
220 .process_commands([editor::Command::SetPosition(cursor)]);
221 Effects::new(vec![], msgs).measure()
222 } else {
223 Effects::none()
224 }
225 }
226 Msg::Mousemove(me) => {
227 let client_x = me.client_x();
228 let client_y = me.client_y();
229 let cursor = self.client_to_grid_clamped(client_x, client_y);
230 if self.is_selecting && !self.show_context_menu {
231 let selection = self.editor.selection();
232 if let Some(start) = selection.start {
233 self.editor.set_selection_end(cursor);
234 let msgs = self
235 .editor
236 .process_commands([editor::Command::SetSelection(start, cursor)]);
237 Effects::new(vec![], msgs).measure()
238 } else {
239 Effects::none()
240 }
241 } else {
242 Effects::none()
243 }
244 }
245 Msg::Mouseup(me) => {
246 let client_x = me.client_x();
247 let client_y = me.client_y();
248 let is_primary_btn = me.button() == 0;
249 if is_primary_btn {
250 let cursor = self.client_to_grid_clamped(client_x, client_y);
251 self.editor
252 .process_commands([editor::Command::SetPosition(cursor)]);
253
254 if self.is_selecting {
255 self.is_selecting = false;
256 self.editor.set_selection_end(cursor);
257 let selection = self.editor.selection();
258 if let (Some(start), Some(end)) = (selection.start, selection.end) {
259 let msgs = self
260 .editor
261 .process_commands([editor::Command::SetSelection(start, end)]);
262 Effects::new(vec![], msgs)
263 } else {
264 Effects::none()
265 }
266 } else {
267 Effects::none()
268 }
269 } else {
270 Effects::none()
271 }
272 }
273 Msg::Keydown(ke) => self.process_keypress(&ke),
274 Msg::Measurements(measure) => {
275 self.update_measure(measure);
276 Effects::none()
277 }
278 Msg::Focused(_fe) => {
279 self.is_focused = true;
280 Effects::none()
281 }
282 Msg::SetFocus => {
283 self.is_focused = true;
284 if let Some(editor_element) = &self.editor_element{
285 let html_elm: &HtmlElement = editor_element.unchecked_ref();
286 html_elm.focus().expect("element must focus");
287 }
288 Effects::none()
289 }
290 Msg::Blur(_fe) => {
291 self.is_focused = false;
292 Effects::none()
293 }
294 Msg::ContextMenu(me) => {
295 self.show_context_menu = true;
296 let (start, _end) = self.bounding_rect().expect("must have a bounding rect");
297 let x = me.client_x() - start.x as i32;
298 let y = me.client_y() - start.y as i32;
299 let (msgs, _) = self
300 .context_menu
301 .update(context_menu::Msg::ShowAt(Point2::new(x, y)))
302 .map_msg(Msg::ContextMenuMsg)
303 .unzip();
304 Effects::new(msgs, [])
305 }
306 Msg::ContextMenuMsg(cm_msg) => {
307 let (msgs, xmsg) = self.context_menu.update(cm_msg).unzip();
308 Effects::new(
309 xmsg.into_iter()
310 .chain(msgs.into_iter().map(Msg::ContextMenuMsg)),
311 [],
312 )
313 }
314 Msg::ScrollCursorIntoView => {
315 if self.options.scroll_cursor_into_view {
316 let cursor_element = self.cursor_element.as_ref().unwrap();
317 let mut options = web_sys::ScrollIntoViewOptions::new();
318 options.behavior(web_sys::ScrollBehavior::Smooth);
319 options.block(web_sys::ScrollLogicalPosition::Center);
320 options.inline(web_sys::ScrollLogicalPosition::Center);
321 cursor_element.scroll_into_view_with_scroll_into_view_options(&options);
322 }
323 Effects::none()
324 }
325 Msg::MenuAction(menu_action) => {
326 self.show_context_menu = false;
327 match menu_action {
328 MenuAction::Undo => {
329 self.process_command(Command::EditorCommand(editor::Command::Undo));
330 }
331 MenuAction::Redo => {
332 self.process_command(Command::EditorCommand(editor::Command::Redo));
333 }
334 MenuAction::Cut => {
335 self.cut_selected_text_to_clipboard();
336 }
337 MenuAction::Copy => {
338 self.copy_selected_text_to_clipboard();
339 }
340 MenuAction::Paste => todo!(),
341 MenuAction::Delete => todo!(),
342 MenuAction::SelectAll => {
343 self.process_command(Command::EditorCommand(editor::Command::SelectAll));
344 log::info!("selected text: {:?}", self.selected_text());
345 }
346 }
347 Effects::none()
348 }
349 Msg::NoOp => Effects::none()
350 }
351 }
352
353 fn view(&self) -> Node<Msg> {
354 let enable_context_menu = self.options.enable_context_menu;
355 let enable_keypresses = self.options.enable_keypresses;
356 let enable_click = self.options.enable_click;
357 div(
358 [
359 class(COMPONENT_NAME),
360 classes_flag_namespaced(
361 COMPONENT_NAME,
362 [("occupy_container", self.options.occupy_container)],
363 ),
364 on_mount(Msg::EditorMounted),
365 attributes::tabindex(1),
366 on_keydown(move|ke| {
367 if enable_keypresses{
368 ke.prevent_default();
369 ke.stop_propagation();
370 Msg::Keydown(ke)
371 }else{
372 Msg::NoOp
373 }
374 }),
375 on_click(move|me|{
376 if enable_click{
377 Msg::Click(me)
378 }else{
379 Msg::NoOp
380 }
381 }),
382 tabindex(0),
383 on_focus(Msg::Focused),
384 on_blur(Msg::Blur),
385 on_contextmenu(move|me| {
386 if enable_context_menu{
387 me.prevent_default();
388 me.stop_propagation();
389 Msg::ContextMenu(me)
390 }else{
391 Msg::NoOp
392 }
393 }),
394 style! {
395 cursor: self.mouse_cursor.to_str(),
396 },
397 ],
398 [
399 if self.options.use_syntax_highlighter {
400 self.view_highlighted_lines()
401 } else {
402 self.plain_view()
403 },
404 view_if(self.options.show_status_line, self.view_status_line()),
405 view_if(
406 self.is_focused && self.options.show_cursor,
407 self.view_cursor(),
408 ),
409 view_if(
410 self.is_focused && self.show_context_menu,
411 self.context_menu.view().map_msg(Msg::ContextMenuMsg),
412 ),
413 ],
414 )
415 }
416
417
418 fn style(&self) -> String {
419 let user_select = if self.options.allow_text_selection {
420 "text"
421 } else {
422 "none"
423 };
424 let main = jss_ns_pretty! {COMPONENT_NAME,
425 ".": {
426 position: "relative",
427 font_size: px(14),
428 white_space: "normal",
429 user_select: user_select,
430 "-webkit-user-select": user_select,
431 },
432
433 ".occupy_container": {
434 width: percent(100),
435 height: "auto",
436 },
437
438 "pre code":{
439 white_space: "pre",
440 word_spacing: "normal",
441 word_break: "normal",
442 word_wrap: "normal",
443 },
444
445 ".code_wrapper": {
446 margin: 0,
447 },
448
449 ".code": {
450 position: "relative",
451 font_size: px(14),
452 display: "block",
453 min_width: "max-content",
456 user_select: user_select,
457 "-webkit-user-select": user_select,
458 font_family: "Iosevka Fixed",
459 },
460
461 ".line_block": {
462 display: "block",
463 height: px(CH_HEIGHT),
464 },
465
466 ".number__line": {
468 display: "flex",
469 height: px(CH_HEIGHT),
470 },
471
472 ".number": {
474 flex: "none", text_align: "right",
476 background_color: "#ddd",
477 padding_right: px(CH_WIDTH as f32 * self.numberline_padding_wide() as f32),
478 height: px(CH_HEIGHT),
479 display: "inline-block",
480 user_select: "none",
481 "-webkit-user-select": "none",
482 },
483 ".number_wide1 .number": {
484 width: px(CH_WIDTH),
485 },
486 ".number_wide2 .number": {
488 width: px(2 * CH_WIDTH),
489 },
490 ".number_wide3 .number": {
492 width: px(3 * CH_WIDTH),
493 },
494 ".number_wide4 .number": {
496 width: px(4 * CH_WIDTH),
497 },
498 ".number_wide5 .number": {
500 width: px(5 * CH_WIDTH),
501 },
502
503 ".line": {
505 flex: "none", height: px(CH_HEIGHT),
507 display: "block",
508 user_select: user_select,
509 "-webkit-user-select": user_select,
510 },
511
512 ".line span::selection": {
513 background_color: self.selection_background().to_css(),
514 },
515
516 ".line .selected": {
517 background_color: self.selection_background().to_css(),
518 },
520
521 ".status": {
522 position: "fixed",
523 bottom: 0,
524 display: "flex",
525 flex_direction: "row",
526 user_select: "none",
527 font_family: "Iosevka Fixed",
528 },
529
530 ".virtual_cursor": {
531 position: "absolute",
532 width: px(CH_WIDTH),
533 height: px(CH_HEIGHT),
534 border_width: px(1),
535 border_color: self.cursor_border().to_css(),
536 opacity: 1,
537 border_style: "solid",
538 },
539
540 ".cursor_center":{
541 width: percent(100),
542 height: percent(100),
543 background_color: self.cursor_color().to_css(),
544 opacity: percent(50),
545 animation: "cursor_blink-anim 1000ms step-end infinite",
546 },
547
548 "@keyframes cursor_blink-anim": {
549 "0%": {
550 opacity: percent(0),
551 },
552 "25%": {
553 opacity: percent(25)
554 },
555 "50%": {
556 opacity: percent(100),
557 },
558 "75%": {
559 opacity: percent(75)
560 },
561 "100%": {
562 opacity: percent(0),
563 },
564 },
565 };
566
567 [main, self.context_menu.style()].join("\n")
568 }
569}
570
571impl<XMSG> WebEditor<XMSG> {
572 fn update_measure(&mut self, measure: Measurements) {
573 if let Some(average_dispatch) = self.measure.average_dispatch.as_mut() {
574 *average_dispatch = (*average_dispatch + measure.total_time) / 2.0;
575 } else {
576 self.measure.average_dispatch = Some(measure.total_time);
577 }
578 self.measure.last_dispatch = Some(measure.total_time);
579 }
580
581 pub fn set_mouse_cursor(&mut self, mouse_cursor: MouseCursor) {
582 self.mouse_cursor = mouse_cursor;
583 }
584
585 pub fn get_char(&self,loc: Point2<usize>) -> Option<char> {
586 self.editor.get_char(loc)
587 }
588
589 pub fn get_position(&self) -> Point2<usize> {
590 self.editor.get_position()
591 }
592
593
594 fn rehighlight_all(&mut self) {
595 self.text_highlighter.borrow_mut().reset();
596 *self.highlighted_lines.borrow_mut() = Self::highlight_lines(
597 &self.editor.text_edit,
598 &mut self.text_highlighter.borrow_mut(),
599 );
600 }
601
602 pub fn rehighlight_visible_lines(&mut self) {
604 if let Some((_top, end)) = self.visible_lines(){
605 let text_highlighter = self.text_highlighter.clone();
606 let highlighted_lines = self.highlighted_lines.clone();
607 let lines = self.editor.text_edit.lines();
608 for handle in self.animation_frame_handles.drain(..) {
609 sauron::dom::util::cancel_animation_frame(handle).expect("must cancel");
611 }
612 let closure = move || {
613 let mut text_highlighter = text_highlighter.borrow_mut();
614 text_highlighter.reset();
615 let start = 0;
620 let new_highlighted_lines = lines.iter().skip(start).take(end - start).map(|line| {
621 text_highlighter
622 .highlight_line(line)
623 .expect("must highlight")
624 .into_iter()
625 .map(|(style, line)| (style, line.chars().map(Ch::new).collect()))
626 .collect()
627 });
628
629 for (line, new_highlight) in highlighted_lines
630 .borrow_mut()
631 .iter_mut()
632 .skip(start)
633 .zip(new_highlighted_lines)
634 {
635 *line = new_highlight;
636 }
637 };
638
639 let handle =
640 sauron::dom::util::request_animation_frame(closure).expect("must have a handle");
641
642 self.animation_frame_handles.push(handle);
643 }else{
644 self.rehighlight_all();
645 }
646 }
647
648 pub fn rehighlight_non_visible_lines_in_background(&mut self) {
650 if let Some((_top, end)) = self.visible_lines(){
651 for handle in self.background_task_handles.drain(..) {
652 sauron::dom::util::cancel_timeout_callback(handle).expect("cancel timeout");
653 }
654 let text_highlighter = self.text_highlighter.clone();
655 let highlighted_lines = self.highlighted_lines.clone();
656 let lines = self.editor.text_edit.lines();
657 let closure = move || {
658 let mut text_highlighter = text_highlighter.borrow_mut();
659
660 let new_highlighted_lines = lines.iter().skip(end).map(|line| {
661 text_highlighter
662 .highlight_line(line)
663 .expect("must highlight")
664 .into_iter()
665 .map(|(style, line)| (style, line.chars().map(Ch::new).collect()))
666 .collect()
667 });
668
669 for (line, new_highlight) in highlighted_lines
672 .borrow_mut()
673 .iter_mut()
674 .skip(end)
675 .zip(new_highlighted_lines)
676 {
677 *line = new_highlight;
678 }
679 };
680
681 let handle =
682 sauron::dom::util::request_timeout_callback(closure, 1_000).expect("timeout handle");
683 self.background_task_handles.push(handle);
684 }else{
685 self.rehighlight_all();
686 }
687 }
688
689 pub fn keyevent_to_command(ke: &web_sys::KeyboardEvent) -> Option<Command> {
690 let is_ctrl = ke.ctrl_key();
691 let is_shift = ke.shift_key();
692 let key = ke.key();
693 if key.chars().count() == 1 {
694 let c = key.chars().next().expect("must be only 1 chr");
695 let command = match c {
696 'c' if is_ctrl => Command::CopyText,
697 'x' if is_ctrl => Command::CutText,
698 'v' if is_ctrl => {
699 log::trace!("pasting is handled");
700 Command::PasteTextBlock(String::new())
701 }
702 'z' | 'Z' if is_ctrl => {
703 if is_shift {
704 Command::EditorCommand(editor::Command::Redo)
705 } else {
706 Command::EditorCommand(editor::Command::Undo)
707 }
708 }
709 'r' if is_ctrl => Command::EditorCommand(editor::Command::Redo),
710 'a' if is_ctrl => Command::EditorCommand(editor::Command::SelectAll),
711 _ => Command::EditorCommand(editor::Command::InsertChar(c)),
712 };
713
714 Some(command)
715 } else {
716 let editor_command = match &*key {
717 "Tab" => Some(editor::Command::IndentForward),
718 "Enter" => Some(editor::Command::BreakLine),
719 "Backspace" => Some(editor::Command::DeleteBack),
720 "Delete" => Some(editor::Command::DeleteForward),
721 "ArrowUp" => Some(editor::Command::MoveUp),
722 "ArrowDown" => Some(editor::Command::MoveDown),
723 "ArrowLeft" => Some(editor::Command::MoveLeft),
724 "ArrowRight" => Some(editor::Command::MoveRight),
725 "Home" => Some(editor::Command::MoveLeftStart),
726 "End" => Some(editor::Command::MoveRightEnd),
727 _ => None,
728 };
729 editor_command.map(Command::EditorCommand)
730 }
731 }
732
733 pub fn process_keypress(&mut self, ke: &web_sys::KeyboardEvent) -> Effects<Msg, XMSG> {
735 if let Some(command) = Self::keyevent_to_command(ke) {
736 let msgs = self.process_commands([command]);
737 Effects::new(vec![Msg::ScrollCursorIntoView], msgs).measure()
738 } else {
739 Effects::none()
740 }
741 }
742
743 pub fn process_commands(&mut self, commands: impl IntoIterator<Item = Command>) -> Vec<XMSG> {
744 let results: Vec<bool> = commands
745 .into_iter()
746 .map(|command| self.process_command(command))
747 .collect();
748 if results.into_iter().any(|v| v) {
749 let xmsgs = self.editor.emit_on_change_listeners();
750 if self.options.use_syntax_highlighter{
751 self.rehighlight_visible_lines();
752 self.rehighlight_non_visible_lines_in_background();
753 }
754 if let Some(host_element) = self.host_element.as_ref(){
755 host_element.set_attribute("content", &self.get_content()).expect("set attr content");
756 host_element.dispatch_event(&InputEvent::create_web_event_composed()).expect("dispatch event");
757 }
758 xmsgs
759 } else {
760 vec![]
761 }
762 }
763
764 pub fn highlight_lines(
765 text_edit: &TextEdit,
766 text_highlighter: &mut TextHighlighter,
767 ) -> Vec<Vec<(Style, Vec<Ch>)>> {
768 text_edit
769 .lines()
770 .iter()
771 .map(|line| {
772 text_highlighter
773 .highlight_line(line)
774 .expect("must highlight")
775 .into_iter()
776 .map(|(style, line)| (style, line.chars().map(Ch::new).collect()))
777 .collect()
778 })
779 .collect()
780 }
781
782 fn insert_to_highlighted_line(&mut self, ch: char) {
786 let cursor = self.get_position();
787 let line = cursor.y;
788 let column = cursor.x;
789 if let Some(line) = self.highlighted_lines.borrow_mut().get_mut(line) {
790 let mut width: usize = 0;
791 for (_style, ref mut range) in line.iter_mut() {
792 let range_width = range.iter().map(|range| range.width).sum::<usize>();
793 if column > width && column <= width + range_width {
794 let diff = column - width;
795 range.insert(diff, Ch::new(ch));
796 }
797 width += range_width;
798 }
799 }
800 }
801
802 pub fn process_command(&mut self, command: Command) -> bool {
803 match command {
804 Command::EditorCommand(ecommand) => match ecommand {
805 editor::Command::InsertChar(ch) => {
806 self.insert_to_highlighted_line(ch);
807 self.editor.process_command(ecommand)
808 }
809 _ => self.editor.process_command(ecommand),
810 },
811 Command::PasteTextBlock(text_block) => self
812 .editor
813 .process_command(editor::Command::PasteTextBlock(text_block)),
814 Command::MergeText(text_block) => self
815 .editor
816 .process_command(editor::Command::MergeText(text_block)),
817 Command::CopyText => self.copy_selected_text_to_clipboard(),
818 Command::CutText => self.cut_selected_text_to_clipboard(),
819 }
820 }
821
822 pub fn selected_text(&self) -> Option<String> {
823 self.editor.selected_text()
824 }
825
826 pub fn is_selected(&self, loc: Point2<i32>) -> bool {
827 self.editor.is_selected(loc)
828 }
829
830 pub fn cut_selected_text(&mut self) -> Option<String> {
831 self.editor.cut_selected_text()
832 }
833
834 pub fn clear(&mut self) {
835 self.editor.clear()
836 }
837
838 pub fn set_selection(&mut self, start: Point2<i32>, end: Point2<i32>) {
839 self.editor.set_selection(start, end);
840 }
841
842 pub fn copy_selected_text_to_clipboard(&self) -> bool {
843 log::warn!("Copying text to clipboard..");
844 if let Some(clipboard) = window().navigator().clipboard() {
845 if let Some(selected_text) = self.selected_text() {
846 log::info!("selected text: {selected_text}");
847 let fut = JsFuture::from(clipboard.write_text(&selected_text));
848 sauron::dom::spawn_local(async move {
849 fut.await.expect("must not error");
850 });
851 return true;
852 } else {
853 log::warn!("No selected text..")
854 }
855 } else {
856 log::error!("Clipboard is not supported");
857 }
858 false
859 }
860
861 pub fn cut_selected_text_to_clipboard(&mut self) -> bool {
862 log::warn!("Cutting text to clipboard");
863 let ret = self.copy_selected_text_to_clipboard();
864 self.cut_selected_text();
865 ret
866 }
867
868 pub fn bounding_rect(&self) -> Option<(Point2<f32>, Point2<f32>)> {
870 if let Some(ref editor_element) = self.editor_element {
871 let rect = editor_element.get_bounding_client_rect();
872 let editor_x = rect.x().round() as f32;
873 let editor_y = rect.y().round() as f32;
874 let bottom = rect.bottom().round() as f32;
875 let right = rect.right().round() as f32;
876 Some((Point2::new(editor_x, editor_y), Point2::new(right, bottom)))
877 } else {
878 None
879 }
880 }
881
882 pub fn in_bounds(&self, client_x: f32, client_y: f32) -> bool {
884 if let Some((start, end)) = self.bounding_rect() {
885 client_x >= start.x && client_x <= end.x && client_y >= start.y && client_y <= end.y
886 } else {
887 false
888 }
889 }
890
891 pub fn editor_offset(&self) -> Option<Point2<f32>> {
892 if let Some((start, _end)) = self.bounding_rect() {
893 Some(start)
894 } else {
895 None
896 }
897 }
898
899 pub fn relative_client(&self, client_x: i32, client_y: i32) -> Point2<i32> {
901 let editor = self.editor_offset().expect("must have an editor offset");
902 let x = client_x as f32 - editor.x;
903 let y = client_y as f32 - editor.y;
904 Point2::new(x.round() as i32, y.round() as i32)
905 }
906
907 pub(crate) fn numberline_padding_wide(&self) -> usize {
909 1
910 }
911
912 fn theme_background(&self) -> RGBA {
913 let default = rgba(255, 255, 255, 1.0);
914 self.text_highlighter
915 .borrow()
916 .active_theme()
917 .settings
918 .background
919 .map(util::to_rgba)
920 .unwrap_or(default)
921 }
922
923 fn gutter_background(&self) -> RGBA {
924 let default = rgba(0, 0, 0, 1.0);
925 self.text_highlighter
926 .borrow()
927 .active_theme()
928 .settings
929 .gutter
930 .map(util::to_rgba)
931 .unwrap_or(default)
932 }
933
934 fn gutter_foreground(&self) -> RGBA {
935 let default = rgba(0, 0, 0, 1.0);
936 self.text_highlighter
937 .borrow()
938 .active_theme()
939 .settings
940 .gutter_foreground
941 .map(util::to_rgba)
942 .unwrap_or(default)
943 }
944
945 fn selection_background(&self) -> RGBA {
946 let default = rgba(0, 0, 255, 1.0);
947 self.text_highlighter
948 .borrow()
949 .active_theme()
950 .settings
951 .selection
952 .map(util::to_rgba)
953 .unwrap_or(default)
954 }
955
956 fn cursor_color(&self) -> RGBA {
957 rgba(0, 0, 0, 1.0)
958 }
959
960 fn cursor_border(&self) -> RGBA {
961 rgba(0, 0, 0, 1.0)
962 }
963
964 fn numberline_wide_with_padding(&self) -> usize {
966 if self.options.show_line_numbers {
967 self.editor.total_lines().to_string().len() + self.numberline_padding_wide()
968 } else {
969 0
970 }
971 }
972
973 pub fn total_lines(&self) -> usize {
974 self.editor.total_lines()
975 }
976
977 pub fn client_to_grid(&self, client_x: i32, client_y: i32) -> Point2<i32> {
979 let numberline_wide_with_padding = self.numberline_wide_with_padding() as f32;
980 let editor = self.editor_offset().expect("must have an editor offset");
981 let col = (client_x as f32 - editor.x) / CH_WIDTH as f32 - numberline_wide_with_padding;
982 let line = (client_y as f32 - editor.y) / CH_HEIGHT as f32;
983 let x = col.floor() as i32;
984 let y = line.floor() as i32;
985 Point2::new(x, y)
986 }
987
988 pub fn client_to_grid_clamped(&self, client_x: i32, client_y: i32) -> Point2<i32> {
991 let cursor = self.client_to_grid(client_x, client_y);
992 util::clamp_to_edge(cursor)
993 }
994
995 pub fn cursor_to_client(&self) -> Point2<f32> {
997 let cursor = self.editor.get_position();
998 Point2::new(
999 (cursor.x + self.numberline_wide_with_padding()) as f32 * CH_WIDTH as f32,
1000 cursor.y as f32 * CH_HEIGHT as f32,
1001 )
1002 }
1003
1004 fn number_line_with_padding_width(&self) -> f32 {
1006 self.numberline_wide_with_padding() as f32 * CH_WIDTH as f32
1007 }
1008
1009 fn view_cursor(&self) -> Node<Msg> {
1010 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1011 let cursor = self.cursor_to_client();
1012 div(
1013 [
1014 class_ns("virtual_cursor"),
1015 style! {
1016 top: px(cursor.y),
1017 left: px(cursor.x),
1018 },
1019 on_mount(Msg::CursorMounted),
1020 ],
1021 [div([class_ns("cursor_center")], [])],
1022 )
1023 }
1024
1025 pub fn view_status_line<MSG>(&self) -> Node<MSG> {
1027 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1028 let cursor = self.editor.get_position();
1029
1030 div(
1031 [
1032 class_ns("status"),
1033 style! {
1034 background_color: self.gutter_background().to_css(),
1035 color: self.gutter_foreground().to_css(),
1036 height: px(self.status_line_height()),
1037 left: px(self.number_line_with_padding_width())
1038 },
1039 ],
1040 [
1041 text!(" |> line: {}, col: {} ", cursor.y + 1, cursor.x + 1),
1042 text!(" |> version:{}", env!("CARGO_PKG_VERSION")),
1043 text!(" |> lines: {}", self.editor.total_lines()),
1044 if let Some((start, end)) = self.bounding_rect() {
1045 text!(" |> bounding rect: {}->{}", start, end)
1046 } else {
1047 text!("")
1048 },
1049 if let Some(visible_lines) = self.max_visible_lines() {
1050 text!(" |> visible lines: {}", visible_lines)
1051 } else {
1052 text!("")
1053 },
1054 if let Some((start, end)) = self.visible_lines() {
1055 text!(" |> lines: ({},{})", start, end)
1056 } else {
1057 text!("")
1058 },
1059 text!(" |> selection: {:?}", self.editor.selection()),
1060 if let Some(average_dispatch) = self.measure.average_dispatch {
1061 text!(" |> average dispatch: {}ms", average_dispatch.round())
1062 } else {
1063 text!("")
1064 },
1065 if let Some(last_dispatch) = self.measure.last_dispatch {
1066 text!(" |> latest: {}ms", last_dispatch.round())
1067 } else {
1068 text!("")
1069 },
1070 ],
1071 )
1072 }
1073
1074 fn view_line_number<MSG>(&self, line_number: usize) -> Node<MSG> {
1075 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1076 view_if(
1077 self.options.show_line_numbers,
1078 span(
1079 [
1080 class_ns("number"),
1081 style! {
1082 background_color: self.gutter_background().to_css(),
1083 color: self.gutter_foreground().to_css(),
1084 },
1085 ],
1086 [text(line_number)],
1087 ),
1088 )
1089 }
1090
1091 fn max_visible_lines(&self) -> Option<usize> {
1093 if let Some((start, end)) = self.bounding_rect() {
1094 Some(((end.y - start.y) / CH_HEIGHT as f32).round() as usize)
1095 } else {
1096 None
1097 }
1098 }
1099
1100 fn visible_lines(&self) -> Option<(usize, usize)> {
1102 if let Some((start, end)) = self.bounding_rect() {
1103 let ch_height = CH_HEIGHT as f32;
1104 let top = ((0.0 - start.y) / ch_height) as usize;
1105 let bottom = ((end.y - 2.0 * start.y) / ch_height) as usize;
1106 Some((top, bottom))
1107 } else {
1108 None
1109 }
1110 }
1111
1112 fn view_highlighted_line<MSG>(
1113 &self,
1114 line_index: usize,
1115 line: &[(Style, Vec<Ch>)],
1116 ) -> Vec<Node<MSG>> {
1117 let mut range_x: usize = 0;
1119 line.iter()
1120 .map(|(style, range)| {
1121 let range_str = String::from_iter(range.iter().map(|ch| ch.ch));
1122
1123 let range_start = Point2::new(range_x, line_index);
1124 range_x += range.iter().map(|ch| ch.width).sum::<usize>();
1125 let range_end = Point2::new(range_x, line_index);
1126
1127 let foreground = util::to_rgba(style.foreground).to_css();
1128
1129 let selection_splits = match self.editor.text_edit.selection_reorder_casted() {
1130 Some((start, end)) => {
1131 let selection_in_same_line = start.y == end.y;
1133 let selection_start_within_first_line = line_index == start.y;
1135 let selection_end_within_last_line = line_index == end.y;
1137 let line_within_selection = line_index > start.y && line_index < end.y;
1139 let line_outside_selection = line_index < start.y || line_index > end.y;
1140
1141 let selection_start_within_range_start = start.x >= range_start.x;
1143 let selection_end_within_range_end = end.x <= range_end.x;
1145 let selection_within_range =
1147 start.x >= range_start.x && end.x <= range_end.x;
1148
1149 let range_in_right_of_selection_start =
1151 range_start.x >= start.x && range_end.x >= start.x;
1152 let range_in_left_of_selection_end =
1153 range_start.x <= end.x && range_end.x <= end.x;
1154 let range_in_right_of_selection_end =
1155 range_start.x > end.x && range_end.x > end.x;
1156
1157 let text_buffer = TextBuffer::from_ch(&[range]);
1158
1159 if line_within_selection {
1160 SelectionSplits::SelectAll(range_str)
1161 } else if line_outside_selection {
1162 SelectionSplits::NotSelected(range_str)
1163 } else if selection_in_same_line {
1164 let range_within_selection =
1165 range_start.x >= start.x && range_end.x <= end.x;
1166 if range_within_selection {
1167 SelectionSplits::SelectAll(range_str)
1168 } else if selection_within_range {
1169 let break1 = Point2::new(start.x - range_start.x, 0);
1173 let break1 = text_buffer.clamp_position(break1);
1174 let break2 = Point2::new(end.x - range_start.x, 0);
1175 let break2 = text_buffer.clamp_position(break2);
1176 let (first, second, third) =
1177 text_buffer.split_line_at_2_points(break1, break2);
1178 SelectionSplits::SelectMiddle(first, second, third)
1179 } else if selection_start_within_range_start {
1180 let break1 = Point2::new(start.x - range_start.x, 0);
1181 let break1 = text_buffer.clamp_position(break1);
1182 let (first, second) = text_buffer.split_line_at_point(break1);
1183 SelectionSplits::SelectRight(first, second)
1184 } else if range_in_right_of_selection_end {
1185 SelectionSplits::NotSelected(range_str)
1186 } else if selection_end_within_range_end {
1187 let break1 = Point2::new(end.x - range_start.x, 0);
1190 let break1 = text_buffer.clamp_position(break1);
1191 let (first, second) = text_buffer.split_line_at_point(break1);
1192 SelectionSplits::SelectLeft(first, second)
1193 } else {
1194 SelectionSplits::NotSelected(range_str)
1195 }
1196 } else if selection_start_within_first_line {
1197 if range_in_right_of_selection_start {
1198 SelectionSplits::SelectAll(range_str)
1199 } else if selection_start_within_range_start {
1200 let break1 = Point2::new(start.x - range_start.x, 0);
1201 let break1 = text_buffer.clamp_position(break1);
1202 let (first, second) = text_buffer.split_line_at_point(break1);
1203 SelectionSplits::SelectRight(first, second)
1204 } else {
1205 SelectionSplits::NotSelected(range_str)
1206 }
1207 } else if selection_end_within_last_line {
1208 if range_in_left_of_selection_end {
1209 SelectionSplits::SelectAll(range_str)
1210 } else if range_in_right_of_selection_end {
1211 SelectionSplits::NotSelected(range_str)
1212 } else if selection_end_within_range_end {
1213 let break1 = Point2::new(end.x - range_start.x, 0);
1216 let break1 = text_buffer.clamp_position(break1);
1217 let (first, second) = text_buffer.split_line_at_point(break1);
1218 SelectionSplits::SelectLeft(first, second)
1219 } else {
1220 SelectionSplits::NotSelected(range_str)
1221 }
1222 } else {
1223 SelectionSplits::NotSelected(range_str)
1224 }
1225 }
1226 None => SelectionSplits::NotSelected(range_str),
1227 };
1228 selection_splits.view_with_style(style! { color: foreground })
1229 })
1230 .collect()
1231 }
1232
1233 pub fn view_highlighted_lines<MSG>(&self) -> Node<MSG> {
1235 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1236 let class_number_wide = format!("number_wide{}", self.editor.numberline_wide());
1237
1238 let code_attributes = [
1239 class_ns("code"),
1240 class_ns(&class_number_wide),
1241 style! {background: self.theme_background().to_css()},
1242 ];
1243
1244 let highlighted_lines = self.highlighted_lines.borrow();
1245 let rendered_lines = highlighted_lines
1246 .iter()
1247 .enumerate()
1248 .map(|(line_index, line)| {
1249 div([class_ns("line")], {
1250 [self.view_line_number(line_index + 1)]
1251 .into_iter()
1252 .chain(self.view_highlighted_line(line_index, line))
1253 .collect::<Vec<_>>()
1254 })
1255 });
1256
1257 if self.options.use_for_ssg {
1258 div(code_attributes, rendered_lines)
1261 } else {
1262 pre(
1266 [class_ns("code_wrapper")],
1267 [code(code_attributes, rendered_lines)],
1268 )
1269 }
1270 }
1271
1272 pub fn plain_view<MSG>(&self) -> Node<MSG> {
1273 self.view_text_edit()
1274 }
1275
1276 pub fn status_line_height(&self) -> i32 {
1278 30
1279 }
1280
1281 fn view_line_with_linear_selection<MSG>(&self, line_index: usize, line: String) -> Node<MSG> {
1282 let line_width = self.editor.text_edit.text_buffer.line_width(line_index);
1283 let line_end = Point2::new(line_width, line_index);
1284
1285 let selection_splits = match self.editor.text_edit.selection_reorder_casted() {
1286 Some((start, end)) => {
1287 let in_inner_line = line_index > start.y && line_index < end.y;
1289
1290 if in_inner_line {
1291 SelectionSplits::SelectAll(line)
1292 } else {
1293 let in_same_line = start.y == end.y;
1295 let in_first_line = line_index == start.y;
1297 let in_last_line = line_index == end.y;
1299 let text_buffer = &self.editor.text_edit.text_buffer;
1300 if in_first_line {
1301 let break1 = Point2::new(start.x, line_index);
1304 let break1 = text_buffer.clamp_position(break1);
1305 let (first, second) = text_buffer.split_line_at_point(break1);
1306 if in_same_line {
1307 let break2 = Point2::new(end.x, line_end.y);
1309 let break2 = text_buffer.clamp_position(break2);
1310 let (first, second, third) =
1311 text_buffer.split_line_at_2_points(break1, break2);
1312 SelectionSplits::SelectMiddle(first, second, third)
1313 } else {
1314 SelectionSplits::SelectRight(first, second)
1315 }
1316 } else if in_last_line {
1317 let break1 = Point2::new(end.x, line_index);
1320 let break1 = text_buffer.clamp_position(break1);
1321 let (first, second) = text_buffer.split_line_at_point(break1);
1322 SelectionSplits::SelectLeft(first, second)
1323 } else {
1324 SelectionSplits::NotSelected(line)
1325 }
1326 }
1327 }
1328 None => SelectionSplits::NotSelected(line),
1329 };
1330 selection_splits.view()
1331 }
1332
1333 fn view_line_with_block_selection<MSG>(&self, line_index: usize, line: String) -> Node<MSG> {
1335 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1336
1337 let default_view = span([], [text(&line)]);
1338 match self.editor.text_edit.selection_normalized_casted() {
1339 Some((start, end)) => {
1340 let text_buffer = &self.editor.text_edit.text_buffer;
1341
1342 let break1 = Point2::new(start.x, line_index);
1347 let break1 = text_buffer.clamp_position(break1);
1348
1349 let break2 = Point2::new(end.x, line_index);
1350 let break2 = text_buffer.clamp_position(break2);
1351 let (first, second, third) = text_buffer.split_line_at_2_points(break1, break2);
1352
1353 if line_index >= start.y && line_index <= end.y {
1354 span(
1355 [],
1356 [
1357 span([], [text(first)]),
1358 span([class_ns("selected")], [text(second)]),
1359 span([], [text(third)]),
1360 ],
1361 )
1362 } else {
1363 default_view
1364 }
1365 }
1366 _ => default_view,
1367 }
1368 }
1369
1370 pub fn view_text_edit<MSG>(&self) -> Node<MSG> {
1371 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1372 let text_edit = &self.editor.text_edit;
1373
1374 let class_number_wide = format!("number_wide{}", text_edit.numberline_wide());
1375
1376 let code_attributes = [class_ns("code"), class_ns(&class_number_wide)];
1377 let rendered_lines = text_edit
1378 .lines()
1379 .into_iter()
1380 .enumerate()
1381 .map(|(line_index, line)| {
1382 let line_number = line_index + 1;
1383 div(
1384 [class_ns("line")],
1385 [
1386 view_if(
1387 self.options.show_line_numbers,
1388 span([class_ns("number")], [text(line_number)]),
1389 ),
1390 match self.options.selection_mode {
1391 SelectionMode::Linear => {
1392 self.view_line_with_linear_selection(line_index, line)
1393 }
1394 SelectionMode::Block => {
1395 self.view_line_with_block_selection(line_index, line)
1396 }
1397 },
1398 ],
1399 )
1400 });
1401
1402 if self.options.use_for_ssg {
1403 div(code_attributes, rendered_lines)
1406 } else {
1407 pre(
1411 [class_ns("code_wrapper")],
1412 [code(code_attributes, rendered_lines)],
1413 )
1414 }
1415 }
1416}
1417
1418pub fn view_text_buffer<MSG>(text_buffer: &TextBuffer, options: &Options) -> Node<MSG> {
1419 let class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1420
1421 let class_number_wide = format!("number_wide{}", text_buffer.numberline_wide());
1422
1423 let code_attributes = [class_ns("code"), class_ns(&class_number_wide)];
1424 let rendered_lines = text_buffer
1425 .lines()
1426 .into_iter()
1427 .enumerate()
1428 .map(|(line_index, line)| {
1429 let line_number = line_index + 1;
1430 div(
1431 [class_ns("line")],
1432 [
1433 view_if(
1434 options.show_line_numbers,
1435 span([class_ns("number")], [text(line_number)]),
1436 ),
1437 span([], [text(line)]),
1440 ],
1441 )
1442 });
1443
1444 if options.use_for_ssg {
1445 div(code_attributes, rendered_lines)
1448 } else {
1449 pre(
1453 [class_ns("code_wrapper")],
1454 [code(code_attributes, rendered_lines)],
1455 )
1456 }
1457}
1458
1459
1460