Skip to main content

floem/views/editor/
gutter.rs

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