Skip to main content

floem/views/editor/
view.rs

1use std::{collections::HashMap, ops::RangeInclusive, rc::Rc};
2
3use crate::{
4    Renderer,
5    action::{set_ime_allowed, set_ime_cursor_area},
6    context::{LayoutCx, PaintCx, UpdateCx},
7    event::{Event, EventListener, EventPropagation},
8    kurbo::{BezPath, Line, Point, Rect, Size, Vec2},
9    peniko::Color,
10    reactive::{Effect, Memo, RwSignal, Scope},
11    style::{CursorStyle, Style},
12    style_class,
13    taffy::tree::NodeId,
14    text::{Attrs, AttrsList, TextLayout},
15    view::ViewId,
16    view::{IntoView, View},
17    views::{Decorators, Scroll, Stack, editor::keypress::KeypressKey},
18};
19use floem_editor_core::{
20    command::EditCommand,
21    cursor::{ColPosition, CursorAffinity, CursorMode},
22    mode::{Mode, VisualMode},
23};
24use floem_reactive::{SignalGet, SignalTrack, SignalUpdate, SignalWith};
25use ui_events::{
26    keyboard::{Key, KeyState, KeyboardEvent, Modifiers},
27    pointer::{PointerButton, PointerButtonEvent, PointerEvent},
28};
29
30use crate::views::editor::{
31    command::CommandExecuted,
32    gutter::editor_gutter_view,
33    layout::LineExtraStyle,
34    visual_line::{RVLine, VLineInfo},
35};
36
37use super::{CHAR_WIDTH, Editor, command::Command};
38
39#[derive(Clone, Copy, PartialEq, Eq)]
40pub enum DiffSectionKind {
41    NoCode,
42    Added,
43    Removed,
44}
45
46#[derive(Clone, PartialEq)]
47pub struct DiffSection {
48    /// The y index that the diff section is at.
49    ///
50    /// This is multiplied by the line height to get the y position.
51    ///
52    /// So this can roughly be considered as the `VLine` of the start of this diff section, but it
53    /// isn't necessarily convertible to a `VLine` due to jumping over empty code sections.
54    pub y_idx: usize,
55    pub height: usize,
56    pub kind: DiffSectionKind,
57}
58
59// TODO(minor): We have diff sections in screen lines because Lapce uses them, but
60// we don't really have support for diffs in floem-editor! Is there a better design for this?
61// Possibly we should just move that out to a separate field on Lapce's editor.
62#[derive(Clone, PartialEq)]
63pub struct ScreenLines {
64    pub lines: Rc<Vec<RVLine>>,
65    /// Guaranteed to have an entry for each `VLine` in `lines`
66    /// You should likely use accessor functions rather than this directly.
67    pub info: Rc<HashMap<RVLine, LineInfo>>,
68    pub diff_sections: Option<Rc<Vec<DiffSection>>>,
69    /// The base y position that all the y positions inside `info` are relative to.
70    /// This exists so that if a text layout is created outside of the view, we don't have to
71    /// completely recompute the screen lines (or do somewhat intricate things to update them)
72    /// we simply have to update the `base_y`.
73    pub base: RwSignal<ScreenLinesBase>,
74}
75impl ScreenLines {
76    pub fn new(cx: Scope, viewport: Rect) -> ScreenLines {
77        ScreenLines {
78            lines: Default::default(),
79            info: Default::default(),
80            diff_sections: Default::default(),
81            base: cx.create_rw_signal(ScreenLinesBase {
82                active_viewport: viewport,
83            }),
84        }
85    }
86
87    pub fn is_empty(&self) -> bool {
88        self.lines.is_empty()
89    }
90
91    pub fn clear(&mut self, viewport: Rect) {
92        self.lines = Default::default();
93        self.info = Default::default();
94        self.diff_sections = Default::default();
95        self.base.set(ScreenLinesBase {
96            active_viewport: viewport,
97        });
98    }
99
100    /// Get the line info for the given rvline.
101    pub fn info(&self, rvline: RVLine) -> Option<LineInfo> {
102        let info = self.info.get(&rvline)?;
103        let base = self.base.get();
104
105        Some(info.clone().with_base(base))
106    }
107
108    pub fn vline_info(&self, rvline: RVLine) -> Option<VLineInfo<()>> {
109        self.info.get(&rvline).map(|info| info.vline_info)
110    }
111
112    pub fn rvline_range(&self) -> Option<(RVLine, RVLine)> {
113        self.lines.first().copied().zip(self.lines.last().copied())
114    }
115
116    /// Iterate over the line info, copying them with the full y positions.
117    pub fn iter_line_info(&self) -> impl Iterator<Item = LineInfo> + '_ {
118        self.lines.iter().map(|rvline| self.info(*rvline).unwrap())
119    }
120
121    /// Iterate over the line info within the range, copying them with the full y positions.
122    ///
123    /// If the values are out of range, it is clamped to the valid lines within.
124    pub fn iter_line_info_r(
125        &self,
126        r: RangeInclusive<RVLine>,
127    ) -> impl Iterator<Item = LineInfo> + '_ {
128        // We search for the start/end indices due to not having a good way to iterate over
129        // successive rvlines without the view.
130        // This should be good enough due to lines being small.
131        let start_idx = self.lines.binary_search(r.start()).ok().or_else(|| {
132            if self.lines.first().map(|l| r.start() < l).unwrap_or(false) {
133                Some(0)
134            } else {
135                // The start is past the start of our lines
136                None
137            }
138        });
139
140        let end_idx = self.lines.binary_search(r.end()).ok().or_else(|| {
141            if self.lines.last().map(|l| r.end() > l).unwrap_or(false) {
142                Some(self.lines.len() - 1)
143            } else {
144                // The end is before the end of our lines but not available
145                None
146            }
147        });
148
149        if let (Some(start_idx), Some(end_idx)) = (start_idx, end_idx) {
150            self.lines.get(start_idx..=end_idx)
151        } else {
152            // Hacky method to get an empty iterator of the same type
153            self.lines.get(0..0)
154        }
155        .into_iter()
156        .flatten()
157        .copied()
158        .map(|rvline| self.info(rvline).unwrap())
159    }
160
161    pub fn iter_vline_info(&self) -> impl Iterator<Item = VLineInfo<()>> + '_ {
162        self.lines
163            .iter()
164            .map(|vline| &self.info[vline].vline_info)
165            .copied()
166    }
167
168    pub fn iter_vline_info_r(
169        &self,
170        r: RangeInclusive<RVLine>,
171    ) -> impl Iterator<Item = VLineInfo<()>> + '_ {
172        // TODO(minor): this should probably skip tracking?
173        self.iter_line_info_r(r).map(|x| x.vline_info)
174    }
175
176    /// Iter the real lines underlying the visual lines on the screen
177    pub fn iter_lines(&self) -> impl Iterator<Item = usize> + '_ {
178        // We can just assume that the lines stored are contiguous and thus just get the first
179        // buffer line and then the last buffer line.
180        let start_vline = self.lines.first().copied().unwrap_or_default();
181        let end_vline = self.lines.last().copied().unwrap_or_default();
182
183        let start_line = self.info(start_vline).unwrap().vline_info.rvline.line;
184        let end_line = self.info(end_vline).unwrap().vline_info.rvline.line;
185
186        start_line..=end_line
187    }
188
189    /// Iterate over the real lines underlying the visual lines on the screen with the y position
190    /// of their layout.
191    ///
192    /// (line, y)
193    ///
194    pub fn iter_lines_y(&self) -> impl Iterator<Item = (usize, f64)> + '_ {
195        let mut last_line = None;
196        self.lines.iter().filter_map(move |vline| {
197            let info = self.info(*vline).unwrap();
198
199            let line = info.vline_info.rvline.line;
200
201            if last_line == Some(line) {
202                // We've already considered this line.
203                return None;
204            }
205
206            last_line = Some(line);
207
208            Some((line, info.y))
209        })
210    }
211
212    /// Get the earliest line info for a given line.
213    pub fn info_for_line(&self, line: usize) -> Option<LineInfo> {
214        self.info(self.first_rvline_for_line(line)?)
215    }
216
217    /// Get the earliest rvline for the given line
218    pub fn first_rvline_for_line(&self, line: usize) -> Option<RVLine> {
219        self.lines
220            .iter()
221            .find(|rvline| rvline.line == line)
222            .copied()
223    }
224
225    /// Get the latest rvline for the given line
226    pub fn last_rvline_for_line(&self, line: usize) -> Option<RVLine> {
227        self.lines
228            .iter()
229            .rfind(|rvline| rvline.line == line)
230            .copied()
231    }
232
233    /// Ran on [`LayoutEvent::CreatedLayout`](super::visual_line::LayoutEvent::CreatedLayout) to update  [`ScreenLinesBase`] &
234    /// the viewport if necessary.
235    ///
236    /// Returns `true` if [`ScreenLines`] needs to be completely updated in response
237    pub fn on_created_layout(&self, ed: &Editor, line: usize) -> bool {
238        // The default creation is empty, force an update if we're ever like this since it should
239        // not happen.
240        if self.is_empty() {
241            return true;
242        }
243
244        let base = self.base.get_untracked();
245        let vp = ed.viewport.get_untracked();
246
247        let is_before = self
248            .iter_vline_info()
249            .next()
250            .map(|l| line < l.rvline.line)
251            .unwrap_or(false);
252
253        // If the line is created before the current screenlines, we can simply shift the
254        // base and viewport forward by the number of extra wrapped lines,
255        // without needing to recompute the screen lines.
256        if is_before {
257            // TODO: don't assume line height is constant
258            let line_height = f64::from(ed.line_height(0));
259
260            // We could use `try_text_layout` here, but I believe this guards against a rare
261            // crash (though it is hard to verify) wherein the style id has changed and so the
262            // layouts get cleared.
263            // However, the original trigger of the layout event was when a layout was created
264            // and it expects it to still exist. So we create it just in case, though we of course
265            // don't trigger another layout event.
266            let layout = ed.text_layout_trigger(line, false);
267
268            // One line was already accounted for by treating it as an unwrapped line.
269            let new_lines = layout.line_count() - 1;
270
271            let new_y0 = base.active_viewport.y0 + new_lines as f64 * line_height;
272            let new_y1 = new_y0 + vp.height();
273            let new_viewport = Rect::new(vp.x0, new_y0, vp.x1, new_y1);
274
275            Effect::batch(|| {
276                self.base.set(ScreenLinesBase {
277                    active_viewport: new_viewport,
278                });
279                ed.viewport.set(new_viewport);
280            });
281
282            // Ensure that it is created even after the base/viewport signals have been updated.
283            // (We need the `text_layout` to still have the layout)
284            // But we have to trigger an event still if it is created because it *would* alter the
285            // screenlines.
286            // TODO: this has some risk for infinite looping if we're unlucky.
287            let _layout = ed.text_layout_trigger(line, true);
288
289            return false;
290        }
291
292        let is_after = self
293            .iter_vline_info()
294            .last()
295            .map(|l| line > l.rvline.line)
296            .unwrap_or(false);
297
298        // If the line created was after the current view, we don't need to update the screenlines
299        // at all, since the new line is not visible and has no effect on y positions
300        if is_after {
301            return false;
302        }
303
304        // If the line is created within the current screenlines, we need to update the
305        // screenlines to account for the new line.
306        // That is handled by the caller.
307        true
308    }
309}
310
311#[derive(Debug, Clone, PartialEq)]
312pub struct ScreenLinesBase {
313    /// The current/previous viewport.
314    ///
315    /// Used for determining whether there were any changes, and the `y0` serves as the
316    /// base for positioning the lines.
317    pub active_viewport: Rect,
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct LineInfo {
322    // font_size: usize,
323    // line_height: f64,
324    // x: f64,
325    /// The starting y position of the overall line that this vline
326    /// is a part of.
327    pub y: f64,
328    /// The y position of the visual line
329    pub vline_y: f64,
330    pub vline_info: VLineInfo<()>,
331}
332
333impl LineInfo {
334    pub fn with_base(mut self, base: ScreenLinesBase) -> Self {
335        self.y += base.active_viewport.y0;
336        self.vline_y += base.active_viewport.y0;
337        self
338    }
339}
340
341pub struct EditorView {
342    id: ViewId,
343    editor: RwSignal<Editor>,
344    is_active: Memo<bool>,
345    inner_node: Option<NodeId>,
346}
347
348impl EditorView {
349    #[allow(clippy::too_many_arguments)]
350    fn paint_normal_selection(
351        cx: &mut PaintCx,
352        ed: &Editor,
353        color: Color,
354        screen_lines: &ScreenLines,
355        start_offset: usize,
356        end_offset: usize,
357        affinity: CursorAffinity,
358    ) {
359        // TODO: selections should have separate start/end affinity
360        let (start_rvline, start_col) = ed.rvline_col_of_offset(start_offset, affinity);
361        let (end_rvline, end_col) = ed.rvline_col_of_offset(end_offset, affinity);
362
363        for LineInfo {
364            vline_y,
365            vline_info: info,
366            ..
367        } in screen_lines.iter_line_info_r(start_rvline..=end_rvline)
368        {
369            let rvline = info.rvline;
370            let line = rvline.line;
371
372            let left_col = if rvline == start_rvline {
373                start_col
374            } else {
375                ed.first_col(info)
376            };
377            let right_col = if rvline == end_rvline {
378                end_col
379            } else {
380                ed.last_col(info, true)
381            };
382
383            let line_height = f64::from(ed.line_height(line));
384
385            // Skip over empty selections within wrapped lines
386            if left_col == right_col && info.line_count > 1 && left_col != ed.last_col(info, true) {
387                continue;
388            }
389
390            // TODO: What affinity should these use?
391            let left_affinity = if rvline == start_rvline && left_col == ed.last_col(info, true) {
392                CursorAffinity::Backward
393            } else {
394                CursorAffinity::Forward
395            };
396
397            let x0 = ed
398                .line_point_of_line_col(line, left_col, left_affinity, true)
399                .x;
400            let x1 = ed
401                .line_point_of_line_col(line, right_col, CursorAffinity::Backward, true)
402                .x;
403
404            // Resolving width for displaying the newline character selection
405            // TODO(minor): Should this be line != end_line?
406            let x1 = if rvline != end_rvline && rvline.line_index + 1 == info.line_count {
407                x1 + CHAR_WIDTH
408            } else {
409                x1
410            };
411
412            let (x0, width) = if info.is_empty_phantom() {
413                let text_layout = ed.text_layout(line);
414                let width = text_layout
415                    .get_layout_x(rvline.line_index)
416                    .map(|(_, x1)| x1)
417                    .unwrap_or(0.0)
418                    .into();
419                (0.0, width)
420            } else {
421                (x0, x1 - x0)
422            };
423
424            let rect = Rect::from_origin_size((x0, vline_y), (width, line_height));
425            cx.fill(&rect, color, 0.0);
426        }
427    }
428
429    #[allow(clippy::too_many_arguments)]
430    pub fn paint_linewise_selection(
431        cx: &mut PaintCx,
432        ed: &Editor,
433        color: Color,
434        screen_lines: &ScreenLines,
435        start_offset: usize,
436        end_offset: usize,
437        affinity: CursorAffinity,
438    ) {
439        let viewport = ed.viewport.get_untracked();
440
441        let (start_rvline, _) = ed.rvline_col_of_offset(start_offset, affinity);
442        let (end_rvline, _) = ed.rvline_col_of_offset(end_offset, affinity);
443        // Linewise selection is by *line* so we move to the start/end rvlines of the line
444        let start_rvline = screen_lines
445            .first_rvline_for_line(start_rvline.line)
446            .unwrap_or(start_rvline);
447        let end_rvline = screen_lines
448            .last_rvline_for_line(end_rvline.line)
449            .unwrap_or(end_rvline);
450
451        for LineInfo {
452            vline_info: info,
453            vline_y,
454            ..
455        } in screen_lines.iter_line_info_r(start_rvline..=end_rvline)
456        {
457            let rvline = info.rvline;
458            let line = rvline.line;
459
460            // The left column is always 0 for linewise selections.
461            let right_col = ed.last_col(info, true);
462
463            // TODO: what affinity to use?
464            let x1 = ed
465                .line_point_of_line_col(line, right_col, CursorAffinity::Backward, true)
466                .x
467                + CHAR_WIDTH;
468
469            let line_height = ed.line_height(line);
470            let rect = Rect::from_origin_size(
471                (viewport.x0, vline_y),
472                (x1 - viewport.x0, f64::from(line_height)),
473            );
474            cx.fill(&rect, color, 0.0);
475        }
476    }
477
478    #[allow(clippy::too_many_arguments)]
479    pub fn paint_blockwise_selection(
480        cx: &mut PaintCx,
481        ed: &Editor,
482        color: Color,
483        screen_lines: &ScreenLines,
484        start_offset: usize,
485        end_offset: usize,
486        affinity: CursorAffinity,
487        horiz: Option<ColPosition>,
488    ) {
489        let (start_rvline, start_col) = ed.rvline_col_of_offset(start_offset, affinity);
490        let (end_rvline, end_col) = ed.rvline_col_of_offset(end_offset, affinity);
491        let left_col = start_col.min(end_col);
492        let right_col = start_col.max(end_col) + 1;
493
494        let lines = screen_lines
495            .iter_line_info_r(start_rvline..=end_rvline)
496            .filter_map(|line_info| {
497                let max_col = ed.last_col(line_info.vline_info, true);
498                (max_col > left_col).then_some((line_info, max_col))
499            });
500
501        for (line_info, max_col) in lines {
502            let line = line_info.vline_info.rvline.line;
503            let right_col = if let Some(ColPosition::End) = horiz {
504                max_col
505            } else {
506                right_col.min(max_col)
507            };
508
509            // TODO: what affinity to use?
510            let x0 = ed
511                .line_point_of_line_col(line, left_col, CursorAffinity::Forward, true)
512                .x;
513            let x1 = ed
514                .line_point_of_line_col(line, right_col, CursorAffinity::Backward, true)
515                .x;
516
517            let line_height = ed.line_height(line);
518            let rect =
519                Rect::from_origin_size((x0, line_info.vline_y), (x1 - x0, f64::from(line_height)));
520            cx.fill(&rect, color, 0.0);
521        }
522    }
523
524    fn paint_cursor(cx: &mut PaintCx, ed: &Editor, screen_lines: &ScreenLines) {
525        let cursor = ed.cursor;
526
527        let viewport = ed.viewport.get_untracked();
528
529        let current_line_color = ed.es.with_untracked(|es| es.current_line());
530
531        cursor.with_untracked(|cursor| {
532            let highlight_current_line = match cursor.mode {
533                // TODO: check if shis should be 0 or 1
534                CursorMode::Normal { offset: size, .. } => size == 0,
535                CursorMode::Insert(ref sel) => sel.is_caret(),
536                CursorMode::Visual { .. } => false,
537            };
538
539            if let Some(current_line_color) = current_line_color {
540                // Highlight the current line
541                if highlight_current_line {
542                    for (_, end, affinity) in cursor.regions_iter() {
543                        // TODO: unsure if this is correct for wrapping lines
544                        let rvline = ed.rvline_of_offset(end, affinity);
545
546                        if let Some(info) = screen_lines.info(rvline) {
547                            let line_height = ed.line_height(info.vline_info.rvline.line);
548                            let rect = Rect::from_origin_size(
549                                (viewport.x0, info.vline_y),
550                                (viewport.width(), f64::from(line_height)),
551                            );
552
553                            cx.fill(&rect, current_line_color, 0.0);
554                        }
555                    }
556                }
557            }
558
559            EditorView::paint_selection(cx, ed, screen_lines);
560        });
561    }
562
563    pub fn paint_selection(cx: &mut PaintCx, ed: &Editor, screen_lines: &ScreenLines) {
564        let cursor = ed.cursor;
565
566        let selection_color = ed.es.with_untracked(|es| es.selection());
567
568        cursor.with_untracked(|cursor| match cursor.mode {
569            CursorMode::Normal { .. } => {}
570            CursorMode::Visual {
571                start,
572                end,
573                mode: VisualMode::Normal,
574                affinity,
575            } => {
576                let start_offset = start.min(end);
577                let end_offset = ed.move_right(start.max(end), Mode::Insert, 1);
578
579                EditorView::paint_normal_selection(
580                    cx,
581                    ed,
582                    selection_color,
583                    screen_lines,
584                    start_offset,
585                    end_offset,
586                    affinity,
587                );
588            }
589            CursorMode::Visual {
590                start,
591                end,
592                mode: VisualMode::Linewise,
593                affinity,
594            } => {
595                EditorView::paint_linewise_selection(
596                    cx,
597                    ed,
598                    selection_color,
599                    screen_lines,
600                    start.min(end),
601                    start.max(end),
602                    affinity,
603                );
604            }
605            CursorMode::Visual {
606                start,
607                end,
608                mode: VisualMode::Blockwise,
609                affinity,
610            } => {
611                EditorView::paint_blockwise_selection(
612                    cx,
613                    ed,
614                    selection_color,
615                    screen_lines,
616                    start.min(end),
617                    start.max(end),
618                    affinity,
619                    cursor.horiz,
620                );
621            }
622            CursorMode::Insert(_) => {
623                for (start, end, affinity) in
624                    cursor.regions_iter().filter(|(start, end, _)| start != end)
625                {
626                    EditorView::paint_normal_selection(
627                        cx,
628                        ed,
629                        selection_color,
630                        screen_lines,
631                        start.min(end),
632                        start.max(end),
633                        affinity,
634                    );
635                }
636            }
637        });
638    }
639
640    fn paint_cursor_caret(
641        cx: &mut PaintCx,
642        ed: &Editor,
643        is_active: bool,
644        screen_lines: &ScreenLines,
645    ) {
646        let cursor = ed.cursor;
647        let hide_cursor = ed.cursor_info.hidden;
648        let caret_color = ed.es.with_untracked(|es| es.ed_caret());
649
650        if !is_active || hide_cursor.get_untracked() {
651            return;
652        }
653
654        cursor.with_untracked(|cursor| {
655            let style = ed.style();
656            let displaying_placeholder =
657                ed.text().is_empty() && ed.preedit().preedit.with_untracked(|p| p.is_none());
658
659            for (_, end, mut affinity) in cursor.regions_iter() {
660                if displaying_placeholder {
661                    affinity = CursorAffinity::Backward;
662                }
663
664                let is_block = match cursor.mode {
665                    CursorMode::Normal { .. } | CursorMode::Visual { .. } => true,
666                    CursorMode::Insert(_) => false,
667                };
668                let LineRegion { x, width, rvline } = cursor_caret(ed, end, is_block, affinity);
669
670                if let Some(info) = screen_lines.info(rvline) {
671                    if !style.paint_caret(ed.id(), rvline.line) {
672                        continue;
673                    }
674
675                    let line_height = ed.line_height(info.vline_info.rvline.line);
676                    let rect =
677                        Rect::from_origin_size((x, info.vline_y), (width, f64::from(line_height)));
678                    cx.fill(&rect, &caret_color, 0.0);
679                }
680            }
681        });
682    }
683
684    pub fn paint_wave_line(cx: &mut PaintCx, width: f64, point: Point, color: Color) {
685        let radius = 2.0;
686        let origin = Point::new(point.x, point.y + radius);
687        let mut path = BezPath::new();
688        path.move_to(origin);
689
690        let mut x = 0.0;
691        let mut direction = -1.0;
692        while x < width {
693            let point = origin + (x, 0.0);
694            let p1 = point + (radius, -radius * direction);
695            let p2 = point + (radius * 2.0, 0.0);
696            path.quad_to(p1, p2);
697            x += radius * 2.0;
698            direction *= -1.0;
699        }
700
701        cx.stroke(&path, color, &peniko::kurbo::Stroke::new(1.));
702    }
703
704    pub fn paint_extra_style(
705        cx: &mut PaintCx,
706        extra_styles: &[LineExtraStyle],
707        y: f64,
708        viewport: Rect,
709    ) {
710        for style in extra_styles {
711            let height = style.height;
712            if let Some(bg) = style.bg_color {
713                let width = style.width.unwrap_or_else(|| viewport.width());
714                let base = if style.width.is_none() {
715                    viewport.x0
716                } else {
717                    0.0
718                };
719                let x = style.x + base;
720                let y = y + style.y;
721                cx.fill(
722                    &Rect::ZERO
723                        .with_size(Size::new(width, height))
724                        .with_origin(Point::new(x, y)),
725                    bg,
726                    0.0,
727                );
728            }
729
730            if let Some(color) = style.under_line {
731                let width = style.width.unwrap_or_else(|| viewport.width());
732                let base = if style.width.is_none() {
733                    viewport.x0
734                } else {
735                    0.0
736                };
737                let x = style.x + base;
738                let y = y + style.y + height;
739                cx.stroke(
740                    &Line::new(Point::new(x, y), Point::new(x + width, y)),
741                    color,
742                    &peniko::kurbo::Stroke::new(1.),
743                );
744            }
745
746            if let Some(color) = style.wave_line {
747                let width = style.width.unwrap_or_else(|| viewport.width());
748                let y = y + style.y + height;
749                EditorView::paint_wave_line(cx, width, Point::new(style.x, y), color);
750            }
751        }
752    }
753
754    pub fn paint_text(
755        cx: &mut PaintCx,
756        view_id: Option<ViewId>,
757        ed: &Editor,
758        viewport: Rect,
759        is_active: bool,
760        screen_lines: &ScreenLines,
761    ) {
762        let edid = ed.id();
763        let style = ed.style();
764
765        // TODO: cache indent text layout width
766        let indent_unit = ed.es.with_untracked(|es| es.indent_style()).as_str();
767        // TODO: don't assume font family is the same for all lines?
768        let family = style.font_family(edid, 0);
769        let attrs = Attrs::new()
770            .family(&family)
771            .font_size(style.font_size(edid, 0) as f32);
772        let attrs_list = AttrsList::new(attrs);
773
774        let mut indent_text = TextLayout::new();
775        indent_text.set_text(&format!("{indent_unit}a"), attrs_list, None);
776        let indent_text_width = indent_text.hit_position(indent_unit.len()).point.x;
777
778        if ed.es.with(|s| s.show_indent_guide()) {
779            // Cache the indent guide color outside the loop to avoid repeated signal access
780            let indent_guide_color = ed.es.with_untracked(|es| es.indent_guide());
781            for (line, y) in screen_lines.iter_lines_y() {
782                let text_layout = ed.text_layout(line);
783                let line_height = f64::from(ed.line_height(line));
784                let mut x = 0.0;
785                while x + 1.0 < text_layout.indent {
786                    cx.stroke(
787                        &Line::new(Point::new(x, y), Point::new(x, y + line_height)),
788                        indent_guide_color,
789                        &peniko::kurbo::Stroke::new(1.),
790                    );
791                    x += indent_text_width;
792                }
793            }
794        }
795
796        let is_active = if let Some(view_id) = view_id {
797            is_active && cx.window_state.is_focused(&view_id)
798        } else {
799            is_active
800        };
801        Self::paint_cursor_caret(cx, ed, is_active, screen_lines);
802
803        // Pre-create whitespace indicator TextLayouts outside the loop.
804        // This avoids creating new TextLayout objects for every line, which is expensive.
805        // We use line 0's font properties, consistent with how indent guides are rendered.
806        // TODO: consider caching these in the Editor if font properties change frequently.
807        let whitespace_color = ed.es.with_untracked(|es| es.visible_whitespace());
808        let ws_attrs = Attrs::new()
809            .color(whitespace_color)
810            .family(&family)
811            .font_size(style.font_size(edid, 0) as f32);
812        let ws_attrs_list = AttrsList::new(ws_attrs);
813        let mut space_text = TextLayout::new();
814        space_text.set_text("·", ws_attrs_list.clone(), None);
815        let mut tab_text = TextLayout::new();
816        tab_text.set_text("→", ws_attrs_list, None);
817
818        for (line, y) in screen_lines.iter_lines_y() {
819            let text_layout = ed.text_layout(line);
820
821            EditorView::paint_extra_style(cx, &text_layout.extra_style, y, viewport);
822
823            if let Some(whitespaces) = &text_layout.whitespaces {
824                for (c, (x0, _x1)) in whitespaces.iter() {
825                    match *c {
826                        '\t' => {
827                            cx.draw_text(&tab_text, Point::new(*x0, y));
828                        }
829                        ' ' => {
830                            cx.draw_text(&space_text, Point::new(*x0, y));
831                        }
832                        _ => {}
833                    }
834                }
835            }
836
837            cx.draw_text(&text_layout.text, Point::new(0.0, y));
838        }
839    }
840}
841
842impl View for EditorView {
843    fn id(&self) -> ViewId {
844        self.id
845    }
846
847    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
848        self.editor.with_untracked(|ed| {
849            ed.es.update(|s| {
850                if s.read(cx) {
851                    ed.floem_style_id.update(|val| *val += 1);
852                    cx.window_state.request_paint(self.id());
853                }
854            })
855        });
856    }
857
858    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
859        "Editor View".into()
860    }
861
862    fn update(&mut self, _cx: &mut UpdateCx, _state: Box<dyn std::any::Any>) {}
863
864    fn layout(&mut self, cx: &mut LayoutCx) -> crate::taffy::tree::NodeId {
865        cx.layout_node(self.id, true, |_cx| {
866            let editor = self.editor.get_untracked();
867
868            let parent_size = editor.parent_size.get_untracked();
869
870            if self.inner_node.is_none() {
871                self.inner_node = Some(self.id.new_taffy_node());
872            }
873
874            let screen_lines = editor.screen_lines.get_untracked();
875            for (line, _) in screen_lines.iter_lines_y() {
876                // fill in text layout cache so that max width is correct.
877                editor.text_layout(line);
878            }
879
880            let inner_node = self.inner_node.unwrap();
881
882            // TODO: don't assume there's a constant line height
883            let line_height = f64::from(editor.line_height(0));
884
885            let width = editor.max_line_width().max(parent_size.width());
886            let last_line_height = line_height * (editor.last_vline().get() + 1) as f64;
887            let height = last_line_height.max(parent_size.height());
888
889            let margin_bottom = if editor.es.with_untracked(|es| es.scroll_beyond_last_line()) {
890                parent_size.height().min(last_line_height) - line_height
891            } else {
892                0.0
893            };
894
895            let style = Style::new()
896                .width(width)
897                .height(height)
898                .margin_bottom(margin_bottom)
899                .to_taffy_style();
900            let _ = self.id.taffy().borrow_mut().set_style(inner_node, style);
901
902            vec![inner_node]
903        })
904    }
905
906    fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
907        let editor = self.editor.get_untracked();
908
909        let viewport = cx.current_viewport();
910        if editor.viewport.with_untracked(|v| v != &viewport) {
911            editor.viewport.set(viewport);
912        }
913
914        if let Some(parent) = self.id.parent() {
915            let parent_size = parent.layout_rect();
916            if editor.parent_size.with_untracked(|ps| ps != &parent_size) {
917                editor.parent_size.set(parent_size);
918            }
919        }
920        None
921    }
922
923    fn paint(&mut self, cx: &mut PaintCx) {
924        let ed = self.editor.get_untracked();
925        let viewport = ed.viewport.get_untracked();
926
927        // We repeatedly get the screen lines because we don't currently carefully manage the
928        // paint functions to avoid potentially needing to recompute them, which could *maybe*
929        // make them invalid.
930        // TODO: One way to get around the above issue would be to more careful, since we
931        // technically don't need to stop it from *recomputing* just stop any possible changes, but
932        // avoiding recomputation seems easiest/clearest.
933        // I expect that most/all of the paint functions could restrict themselves to only what is
934        // within the active screen lines without issue.
935        let screen_lines = ed.screen_lines.get_untracked();
936        EditorView::paint_cursor(cx, &ed, &screen_lines);
937        let screen_lines = ed.screen_lines.get_untracked();
938        EditorView::paint_text(
939            cx,
940            Some(self.id()),
941            &ed,
942            viewport,
943            self.is_active.get_untracked(),
944            &screen_lines,
945        );
946    }
947}
948
949style_class!(pub EditorViewClass);
950
951pub fn editor_view(
952    editor: RwSignal<Editor>,
953    is_active: impl Fn(bool) -> bool + 'static + Copy,
954) -> EditorView {
955    let id = ViewId::new();
956    let is_active = Scope::current().create_memo(move |_| is_active(true));
957
958    let ed = editor.get_untracked();
959
960    let doc = ed.doc;
961    let style = ed.style;
962    let lines = ed.screen_lines;
963    Effect::new(move |_| {
964        doc.track();
965        style.track();
966        lines.track();
967        id.request_layout();
968    });
969
970    let hide_cursor = ed.cursor_info.hidden;
971    Effect::new(move |_| {
972        hide_cursor.track();
973        id.request_paint();
974    });
975
976    let editor_window_origin = ed.window_origin;
977    let cursor = ed.cursor;
978    let cursor_memo =
979        Scope::current().create_memo(move |_| cursor.with(|c| (c.is_insert(), c.offset())));
980    let allows_ime = ed.ime_allowed;
981    let editor_viewport = ed.viewport;
982    let focused = ed.editor_view_focused_value;
983    let prev_ime_area = ed.ime_cursor_area;
984    let preedit = ed.preedit().preedit;
985
986    Effect::new(move |_| {
987        if !is_active.get() {
988            return;
989        }
990
991        let (allowing_ime, offset) = cursor_memo.get();
992        let focused = focused.get();
993
994        // apply ime state changes
995        if allows_ime.get_untracked() != allowing_ime {
996            allows_ime.set(allowing_ime);
997
998            if focused {
999                set_ime_allowed(allowing_ime);
1000            }
1001        }
1002
1003        if !allowing_ime || !focused {
1004            // avoid resolving cursor area if we don't need it
1005            return;
1006        }
1007
1008        // subscribe to preedit changes, as it affects the CursorAffinity::Forward calculation
1009        preedit.with(|_| {});
1010
1011        let (point_above, _) = ed.points_of_offset(offset, CursorAffinity::Backward);
1012        let (point_above2, point_below) = ed.points_of_offset(offset, CursorAffinity::Forward);
1013
1014        let viewport = editor_viewport.get();
1015        let (min_x, max_x);
1016
1017        if point_above.y != point_above2.y {
1018            // multiline
1019            min_x = 0.0;
1020            max_x = viewport.x1 - viewport.x0;
1021        } else {
1022            min_x = point_above.x.min(point_above2.x);
1023            max_x = point_above.x.max(point_above2.x);
1024        }
1025
1026        let window_origin = editor_window_origin.get();
1027        let pos = window_origin + (min_x - viewport.x0, point_above.y - viewport.y0);
1028        let size = Size::new(max_x - min_x, point_below.y - point_above.y);
1029
1030        if prev_ime_area.get_untracked() != Some((pos, size)) {
1031            set_ime_cursor_area(pos, size);
1032            prev_ime_area.set(Some((pos, size)));
1033        }
1034    });
1035
1036    EditorView {
1037        id,
1038        editor,
1039        is_active,
1040        inner_node: None,
1041    }
1042    .style(|s| s.focusable(true))
1043    .on_event_cont(EventListener::FocusGained, move |_| {
1044        focused.set(true);
1045        prev_ime_area.set(None);
1046
1047        if allows_ime.get_untracked() {
1048            set_ime_allowed(true);
1049        }
1050    })
1051    .on_event_cont(EventListener::FocusLost, move |_| {
1052        focused.set(false);
1053        editor.with_untracked(|ed| ed.commit_preedit());
1054        set_ime_allowed(false);
1055    })
1056    .on_event(EventListener::ImePreedit, move |event| {
1057        if !is_active.get_untracked() || !focused.get_untracked() {
1058            return EventPropagation::Continue;
1059        }
1060
1061        if let Event::ImePreedit { text, cursor } = event {
1062            editor.with_untracked(|ed| {
1063                if text.is_empty() {
1064                    ed.clear_preedit();
1065                } else {
1066                    ed.doc.with_untracked(|doc| {
1067                        doc.run_command(
1068                            ed,
1069                            &Command::Edit(EditCommand::DeleteSelection),
1070                            Some(1),
1071                            Modifiers::empty(),
1072                        );
1073                    });
1074
1075                    let offset = ed.cursor.with_untracked(|c| c.offset());
1076
1077                    // update affinity to display caret after preedit
1078                    ed.cursor
1079                        .update(|c| c.set_latest_affinity(CursorAffinity::Forward));
1080
1081                    ed.set_preedit(text.clone(), *cursor, offset);
1082                }
1083            });
1084        }
1085        EventPropagation::Stop
1086    })
1087    .on_event(EventListener::ImeCommit, move |event| {
1088        if !is_active.get_untracked() || !focused.get_untracked() {
1089            return EventPropagation::Continue;
1090        }
1091
1092        if let Event::ImeCommit(text) = event {
1093            editor.with_untracked(|ed| {
1094                ed.clear_preedit();
1095                ed.receive_char(text);
1096            });
1097        }
1098        EventPropagation::Stop
1099    })
1100    .class(EditorViewClass)
1101}
1102
1103#[derive(Clone, Debug)]
1104pub struct LineRegion {
1105    pub x: f64,
1106    pub width: f64,
1107    pub rvline: RVLine,
1108}
1109
1110/// Get the render information for a caret cursor at the given `offset`.
1111pub fn cursor_caret(
1112    ed: &Editor,
1113    offset: usize,
1114    block: bool,
1115    affinity: CursorAffinity,
1116) -> LineRegion {
1117    let info = ed.rvline_info_of_offset(offset, affinity);
1118    let (_, col) = ed.offset_to_line_col(offset);
1119    let after_last_char = col == ed.line_end_col(info.rvline.line, true);
1120
1121    let doc = ed.doc();
1122    let preedit_start = doc
1123        .preedit()
1124        .preedit
1125        .with_untracked(|preedit| {
1126            preedit.as_ref().and_then(|preedit| {
1127                let preedit_line = ed.line_of_offset(preedit.offset);
1128                preedit.cursor.map(|x| (preedit_line, x))
1129            })
1130        })
1131        .filter(|(preedit_line, _)| *preedit_line == info.rvline.line)
1132        .map(|(_, (start, _))| start);
1133
1134    let point = ed.line_point_of_line_col(info.rvline.line, col, affinity, false);
1135
1136    let rvline = if preedit_start.is_some() {
1137        // If there's an IME edit, then we need to use the point's y to get the actual y position
1138        // that the IME cursor is at. Since it could be in the middle of the IME phantom text
1139        let y = point.y;
1140
1141        // TODO: I don't think this is handling varying line heights properly
1142        let line_height = ed.line_height(info.rvline.line);
1143
1144        let line_index = (y / f64::from(line_height)).floor() as usize;
1145        RVLine::new(info.rvline.line, line_index)
1146    } else {
1147        info.rvline
1148    };
1149
1150    let x0 = point.x;
1151    if block {
1152        let x0 = ed
1153            .line_point_of_line_col(info.rvline.line, col, CursorAffinity::Forward, true)
1154            .x;
1155        let new_offset = ed.move_right(offset, Mode::Insert, 1);
1156        let (_, new_col) = ed.offset_to_line_col(new_offset);
1157        let width = if after_last_char {
1158            CHAR_WIDTH
1159        } else {
1160            let x1 = ed
1161                .line_point_of_line_col(info.rvline.line, new_col, CursorAffinity::Backward, true)
1162                .x;
1163            x1 - x0
1164        };
1165
1166        LineRegion {
1167            x: x0,
1168            width,
1169            rvline,
1170        }
1171    } else {
1172        LineRegion {
1173            x: x0 - 1.0,
1174            width: 2.0,
1175            rvline,
1176        }
1177    }
1178}
1179
1180pub fn editor_container_view(
1181    editor: RwSignal<Editor>,
1182    is_active: impl Fn(bool) -> bool + 'static + Copy,
1183    handle_key_event: impl Fn(KeypressKey) -> CommandExecuted + 'static,
1184) -> impl IntoView {
1185    Stack::new((
1186        editor_gutter(editor),
1187        editor_content(editor, is_active, handle_key_event),
1188    ))
1189    .style(|s| s.absolute().size_pct(100.0, 100.0))
1190    .on_cleanup(move || {
1191        // TODO: should we have some way for doc to tell us if we're allowed to cleanup the editor?
1192        let editor = editor.get_untracked();
1193        editor.cx.get().dispose();
1194    })
1195}
1196
1197/// Default editor gutter
1198/// Simply shows line numbers
1199pub fn editor_gutter(editor: RwSignal<Editor>) -> impl IntoView {
1200    let ed = editor.get_untracked();
1201
1202    let scroll_delta = ed.scroll_delta;
1203
1204    let gutter_rect = RwSignal::new(Rect::ZERO);
1205
1206    editor_gutter_view(editor)
1207        .on_resize(move |rect| {
1208            gutter_rect.set(rect);
1209        })
1210        .on_event_stop(EventListener::PointerWheel, move |event| {
1211            if let Some(vec2) = event.pixel_scroll_delta_vec2() {
1212                scroll_delta.set(vec2);
1213            }
1214        })
1215}
1216
1217fn editor_content(
1218    editor: RwSignal<Editor>,
1219    is_active: impl Fn(bool) -> bool + 'static + Copy,
1220    handle_key_event: impl Fn(KeypressKey) -> CommandExecuted + 'static,
1221) -> impl IntoView {
1222    let ed = editor.get_untracked();
1223    let cursor = ed.cursor;
1224    let scroll_delta = ed.scroll_delta;
1225    let scroll_to = ed.scroll_to;
1226    let window_origin = ed.window_origin;
1227    let viewport = ed.viewport;
1228
1229    Scroll::new({
1230        let editor_content_view =
1231            editor_view(editor, is_active).style(move |s| s.absolute().cursor(CursorStyle::Text));
1232
1233        let id = editor_content_view.id();
1234        ed.editor_view_id.set(Some(id));
1235
1236        editor_content_view
1237            .on_event_cont(EventListener::FocusGained, move |_| {
1238                editor.with_untracked(|ed| ed.editor_view_focused.notify())
1239            })
1240            .on_event_cont(EventListener::FocusLost, move |_| {
1241                editor.with_untracked(|ed| ed.editor_view_focus_lost.notify())
1242            })
1243            .on_event_cont(EventListener::PointerDown, move |event| {
1244                if let Event::Pointer(
1245                    pointer_event @ PointerEvent::Down(PointerButtonEvent { state, button, .. }),
1246                ) = event
1247                {
1248                    id.request_active();
1249                    id.request_focus();
1250                    if pointer_event.is_primary_pointer() {
1251                        editor.get_untracked().pointer_down_primary(state);
1252                    } else if button.is_some_and(|b| b == PointerButton::Secondary) {
1253                        editor.get_untracked().right_click(state);
1254                    }
1255                }
1256            })
1257            .on_event_cont(EventListener::PointerMove, move |event| {
1258                if let Event::Pointer(PointerEvent::Move(pu)) = event {
1259                    editor.get_untracked().pointer_move(&pu.current);
1260                }
1261            })
1262            .on_event_cont(EventListener::PointerUp, move |event| {
1263                if let Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) = event {
1264                    editor.get_untracked().pointer_up(state);
1265                }
1266            })
1267            .on_event_stop(EventListener::KeyDown, move |event| {
1268                let Event::Key(
1269                    key_event @ KeyboardEvent {
1270                        state: KeyState::Down,
1271                        ..
1272                    },
1273                ) = event
1274                else {
1275                    return;
1276                };
1277
1278                handle_key_event(KeypressKey {
1279                    key: key_event.key.clone(),
1280                    modifiers: key_event.modifiers,
1281                });
1282
1283                let mut mods = key_event.modifiers;
1284                mods.set(Modifiers::SHIFT, false);
1285                mods.set(Modifiers::ALT, false);
1286                #[cfg(target_os = "macos")]
1287                mods.set(Modifiers::ALT, false);
1288
1289                if mods.is_empty() {
1290                    if let Key::Character(c) = &key_event.key {
1291                        editor.get_untracked().receive_char(c);
1292                    }
1293                }
1294            })
1295            .style(|s| s.min_size_full())
1296    })
1297    .on_move(move |point| {
1298        window_origin.set(point);
1299    })
1300    .scroll_to(move || scroll_to.get().map(Vec2::to_point))
1301    .scroll_delta(move || scroll_delta.get())
1302    .ensure_visible(move || {
1303        let editor = editor.get_untracked();
1304        let cursor = cursor.get();
1305        let offset = cursor.offset();
1306        editor.doc.track();
1307        // TODO:?
1308        // editor.kind.track();
1309
1310        let LineRegion { x, width, rvline } =
1311            cursor_caret(&editor, offset, !cursor.is_insert(), cursor.affinity());
1312
1313        // TODO: don't assume line-height is constant
1314        let line_height = f64::from(editor.line_height(0));
1315
1316        // TODO: is there a good way to avoid the calculation of the vline here?
1317        let vline = editor.vline_of_rvline(rvline);
1318        let rect =
1319            Rect::from_origin_size((x, vline.get() as f64 * line_height), (width, line_height))
1320                .inflate(10.0, 1.0);
1321
1322        let viewport = viewport.get_untracked();
1323        let smallest_distance = (viewport.y0 - rect.y0)
1324            .abs()
1325            .min((viewport.y1 - rect.y0).abs())
1326            .min((viewport.y0 - rect.y1).abs())
1327            .min((viewport.y1 - rect.y1).abs());
1328        let biggest_distance = (viewport.y0 - rect.y0)
1329            .abs()
1330            .max((viewport.y1 - rect.y0).abs())
1331            .max((viewport.y0 - rect.y1).abs())
1332            .max((viewport.y1 - rect.y1).abs());
1333        let jump_to_middle =
1334            biggest_distance > viewport.height() && smallest_distance > viewport.height() / 2.0;
1335
1336        if jump_to_middle {
1337            rect.inflate(0.0, viewport.height() / 2.0)
1338        } else {
1339            let mut rect = rect;
1340            let cursor_surrounding_lines = editor.es.with(|s| s.cursor_surrounding_lines()) as f64;
1341            rect.y0 -= cursor_surrounding_lines * line_height;
1342            rect.y1 += cursor_surrounding_lines * line_height;
1343            rect
1344        }
1345    })
1346    .style(|s| s.size_pct(100.0, 100.0))
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351    use std::{collections::HashMap, rc::Rc};
1352
1353    use floem_reactive::RwSignal;
1354    use peniko::kurbo::Rect;
1355
1356    use crate::views::editor::{
1357        view::LineInfo,
1358        visual_line::{RVLine, VLineInfo},
1359    };
1360
1361    use super::{ScreenLines, ScreenLinesBase};
1362
1363    #[test]
1364    fn iter_line_info_range() {
1365        let lines = vec![
1366            RVLine::new(10, 0),
1367            RVLine::new(10, 1),
1368            RVLine::new(10, 2),
1369            RVLine::new(10, 3),
1370        ];
1371        let mut info = HashMap::new();
1372        for rv in lines.iter() {
1373            info.insert(
1374                *rv,
1375                LineInfo {
1376                    // The specific values don't really matter
1377                    y: 0.0,
1378                    vline_y: 0.0,
1379                    vline_info: VLineInfo::new(0..0, *rv, 4, ()),
1380                },
1381            );
1382        }
1383        let sl = ScreenLines {
1384            lines: Rc::new(lines),
1385            info: Rc::new(info),
1386            diff_sections: None,
1387            base: RwSignal::new(ScreenLinesBase {
1388                active_viewport: Rect::ZERO,
1389            }),
1390        };
1391
1392        // Completely outside range should be empty
1393        assert_eq!(
1394            sl.iter_line_info_r(RVLine::new(0, 0)..=RVLine::new(1, 5))
1395                .collect::<Vec<_>>(),
1396            Vec::new()
1397        );
1398        // Should include itself
1399        assert_eq!(
1400            sl.iter_line_info_r(RVLine::new(10, 0)..=RVLine::new(10, 0))
1401                .count(),
1402            1
1403        );
1404        // Typical case
1405        assert_eq!(
1406            sl.iter_line_info_r(RVLine::new(10, 0)..=RVLine::new(10, 2))
1407                .count(),
1408            3
1409        );
1410        assert_eq!(
1411            sl.iter_line_info_r(RVLine::new(10, 0)..=RVLine::new(10, 3))
1412                .count(),
1413            4
1414        );
1415        // Should only include what is within the interval
1416        assert_eq!(
1417            sl.iter_line_info_r(RVLine::new(10, 0)..=RVLine::new(10, 5))
1418                .count(),
1419            4
1420        );
1421        assert_eq!(
1422            sl.iter_line_info_r(RVLine::new(0, 0)..=RVLine::new(10, 5))
1423                .count(),
1424            4
1425        );
1426    }
1427}