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, ExcerptBoundaryInfo};
 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);
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((text_anchor, _)) = snapshot.buffer_snapshot().anchor_to_buffer_anchor(head)
480            {
481                anchors_by_buffer
482                    .entry(text_anchor.buffer_id)
483                    .and_modify(|(latest_id, latest_anchor)| {
484                        if selection.id > *latest_id {
485                            *latest_id = selection.id;
486                            *latest_anchor = head;
487                        }
488                    })
489                    .or_insert((selection.id, head));
490            }
491        }
492        let latest_selection_anchors = anchors_by_buffer
493            .into_iter()
494            .map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
495            .collect();
496
497        (selected_buffer_ids, latest_selection_anchors)
498    }
499
500    fn build_sticky_header(
501        &self,
502        StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
503        snapshot: &EditorSnapshot,
504        scroll_position: gpui::Point<ScrollOffset>,
505        bounds: Bounds<Pixels>,
506        available_width: Pixels,
507        line_height: Pixels,
508        selected_buffer_ids: &HashSet<BufferId>,
509        latest_selection_anchors: &HashMap<BufferId, Anchor>,
510        start_row: DisplayRow,
511        end_row: DisplayRow,
512        window: &mut Window,
513        cx: &mut App,
514    ) -> AnyElement {
515        let jump_data = header_jump_data(
516            snapshot,
517            DisplayRow(scroll_position.y as u32),
518            FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
519            excerpt,
520            latest_selection_anchors,
521        );
522
523        let editor_bg_color = cx.theme().colors().editor_background;
524        let selected = selected_buffer_ids.contains(&excerpt.buffer_id());
525
526        let mut header = v_flex()
527            .id("sticky-buffer-header")
528            .w(available_width)
529            .relative()
530            .child(
531                div()
532                    .w(available_width)
533                    .h(FILE_HEADER_HEIGHT as f32 * line_height)
534                    .bg(linear_gradient(
535                        0.,
536                        linear_color_stop(editor_bg_color.opacity(0.), 0.),
537                        linear_color_stop(editor_bg_color, 0.6),
538                    ))
539                    .absolute()
540                    .top_0(),
541            )
542            .child(
543                render_buffer_header(
544                    &self.editor,
545                    excerpt,
546                    false,
547                    selected,
548                    true,
549                    jump_data,
550                    window,
551                    cx,
552                )
553                .into_any_element(),
554            )
555            .into_any_element();
556
557        let mut origin = bounds.origin;
558
559        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
560            if !block.is_buffer_header() {
561                continue;
562            }
563
564            if block_row.0 <= scroll_position.y as u32 {
565                continue;
566            }
567
568            let max_row = block_row.0.saturating_sub(FILE_HEADER_HEIGHT);
569            let offset = scroll_position.y - max_row as f64;
570
571            if offset > 0.0 {
572                origin.y -= Pixels::from(offset * f64::from(line_height));
573            }
574            break;
575        }
576
577        let available_size = size(
578            AvailableSpace::Definite(available_width),
579            AvailableSpace::MinContent,
580        );
581
582        header.prepaint_as_root(origin, available_size, window, cx);
583
584        header
585    }
586
587    fn build_non_sticky_headers(
588        &self,
589        snapshot: &EditorSnapshot,
590        scroll_position: gpui::Point<ScrollOffset>,
591        bounds: Bounds<Pixels>,
592        available_width: Pixels,
593        line_height: Pixels,
594        start_row: DisplayRow,
595        end_row: DisplayRow,
596        selected_buffer_ids: &HashSet<BufferId>,
597        latest_selection_anchors: &HashMap<BufferId, Anchor>,
598        sticky_header: Option<&ExcerptBoundaryInfo>,
599        window: &mut Window,
600        cx: &mut App,
601    ) -> Vec<BufferHeaderLayout> {
602        let mut headers = Vec::new();
603
604        for (block_row, block) in snapshot.blocks_in_range(start_row..end_row) {
605            let (excerpt, is_folded) = match block {
606                Block::BufferHeader { excerpt, .. } => {
607                    if sticky_header == Some(excerpt) {
608                        continue;
609                    }
610                    (excerpt, false)
611                }
612                Block::FoldedBuffer { first_excerpt, .. } => (first_excerpt, true),
613                // ExcerptBoundary is just a separator line, not a buffer header
614                Block::ExcerptBoundary { .. } | Block::Custom(_) | Block::Spacer { .. } => continue,
615            };
616
617            let selected = selected_buffer_ids.contains(&excerpt.buffer_id());
618            let jump_data = header_jump_data(
619                snapshot,
620                block_row,
621                block.height(),
622                excerpt,
623                latest_selection_anchors,
624            );
625
626            let mut header = render_buffer_header(
627                &self.editor,
628                excerpt,
629                is_folded,
630                selected,
631                false,
632                jump_data,
633                window,
634                cx,
635            )
636            .into_any_element();
637
638            let y_offset = (block_row.0 as f64 - scroll_position.y) * f64::from(line_height);
639            let origin = point(bounds.origin.x, bounds.origin.y + Pixels::from(y_offset));
640
641            let available_size = size(
642                AvailableSpace::Definite(available_width),
643                AvailableSpace::MinContent,
644            );
645
646            header.prepaint_as_root(origin, available_size, window, cx);
647
648            headers.push(BufferHeaderLayout { element: header });
649        }
650
651        headers
652    }
653}