split_editor_view.rs

  1use std::cmp;
  2
  3use collections::{HashMap, HashSet};
  4use gpui::{
  5    AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, Context, DragMoveEvent, Element,
  6    Entity, GlobalElementId, Hsla, InspectorElementId, IntoElement, LayoutId, Length,
  7    ParentElement, Pixels, StatefulInteractiveElement, Styled, TextStyleRefinement, Window, div,
  8    linear_color_stop, linear_gradient, point, px, size,
  9};
 10use multi_buffer::{Anchor, ExcerptId};
 11use settings::Settings;
 12use smallvec::smallvec;
 13use text::BufferId;
 14use theme::ActiveTheme;
 15use ui::scrollbars::ShowScrollbar;
 16use ui::{h_flex, prelude::*, v_flex};
 17
 18use gpui::ContentMask;
 19
 20use crate::{
 21    DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
 22    MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, RowExt, StickyHeaderExcerpt,
 23    display_map::Block,
 24    element::{EditorElement, SplitSide, header_jump_data, render_buffer_header},
 25    scroll::ScrollOffset,
 26    split::SplittableEditor,
 27};
 28
 29const RESIZE_HANDLE_WIDTH: f32 = 8.0;
 30
 31#[derive(Debug, Clone)]
 32struct DraggedSplitHandle;
 33
 34pub struct SplitEditorState {
 35    left_ratio: f32,
 36    visible_left_ratio: f32,
 37    cached_width: Pixels,
 38}
 39
 40impl SplitEditorState {
 41    pub fn new(_cx: &mut App) -> Self {
 42        Self {
 43            left_ratio: 0.5,
 44            visible_left_ratio: 0.5,
 45            cached_width: px(0.),
 46        }
 47    }
 48
 49    #[allow(clippy::misnamed_getters)]
 50    pub fn left_ratio(&self) -> f32 {
 51        self.visible_left_ratio
 52    }
 53
 54    pub fn right_ratio(&self) -> f32 {
 55        1.0 - self.visible_left_ratio
 56    }
 57
 58    fn on_drag_move(
 59        &mut self,
 60        drag_event: &DragMoveEvent<DraggedSplitHandle>,
 61        _window: &mut Window,
 62        _cx: &mut Context<Self>,
 63    ) {
 64        let drag_position = drag_event.event.position;
 65        let bounds = drag_event.bounds;
 66        let bounds_width = bounds.right() - bounds.left();
 67
 68        if bounds_width > px(0.) {
 69            self.cached_width = bounds_width;
 70        }
 71
 72        let min_ratio = 0.1;
 73        let max_ratio = 0.9;
 74
 75        let new_ratio = (drag_position.x - bounds.left()) / bounds_width;
 76        self.visible_left_ratio = new_ratio.clamp(min_ratio, max_ratio);
 77    }
 78
 79    fn commit_ratio(&mut self) {
 80        self.left_ratio = self.visible_left_ratio;
 81    }
 82
 83    fn on_double_click(&mut self) {
 84        self.left_ratio = 0.5;
 85        self.visible_left_ratio = 0.5;
 86    }
 87}
 88
 89#[derive(IntoElement)]
 90pub struct SplitEditorView {
 91    splittable_editor: Entity<SplittableEditor>,
 92    style: EditorStyle,
 93    split_state: Entity<SplitEditorState>,
 94}
 95
 96impl SplitEditorView {
 97    pub fn new(
 98        splittable_editor: Entity<SplittableEditor>,
 99        style: EditorStyle,
100        split_state: Entity<SplitEditorState>,
101    ) -> Self {
102        Self {
103            splittable_editor,
104            style,
105            split_state,
106        }
107    }
108}
109
110fn render_resize_handle(
111    state: &Entity<SplitEditorState>,
112    separator_color: Hsla,
113    _window: &mut Window,
114    _cx: &mut App,
115) -> AnyElement {
116    let state_for_click = state.clone();
117
118    div()
119        .id("split-resize-container")
120        .relative()
121        .h_full()
122        .flex_shrink_0()
123        .w(px(1.))
124        .bg(separator_color)
125        .child(
126            div()
127                .id("split-resize-handle")
128                .absolute()
129                .left(px(-RESIZE_HANDLE_WIDTH / 2.0))
130                .w(px(RESIZE_HANDLE_WIDTH))
131                .h_full()
132                .cursor_col_resize()
133                .block_mouse_except_scroll()
134                .on_click(move |event, _, cx| {
135                    if event.click_count() >= 2 {
136                        state_for_click.update(cx, |state, _| {
137                            state.on_double_click();
138                        });
139                    }
140                    cx.stop_propagation();
141                })
142                .on_drag(DraggedSplitHandle, |_, _, _, cx| cx.new(|_| gpui::Empty)),
143        )
144        .into_any_element()
145}
146
147impl RenderOnce for SplitEditorView {
148    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
149        let splittable_editor = self.splittable_editor.read(cx);
150
151        assert!(
152            splittable_editor.lhs_editor().is_some(),
153            "`SplitEditorView` requires `SplittableEditor` to be in split mode"
154        );
155
156        let lhs_editor = splittable_editor.lhs_editor().unwrap().clone();
157        let rhs_editor = splittable_editor.rhs_editor().clone();
158
159        let mut lhs = EditorElement::new(&lhs_editor, self.style.clone());
160        let mut rhs = EditorElement::new(&rhs_editor, self.style.clone());
161
162        lhs.set_split_side(SplitSide::Left);
163        rhs.set_split_side(SplitSide::Right);
164
165        let left_ratio = self.split_state.read(cx).left_ratio();
166        let right_ratio = self.split_state.read(cx).right_ratio();
167
168        let separator_color = cx.theme().colors().border_variant;
169
170        let resize_handle = render_resize_handle(&self.split_state, separator_color, window, cx);
171
172        let state_for_drag = self.split_state.downgrade();
173        let state_for_drop = self.split_state.downgrade();
174
175        let buffer_headers = SplitBufferHeadersElement::new(rhs_editor.clone(), self.style.clone());
176
177        let lhs_editor_for_order = lhs_editor;
178        let rhs_editor_for_order = rhs_editor;
179
180        div()
181            .id("split-editor-view-container")
182            .size_full()
183            .relative()
184            .child(
185                h_flex()
186                    .with_dynamic_prepaint_order(move |_window, cx| {
187                        let lhs_needs = lhs_editor_for_order.read(cx).has_autoscroll_request();
188                        let rhs_needs = rhs_editor_for_order.read(cx).has_autoscroll_request();
189                        match (lhs_needs, rhs_needs) {
190                            (false, true) => smallvec![2, 1, 0],
191                            _ => smallvec![0, 1, 2],
192                        }
193                    })
194                    .id("split-editor-view")
195                    .size_full()
196                    .on_drag_move::<DraggedSplitHandle>(move |event, window, cx| {
197                        state_for_drag
198                            .update(cx, |state, cx| {
199                                state.on_drag_move(event, window, cx);
200                            })
201                            .ok();
202                    })
203                    .on_drop::<DraggedSplitHandle>(move |_, _, cx| {
204                        state_for_drop
205                            .update(cx, |state, _| {
206                                state.commit_ratio();
207                            })
208                            .ok();
209                    })
210                    .child(
211                        div()
212                            .id("split-editor-left")
213                            .flex_shrink()
214                            .min_w_0()
215                            .h_full()
216                            .flex_basis(DefiniteLength::Fraction(left_ratio))
217                            .overflow_hidden()
218                            .child(lhs),
219                    )
220                    .child(resize_handle)
221                    .child(
222                        div()
223                            .id("split-editor-right")
224                            .flex_shrink()
225                            .min_w_0()
226                            .h_full()
227                            .flex_basis(DefiniteLength::Fraction(right_ratio))
228                            .overflow_hidden()
229                            .child(rhs),
230                    ),
231            )
232            .child(buffer_headers)
233    }
234}
235
236struct SplitBufferHeadersElement {
237    editor: Entity<Editor>,
238    style: EditorStyle,
239}
240
241impl SplitBufferHeadersElement {
242    fn new(editor: Entity<Editor>, style: EditorStyle) -> Self {
243        Self { editor, style }
244    }
245}
246
247struct BufferHeaderLayout {
248    element: AnyElement,
249}
250
251struct SplitBufferHeadersPrepaintState {
252    sticky_header: Option<AnyElement>,
253    non_sticky_headers: Vec<BufferHeaderLayout>,
254}
255
256impl IntoElement for SplitBufferHeadersElement {
257    type Element = Self;
258
259    fn into_element(self) -> Self::Element {
260        self
261    }
262}
263
264impl Element for SplitBufferHeadersElement {
265    type RequestLayoutState = ();
266    type PrepaintState = SplitBufferHeadersPrepaintState;
267
268    fn id(&self) -> Option<gpui::ElementId> {
269        Some("split-buffer-headers".into())
270    }
271
272    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
273        None
274    }
275
276    fn request_layout(
277        &mut self,
278        _id: Option<&GlobalElementId>,
279        _inspector_id: Option<&InspectorElementId>,
280        window: &mut Window,
281        _cx: &mut App,
282    ) -> (LayoutId, Self::RequestLayoutState) {
283        let mut style = gpui::Style::default();
284        style.position = gpui::Position::Absolute;
285        style.inset.top = DefiniteLength::Fraction(0.0).into();
286        style.inset.left = DefiniteLength::Fraction(0.0).into();
287        style.size.width = Length::Definite(DefiniteLength::Fraction(1.0));
288        style.size.height = Length::Definite(DefiniteLength::Fraction(1.0));
289        let layout_id = window.request_layout(style, [], _cx);
290        (layout_id, ())
291    }
292
293    fn prepaint(
294        &mut self,
295        _id: Option<&GlobalElementId>,
296        _inspector_id: Option<&InspectorElementId>,
297        bounds: Bounds<Pixels>,
298        _request_layout: &mut Self::RequestLayoutState,
299        window: &mut Window,
300        cx: &mut App,
301    ) -> Self::PrepaintState {
302        if bounds.size.width <= px(0.) || bounds.size.height <= px(0.) {
303            return SplitBufferHeadersPrepaintState {
304                sticky_header: None,
305                non_sticky_headers: Vec::new(),
306            };
307        }
308
309        let rem_size = self.rem_size();
310        let text_style = TextStyleRefinement {
311            font_size: Some(self.style.text.font_size),
312            line_height: Some(self.style.text.line_height),
313            ..Default::default()
314        };
315
316        window.with_rem_size(rem_size, |window| {
317            window.with_text_style(Some(text_style), |window| {
318                Self::prepaint_inner(self, bounds, window, cx)
319            })
320        })
321    }
322
323    fn paint(
324        &mut self,
325        _id: Option<&GlobalElementId>,
326        _inspector_id: Option<&InspectorElementId>,
327        bounds: Bounds<Pixels>,
328        _request_layout: &mut Self::RequestLayoutState,
329        prepaint: &mut Self::PrepaintState,
330        window: &mut Window,
331        cx: &mut App,
332    ) {
333        let rem_size = self.rem_size();
334        let text_style = TextStyleRefinement {
335            font_size: Some(self.style.text.font_size),
336            line_height: Some(self.style.text.line_height),
337            ..Default::default()
338        };
339
340        window.with_rem_size(rem_size, |window| {
341            window.with_text_style(Some(text_style), |window| {
342                window.with_content_mask(Some(ContentMask { bounds }), |window| {
343                    for header_layout in &mut prepaint.non_sticky_headers {
344                        header_layout.element.paint(window, cx);
345                    }
346
347                    if let Some(mut sticky_header) = prepaint.sticky_header.take() {
348                        sticky_header.paint(window, cx);
349                    }
350                });
351            });
352        });
353    }
354}
355
356impl SplitBufferHeadersElement {
357    fn rem_size(&self) -> Option<Pixels> {
358        match self.style.text.font_size {
359            AbsoluteLength::Pixels(pixels) => {
360                let rem_size_scale = {
361                    let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX;
362                    let default_font_size_delta = 1. - default_font_size_scale;
363                    1. + default_font_size_delta
364                };
365
366                Some(pixels * rem_size_scale)
367            }
368            AbsoluteLength::Rems(rems) => Some(rems.to_pixels(ui::BASE_REM_SIZE_IN_PX.into())),
369        }
370    }
371
372    fn prepaint_inner(
373        &mut self,
374        bounds: Bounds<Pixels>,
375        window: &mut Window,
376        cx: &mut App,
377    ) -> SplitBufferHeadersPrepaintState {
378        let line_height = window.line_height();
379
380        let snapshot = self
381            .editor
382            .update(cx, |editor, cx| editor.snapshot(window, cx));
383        let scroll_position = snapshot.scroll_position();
384
385        // Compute right margin to avoid overlapping the scrollbar
386        let settings = EditorSettings::get_global(cx);
387        let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never;
388        let vertical_scrollbar_width = (scrollbars_shown
389            && settings.scrollbar.axes.vertical
390            && self.editor.read(cx).show_scrollbars.vertical)
391            .then_some(EditorElement::SCROLLBAR_WIDTH)
392            .unwrap_or_default();
393        let available_width = bounds.size.width - vertical_scrollbar_width;
394
395        let visible_height_in_lines = bounds.size.height / line_height;
396        let max_row = snapshot.max_point().row();
397        let start_row = cmp::min(DisplayRow(scroll_position.y.floor() as u32), max_row);
398        let end_row = cmp::min(
399            (scroll_position.y + visible_height_in_lines as f64).ceil() as u32,
400            max_row.next_row().0,
401        );
402        let end_row = DisplayRow(end_row);
403
404        let (selected_buffer_ids, latest_selection_anchors) =
405            self.compute_selection_info(&snapshot, cx);
406
407        let sticky_header = if snapshot.buffer_snapshot().show_headers() {
408            snapshot
409                .sticky_header_excerpt(scroll_position.y)
410                .map(|sticky_excerpt| {
411                    self.build_sticky_header(
412                        sticky_excerpt,
413                        &snapshot,
414                        scroll_position,
415                        bounds,
416                        available_width,
417                        line_height,
418                        &selected_buffer_ids,
419                        &latest_selection_anchors,
420                        start_row,
421                        end_row,
422                        window,
423                        cx,
424                    )
425                })
426        } else {
427            None
428        };
429
430        let sticky_header_excerpt_id = snapshot
431            .sticky_header_excerpt(scroll_position.y)
432            .map(|e| e.excerpt.id);
433
434        let non_sticky_headers = self.build_non_sticky_headers(
435            &snapshot,
436            scroll_position,
437            bounds,
438            available_width,
439            line_height,
440            start_row,
441            end_row,
442            &selected_buffer_ids,
443            &latest_selection_anchors,
444            sticky_header_excerpt_id,
445            window,
446            cx,
447        );
448
449        SplitBufferHeadersPrepaintState {
450            sticky_header,
451            non_sticky_headers,
452        }
453    }
454
455    fn compute_selection_info(
456        &self,
457        snapshot: &EditorSnapshot,
458        cx: &App,
459    ) -> (HashSet<BufferId>, HashMap<BufferId, Anchor>) {
460        let editor = self.editor.read(cx);
461        let all_selections = editor
462            .selections
463            .all::<crate::Point>(&snapshot.display_snapshot);
464        let all_anchor_selections = editor.selections.all_anchors(&snapshot.display_snapshot);
465
466        let mut selected_buffer_ids = HashSet::default();
467        for selection in &all_selections {
468            for buffer_id in snapshot
469                .buffer_snapshot()
470                .buffer_ids_for_range(selection.range())
471            {
472                selected_buffer_ids.insert(buffer_id);
473            }
474        }
475
476        let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> = HashMap::default();
477        for selection in all_anchor_selections.iter() {
478            let head = selection.head();
479            if let Some(buffer_id) = head.text_anchor.buffer_id {
480                anchors_by_buffer
481                    .entry(buffer_id)
482                    .and_modify(|(latest_id, latest_anchor)| {
483                        if selection.id > *latest_id {
484                            *latest_id = selection.id;
485                            *latest_anchor = head;
486                        }
487                    })
488                    .or_insert((selection.id, head));
489            }
490        }
491        let latest_selection_anchors = anchors_by_buffer
492            .into_iter()
493            .map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
494            .collect();
495
496        (selected_buffer_ids, latest_selection_anchors)
497    }
498
499    fn build_sticky_header(
500        &self,
501        StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
502        snapshot: &EditorSnapshot,
503        scroll_position: gpui::Point<ScrollOffset>,
504        bounds: Bounds<Pixels>,
505        available_width: Pixels,
506        line_height: Pixels,
507        selected_buffer_ids: &HashSet<BufferId>,
508        latest_selection_anchors: &HashMap<BufferId, Anchor>,
509        start_row: DisplayRow,
510        end_row: DisplayRow,
511        window: &mut Window,
512        cx: &mut App,
513    ) -> AnyElement {
514        let jump_data = header_jump_data(
515            snapshot,
516            DisplayRow(scroll_position.y as u32),
517            FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
518            excerpt,
519            latest_selection_anchors,
520        );
521
522        let editor_bg_color = cx.theme().colors().editor_background;
523        let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
524
525        let mut header = v_flex()
526            .id("sticky-buffer-header")
527            .w(available_width)
528            .relative()
529            .child(
530                div()
531                    .w(available_width)
532                    .h(FILE_HEADER_HEIGHT as f32 * line_height)
533                    .bg(linear_gradient(
534                        0.,
535                        linear_color_stop(editor_bg_color.opacity(0.), 0.),
536                        linear_color_stop(editor_bg_color, 0.6),
537                    ))
538                    .absolute()
539                    .top_0(),
540            )
541            .child(
542                render_buffer_header(
543                    &self.editor,
544                    excerpt,
545                    false,
546                    selected,
547                    true,
548                    jump_data,
549                    window,
550                    cx,
551                )
552                .into_any_element(),
553            )
554            .into_any_element();
555
556        let mut origin = bounds.origin;
557
558        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
559            if !block.is_buffer_header() {
560                continue;
561            }
562
563            if block_row.0 <= scroll_position.y as u32 {
564                continue;
565            }
566
567            let max_row = block_row.0.saturating_sub(FILE_HEADER_HEIGHT);
568            let offset = scroll_position.y - max_row as f64;
569
570            if offset > 0.0 {
571                origin.y -= Pixels::from(offset * f64::from(line_height));
572            }
573            break;
574        }
575
576        let available_size = size(
577            AvailableSpace::Definite(available_width),
578            AvailableSpace::MinContent,
579        );
580
581        header.prepaint_as_root(origin, available_size, window, cx);
582
583        header
584    }
585
586    fn build_non_sticky_headers(
587        &self,
588        snapshot: &EditorSnapshot,
589        scroll_position: gpui::Point<ScrollOffset>,
590        bounds: Bounds<Pixels>,
591        available_width: Pixels,
592        line_height: Pixels,
593        start_row: DisplayRow,
594        end_row: DisplayRow,
595        selected_buffer_ids: &HashSet<BufferId>,
596        latest_selection_anchors: &HashMap<BufferId, Anchor>,
597        sticky_header_excerpt_id: Option<ExcerptId>,
598        window: &mut Window,
599        cx: &mut App,
600    ) -> Vec<BufferHeaderLayout> {
601        let mut headers = Vec::new();
602
603        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
604            let (excerpt, is_folded) = match block {
605                Block::BufferHeader { excerpt, .. } => {
606                    if sticky_header_excerpt_id == Some(excerpt.id) {
607                        continue;
608                    }
609                    (excerpt, false)
610                }
611                Block::FoldedBuffer { first_excerpt, .. } => (first_excerpt, true),
612                // ExcerptBoundary is just a separator line, not a buffer header
613                Block::ExcerptBoundary { .. } | Block::Custom(_) | Block::Spacer { .. } => continue,
614            };
615
616            let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
617            let jump_data = header_jump_data(
618                snapshot,
619                block_row,
620                block.height(),
621                excerpt,
622                latest_selection_anchors,
623            );
624
625            let mut header = render_buffer_header(
626                &self.editor,
627                excerpt,
628                is_folded,
629                selected,
630                false,
631                jump_data,
632                window,
633                cx,
634            )
635            .into_any_element();
636
637            let y_offset = (block_row.0 as f64 - scroll_position.y) * f64::from(line_height);
638            let origin = point(bounds.origin.x, bounds.origin.y + Pixels::from(y_offset));
639
640            let available_size = size(
641                AvailableSpace::Definite(available_width),
642                AvailableSpace::MinContent,
643            );
644
645            header.prepaint_as_root(origin, available_size, window, cx);
646
647            headers.push(BufferHeaderLayout { element: header });
648        }
649
650        headers
651    }
652}