Skip to main content

floem/views/editor/
gutter.rs

1use crate::{
2    Renderer,
3    context::PaintCx,
4    peniko::kurbo::Point,
5    prop, prop_extractor,
6    style::{Style, TextColor},
7    style_class,
8    text::{Attrs, AttrsList, TextLayout},
9    view::View,
10    view::ViewId,
11    views::Decorators,
12};
13use floem_editor_core::{cursor::CursorMode, mode::Mode};
14use floem_reactive::{RwSignal, SignalGet, SignalWith};
15use peniko::Color;
16use peniko::color::palette;
17use peniko::kurbo::Rect;
18
19use super::{CurrentLineColor, Editor};
20
21prop!(pub LeftOfCenterPadding: f64 {} = 25.);
22prop!(pub RightOfCenterPadding: f64 {} = 30.);
23prop!(pub DimColor: Option<Color> {} = None);
24
25prop_extractor! {
26    GutterStyle {
27        accent_color: TextColor,
28        dim_color: DimColor,
29        left_padding: LeftOfCenterPadding,
30        right_padding: RightOfCenterPadding,
31        current_line_color: CurrentLineColor,
32    }
33}
34impl GutterStyle {
35    fn gs_accent_color(&self) -> Color {
36        self.accent_color().unwrap_or(palette::css::BLACK)
37    }
38
39    fn gs_dim_color(&self) -> Color {
40        self.dim_color().unwrap_or(self.gs_accent_color())
41    }
42}
43
44pub struct EditorGutterView {
45    id: ViewId,
46    editor: RwSignal<Editor>,
47    full_width: f64,
48    text_width: f64,
49    gutter_style: GutterStyle,
50}
51
52style_class!(pub GutterClass);
53
54pub fn editor_gutter_view(editor: RwSignal<Editor>) -> EditorGutterView {
55    let id = ViewId::new();
56
57    EditorGutterView {
58        id,
59        editor,
60        full_width: 0.0,
61        text_width: 0.0,
62        gutter_style: Default::default(),
63    }
64    .class(GutterClass)
65}
66
67impl View for EditorGutterView {
68    fn id(&self) -> ViewId {
69        self.id
70    }
71
72    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
73        "Editor Gutter View".into()
74    }
75
76    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
77        if self.gutter_style.read(cx) {
78            cx.window_state.request_paint(self.id());
79        }
80    }
81
82    fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::prelude::NodeId {
83        cx.layout_node(self.id(), true, |_cx| {
84            let (width, height) = (self.text_width, 10.0);
85            let layout_node = self
86                .id
87                .taffy()
88                .borrow_mut()
89                .new_leaf(taffy::style::Style::DEFAULT)
90                .unwrap();
91
92            let style = Style::new()
93                .width(self.gutter_style.left_padding() + width + self.gutter_style.right_padding())
94                .height(height)
95                .to_taffy_style();
96            let _ = self.id.taffy().borrow_mut().set_style(layout_node, style);
97            vec![layout_node]
98        })
99    }
100
101    fn compute_layout(&mut self, _cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
102        if let Some(width) = self.id.get_layout().map(|l| l.size.width as f64) {
103            self.full_width = width;
104        }
105
106        let editor = self.editor.get_untracked();
107        let edid = editor.id();
108        let style = editor.style();
109        // TODO: don't assume font family is constant for each line
110        let family = style.font_family(edid, 0);
111        let attrs = Attrs::new()
112            .family(&family)
113            .font_size(style.font_size(edid, 0) as f32);
114
115        let attrs_list = AttrsList::new(attrs);
116
117        let widest_text_width = self.compute_widest_text_width(&attrs_list);
118        if (self.full_width
119            - widest_text_width
120            - self.gutter_style.left_padding()
121            - self.gutter_style.right_padding())
122        .abs()
123            > 1e-2
124        {
125            self.text_width = widest_text_width;
126            self.id.request_layout();
127        }
128        None
129    }
130
131    fn paint(&mut self, cx: &mut PaintCx) {
132        let editor = self.editor.get_untracked();
133        let edid = editor.id();
134
135        let viewport = editor.viewport.get_untracked();
136        let cursor = editor.cursor;
137        let style = editor.style.get_untracked();
138
139        let (offset, mode) = cursor.with_untracked(|c| (c.offset(), c.get_mode()));
140        let last_line = editor.last_line();
141        let current_line = editor.line_of_offset(offset);
142
143        // TODO: don't assume font family is constant for each line
144        let family = style.font_family(edid, 0);
145        let accent_color = self.gutter_style.gs_accent_color();
146        let dim_color = self.gutter_style.gs_dim_color();
147        let attrs = Attrs::new()
148            .family(&family)
149            .color(dim_color)
150            .font_size(style.font_size(edid, 0) as f32);
151        let attrs_list = AttrsList::new(attrs.clone());
152        let current_line_attrs_list = AttrsList::new(attrs.color(accent_color));
153        let show_relative = editor.es.with_untracked(|es| es.modal())
154            && editor.es.with_untracked(|es| es.modal_relative_line())
155            && mode != Mode::Insert;
156
157        self.text_width = self.compute_widest_text_width(&attrs_list);
158
159        editor.screen_lines.with_untracked(|screen_lines| {
160            if let Some(current_line_color) = self.gutter_style.current_line_color() {
161                cursor.with_untracked(|cursor| {
162                    let highlight_current_line = match cursor.mode {
163                        // TODO: check if shis should be 0 or 1
164                        CursorMode::Normal { offset: size, .. } => size == 0,
165                        CursorMode::Insert(ref sel) => sel.is_caret(),
166                        CursorMode::Visual { .. } => false,
167                    };
168
169                    // Highlight the current line
170                    if highlight_current_line {
171                        for (_, end, affinity) in cursor.regions_iter() {
172                            // TODO: unsure if this is correct for wrapping lines
173                            let rvline = editor.rvline_of_offset(end, affinity);
174
175                            if let Some(info) = screen_lines.info(rvline) {
176                                let line_height = editor.line_height(info.vline_info.rvline.line);
177                                // the extra 1px is for a small line that appears between
178                                let rect = Rect::from_origin_size(
179                                    (viewport.x0, info.vline_y - viewport.y0),
180                                    (self.full_width + 1.1, f64::from(line_height)),
181                                );
182
183                                cx.fill(&rect, current_line_color, 0.0);
184                            }
185                        }
186                    }
187                })
188            }
189
190            for (line, y) in screen_lines.iter_lines_y() {
191                // If it ends up outside the bounds of the file, stop trying to display line numbers
192                if line > last_line {
193                    break;
194                }
195
196                let line_height = f64::from(style.line_height(edid, line));
197
198                let text = if show_relative {
199                    if line == current_line {
200                        line + 1
201                    } else {
202                        line.abs_diff(current_line)
203                    }
204                } else {
205                    line + 1
206                }
207                .to_string();
208
209                let mut text_layout = TextLayout::new();
210                if line == current_line {
211                    text_layout.set_text(&text, current_line_attrs_list.clone(), None);
212                } else {
213                    text_layout.set_text(&text, attrs_list.clone(), None);
214                }
215                let size = text_layout.size();
216                let height = size.height;
217
218                let pos = Point::new(
219                    (self.full_width - (size.width) - self.gutter_style.right_padding()).max(0.0),
220                    y + (line_height - height) / 2.0 - viewport.y0,
221                );
222
223                cx.draw_text(&text_layout, pos);
224            }
225        });
226    }
227}
228
229impl EditorGutterView {
230    fn compute_widest_text_width(&mut self, attrs_list: &AttrsList) -> f64 {
231        let last_line = self.editor.get_untracked().last_line() + 1;
232        let mut text = TextLayout::new();
233        text.set_text(&last_line.to_string(), attrs_list.clone(), None);
234        text.size().width
235    }
236}