Skip to main content

freya_code_editor/
editor_line.rs

1use std::borrow::Cow;
2
3use freya_components::scrollviews::ScrollController;
4use freya_core::prelude::*;
5use freya_edit::{
6    EditableEvent,
7    EditorLine,
8    TextEditor,
9};
10use torin::size::Size;
11
12use crate::{
13    editor_data::CodeEditorData,
14    editor_theme::EditorTheme,
15    syntax::TextNode,
16};
17
18#[derive(Clone, PartialEq)]
19pub struct EditorLineUI {
20    pub(crate) editor: Writable<CodeEditorData>,
21    pub(crate) font_size: f32,
22    pub(crate) line_height: f32,
23    pub(crate) line_index: usize,
24    pub(crate) read_only: bool,
25    pub(crate) show_whitespace: bool,
26    pub(crate) font_family: Cow<'static, str>,
27    pub(crate) cursor_mode: CursorMode,
28    pub(crate) theme: EditorTheme,
29    pub(crate) a11y_id: AccessibilityId,
30    pub(crate) scroll_controller: ScrollController,
31}
32
33impl Component for EditorLineUI {
34    fn render_key(&self) -> DiffKey {
35        DiffKey::from(&self.line_index)
36    }
37    fn render(&self) -> impl IntoElement {
38        let EditorLineUI {
39            mut editor,
40            font_size,
41            line_height,
42            line_index,
43            read_only,
44            show_whitespace,
45            font_family,
46            cursor_mode,
47            theme,
48            a11y_id,
49            scroll_controller,
50        } = self.clone();
51
52        let holder = use_state(ParagraphHolder::default);
53
54        let editor_data = editor.read_unchecked();
55
56        let longest_width = editor_data.metrics.longest_width;
57        let line = editor_data.metrics.syntax_blocks.get_line(line_index);
58        let highlights = editor_data.get_visible_selection(EditorLine::Paragraph(line_index));
59        let is_line_selected = editor_data.cursor_row() == line_index;
60
61        let on_tap = {
62            let mut editor = editor.clone();
63            let font_family = font_family.clone();
64            move |e: Event<FocusPressEventData>| {
65                let processed = editor.write_if(|mut editor| {
66                    editor.process(
67                        font_size,
68                        &font_family,
69                        EditableEvent::Down {
70                            location: e.element_location(),
71                            editor_line: EditorLine::Paragraph(line_index),
72                            holder: &holder.read(),
73                        },
74                    )
75                });
76                if processed {
77                    a11y_id.request_focus();
78                    editor
79                        .read()
80                        .scroll_to_cursor(scroll_controller, line_height);
81                }
82            }
83        };
84
85        let on_pointer_move = {
86            let font_family = font_family.clone();
87            move |e: Event<PointerEventData>| {
88                let processed = editor.write_if(|mut editor| {
89                    editor.process(
90                        font_size,
91                        &font_family,
92                        EditableEvent::Move {
93                            location: e.element_location(),
94                            editor_line: EditorLine::Paragraph(line_index),
95                            holder: &holder.read(),
96                        },
97                    )
98                });
99                if processed {
100                    editor
101                        .read()
102                        .scroll_to_cursor(scroll_controller, line_height);
103                }
104            }
105        };
106
107        let cursor_index = if read_only {
108            None
109        } else {
110            is_line_selected.then(|| editor_data.cursor_col())
111        };
112        let visible_selection = match editor_data.get_selection() {
113            None => false,
114            Some((s, e)) if s != e => true,
115            _ => false,
116        };
117        let line_background = if is_line_selected && !visible_selection {
118            theme.line_selected_background
119        } else {
120            Color::TRANSPARENT
121        };
122
123        rect()
124            .horizontal()
125            .height(Size::px(line_height))
126            .background(line_background)
127            .font_size(font_size)
128            .child(
129                paragraph()
130                    .holder(holder.read().clone())
131                    .on_pointer_move(on_pointer_move)
132                    .on_focus_press(on_tap)
133                    .cursor_color(theme.cursor)
134                    .cursor_style(CursorStyle::Block)
135                    .cursor_index(cursor_index)
136                    .cursor_mode(cursor_mode)
137                    .vertical_align(VerticalAlign::Center)
138                    .highlights(highlights.map(|h| vec![h]))
139                    .highlight_color(theme.highlight)
140                    .width(Size::px(longest_width))
141                    .min_width(Size::fill())
142                    .height(Size::fill())
143                    .font_family(font_family)
144                    .max_lines(1)
145                    .color(theme.text)
146                    .spans_iter(line.iter().map(|span| {
147                        let text: Cow<str> = match &span.1 {
148                            TextNode::Range(word_pos) => {
149                                editor_data.rope.slice(word_pos.clone()).into()
150                            }
151                            TextNode::LineOfChars { len, char } => {
152                                if show_whitespace {
153                                    Cow::Owned(char.to_string().repeat(*len))
154                                } else {
155                                    Cow::Owned(" ".repeat(*len))
156                                }
157                            }
158                        };
159                        Span::new(Cow::Owned(text.to_string())).color(span.0)
160                    })),
161            )
162    }
163}
164
165#[derive(Clone, PartialEq)]
166pub(crate) struct EditorGutterLineUI {
167    pub(crate) editor: Writable<CodeEditorData>,
168    pub(crate) font_size: f32,
169    pub(crate) line_height: f32,
170    pub(crate) line_index: usize,
171    pub(crate) theme: EditorTheme,
172}
173
174impl Component for EditorGutterLineUI {
175    fn render_key(&self) -> DiffKey {
176        DiffKey::from(&self.line_index)
177    }
178
179    fn render(&self) -> impl IntoElement {
180        let EditorGutterLineUI {
181            editor,
182            font_size,
183            line_height,
184            line_index,
185            theme,
186        } = self.clone();
187        let editor_data = editor.read();
188        let is_line_selected = editor_data.cursor_row() == line_index;
189        let visible_selection = matches!(
190            editor_data.get_selection(),
191            Some((start, end)) if start != end
192        );
193        let line_background = if is_line_selected && !visible_selection {
194            theme.line_selected_background
195        } else {
196            Color::TRANSPARENT
197        };
198        let gutter_color = if is_line_selected {
199            theme.gutter_selected
200        } else {
201            theme.gutter_unselected
202        };
203
204        rect()
205            .height(Size::px(line_height))
206            .width(Size::fill())
207            .background(line_background)
208            .font_size(font_size)
209            .padding((0., 0., 0., 20.))
210            .main_align(torin::prelude::Alignment::Center)
211            .child(
212                label()
213                    .color(gutter_color)
214                    .text(format!("{} ", line_index + 1)),
215            )
216    }
217}