ultron_web/
web_editor.rs

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    /// Discard current editor content if any, and use this new value
31    /// This is triggered from the top-level DOM of this component
32    ChangeValue(String),
33    /// Syntax token is changed
34    ChangeSyntax(String),
35    /// Change the theme of the editor
36    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    /// set focus to the editor
51    SetFocus,
52    NoOp,
53}
54
55#[derive(Debug)]
56pub enum Command {
57    EditorCommand(editor::Command),
58    /// execute paste text
59    PasteTextBlock(String),
60    MergeText(String),
61    /// execute copy text
62    CopyText,
63    /// execute cut text
64    CutText,
65}
66
67/// rename this to WebEditor
68pub struct WebEditor<XMSG> {
69    options: Options,
70    pub editor: Editor<XMSG>,
71    editor_element: Option<web_sys::Element>,
72    /// the host element the web editor is mounted to, when mounted as a custom web component
73    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    /// lines of highlighted ranges
80    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.editor.clear_selection();
213                    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                // to make the background color extend to the longest line, otherwise only the
454                // longest lines has a background-color leaving the shorter lines ugly
455                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 and line
467            ".number__line": {
468                display: "flex",
469                height: px(CH_HEIGHT),
470            },
471
472            // numbers
473            ".number": {
474                flex: "none", // dont compress the numbers
475                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            // when line number is in between: 10 - 99
487            ".number_wide2 .number": {
488                width: px(2 * CH_WIDTH),
489            },
490            // when total lines is in between 100 - 999
491            ".number_wide3 .number": {
492                width: px(3 * CH_WIDTH),
493            },
494            // when total lines is in between 1000 - 9000
495            ".number_wide4 .number": {
496                width: px(4 * CH_WIDTH),
497            },
498            // 10000 - 90000
499            ".number_wide5 .number": {
500                width: px(5 * CH_WIDTH),
501            },
502
503            // line content
504            ".line": {
505                flex: "none", // dont compress lines
506                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               //background_color: rgba(221, 72, 20, 1.0).to_css(),
519            },
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    /// rehighlight from 0 to the end of the visible lines
603    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                //cancel the old ones
610                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                // Note: we are just starting from the very top.
616                // The alternative would be to save parse state, and start to the line
617                // where the parse state didn't change at that location, but that would be a much
618                // complex code
619                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    /// rehighlight the rest of the lines that are not visible
649    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                // TODO: there could be a bug here, what if the new and old highlighted lines have
670                // different length, it will only iterate to which ever is the shorted length.
671                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    /// make this into keypress to command
734    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    /// insert the newly typed character to the highlighted line
783    /// Note: This is a hacky way to have a visual feedback for the users to see
784    /// the typed letter, the highlighter will eventually color it when it is done running
785    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    /// calculate the bounding rect of the editor using a DOM call [getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
869    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    /// check if this mouse client x and y is inside the editor bounds
883    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    /// calculate the points relative to the editor bounding box
900    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    /// the padding of the number line width
908    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    /// how wide the numberline based on the character lengths of the number
965    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    /// convert screen coordinate to grid coordinate taking into account the editor element
978    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    /// convert screen coordinate to grid coordinate
989    /// clamped negative values due to padding in the line number
990    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    /// convert current cursor position to client coordinate relative to the editor div
996    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    /// calculate the width of the numberline including the padding
1005    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    /// the view for the status line
1026    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    /// calculate the maximum number of visible lines
1092    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    /// calculate which lines are visible in the editor
1101    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 class_ns = |class_names| attributes::class_namespaced(COMPONENT_NAME, class_names);
1118        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                        // selection end points is only on the same line
1132                        let selection_in_same_line = start.y == end.y;
1133                        // this line is on the first line of selection
1134                        let selection_start_within_first_line = line_index == start.y;
1135                        // this line is on the last line of selection
1136                        let selection_end_within_last_line = line_index == end.y;
1137                        // this line is in between the selection end points
1138                        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                        // the start selection is within this range  location
1142                        let selection_start_within_range_start = start.x >= range_start.x;
1143                        // the end selection is within this range location
1144                        let selection_end_within_range_end = end.x <= range_end.x;
1145                        // both selection endpoints is inside this range
1146                        let selection_within_range =
1147                            start.x >= range_start.x && end.x <= range_end.x;
1148
1149                        // range is in the right side of selection start
1150                        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                                // the first is plain
1170                                // the second is selected
1171                                // the third is plain
1172                                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                                // the first is selected
1188                                // the second is plain
1189                                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                                // the first is selected
1214                                // the second is plain
1215                                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    // highlighted view
1234    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            // using div works well when select-copying for both chrome and firefox
1259            // this is ideal for statis site generation highlighting
1260            div(code_attributes, rendered_lines)
1261        } else {
1262            // using <pre><code> works well when copying in chrome
1263            // but in firefox, it creates a double line when select-copying the text
1264            // whe need to use <pre><code> in order for typing whitespace works.
1265            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    /// height of the status line which displays editor infor such as cursor location
1277    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                // this line is in between the selection end points
1288                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                    // selection end points is only on the same line
1294                    let in_same_line = start.y == end.y;
1295                    // this line is on the first line of selection
1296                    let in_first_line = line_index == start.y;
1297                    // this line is on the last line of selection
1298                    let in_last_line = line_index == end.y;
1299                    let text_buffer = &self.editor.text_edit.text_buffer;
1300                    if in_first_line {
1301                        // the first part is the plain
1302                        // the second part is the highlighted
1303                        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                            // the third part will be in plain
1308                            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                        // the first part is the highlighted
1318                        // the second part is plain
1319                        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    //TODO: this needs fixing, as we are accessing characters that may not not in the right index
1334    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                // there will be 3 parts
1343                // the first one is plain
1344                // the second one is highlighted
1345                // the third one is plain
1346                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            // using div works well when select-copying for both chrome and firefox
1404            // this is ideal for static site generation highlighting
1405            div(code_attributes, rendered_lines)
1406        } else {
1407            // using <pre><code> works well when copying in chrome
1408            // but in firefox, it creates a double line when select-copying the text
1409            // whe need to use <pre><code> in order for typing whitespace works.
1410            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                    // Note: this is important since text node with empty
1438                    // content seems to cause error when finding the dom in rust
1439                    span([], [text(line)]),
1440                ],
1441            )
1442        });
1443
1444    if options.use_for_ssg {
1445        // using div works well when select-copying for both chrome and firefox
1446        // this is ideal for static site generation highlighting
1447        div(code_attributes, rendered_lines)
1448    } else {
1449        // using <pre><code> works well when copying in chrome
1450        // but in firefox, it creates a double line when select-copying the text
1451        // whe need to use <pre><code> in order for typing whitespace works.
1452        pre(
1453            [class_ns("code_wrapper")],
1454            [code(code_attributes, rendered_lines)],
1455        )
1456    }
1457}
1458
1459
1460