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