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