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}