1use crate::{
2 AnyActiveCall, AppState, CollaboratorId, FollowerState, Pane, ParticipantLocation, Workspace,
3 WorkspaceSettings,
4 pane_group::element::pane_axis,
5 workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
6};
7use anyhow::Result;
8use collections::HashMap;
9use gpui::{
10 Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
11 Point, StyleRefinement, WeakEntity, Window, point, size,
12};
13use parking_lot::Mutex;
14use project::Project;
15use schemars::JsonSchema;
16use serde::Deserialize;
17use settings::Settings;
18use std::sync::Arc;
19use ui::prelude::*;
20
21pub const HANDLE_HITBOX_SIZE: f32 = 4.0;
22const HORIZONTAL_MIN_SIZE: f32 = 80.;
23const VERTICAL_MIN_SIZE: f32 = 100.;
24
25/// One or many panes, arranged in a horizontal or vertical axis due to a split.
26/// Panes have all their tabs and capabilities preserved, and can be split again or resized.
27/// Single-pane group is a regular pane.
28#[derive(Clone)]
29pub struct PaneGroup {
30 pub root: Member,
31 pub is_center: bool,
32}
33
34pub struct PaneRenderResult {
35 pub element: gpui::AnyElement,
36 pub contains_active_pane: bool,
37}
38
39impl PaneGroup {
40 pub fn with_root(root: Member) -> Self {
41 Self {
42 root,
43 is_center: false,
44 }
45 }
46
47 pub fn new(pane: Entity<Pane>) -> Self {
48 Self {
49 root: Member::Pane(pane),
50 is_center: false,
51 }
52 }
53
54 pub fn set_is_center(&mut self, is_center: bool) {
55 self.is_center = is_center;
56 }
57
58 pub fn split(
59 &mut self,
60 old_pane: &Entity<Pane>,
61 new_pane: &Entity<Pane>,
62 direction: SplitDirection,
63 cx: &mut App,
64 ) {
65 let found = match &mut self.root {
66 Member::Pane(pane) => {
67 if pane == old_pane {
68 self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
69 true
70 } else {
71 false
72 }
73 }
74 Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
75 };
76
77 // If the pane wasn't found, fall back to splitting the first pane in the tree.
78 if !found {
79 let first_pane = self.root.first_pane();
80 match &mut self.root {
81 Member::Pane(_) => {
82 self.root = Member::new_axis(first_pane, new_pane.clone(), direction);
83 }
84 Member::Axis(axis) => {
85 let _ = axis.split(&first_pane, new_pane, direction);
86 }
87 }
88 }
89
90 self.mark_positions(cx);
91 }
92
93 pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
94 match &self.root {
95 Member::Pane(_) => None,
96 Member::Axis(axis) => axis.bounding_box_for_pane(pane),
97 }
98 }
99
100 pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
101 match &self.root {
102 Member::Pane(pane) => Some(pane),
103 Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
104 }
105 }
106
107 /// Moves active pane to span the entire border in the given direction,
108 /// similar to Vim ctrl+w shift-[hjkl] motion.
109 ///
110 /// Returns:
111 /// - Ok(true) if it found and moved a pane
112 /// - Ok(false) if it found but did not move the pane
113 /// - Err(_) if it did not find the pane
114 pub fn move_to_border(
115 &mut self,
116 active_pane: &Entity<Pane>,
117 direction: SplitDirection,
118 cx: &mut App,
119 ) -> Result<bool> {
120 if let Some(pane) = self.find_pane_at_border(direction)
121 && pane == active_pane
122 {
123 return Ok(false);
124 }
125
126 if !self.remove_internal(active_pane)? {
127 return Ok(false);
128 }
129
130 if let Member::Axis(root) = &mut self.root
131 && direction.axis() == root.axis
132 {
133 let idx = if direction.increasing() {
134 root.members.len()
135 } else {
136 0
137 };
138 root.insert_pane(idx, active_pane);
139 self.mark_positions(cx);
140 return Ok(true);
141 }
142
143 let members = if direction.increasing() {
144 vec![self.root.clone(), Member::Pane(active_pane.clone())]
145 } else {
146 vec![Member::Pane(active_pane.clone()), self.root.clone()]
147 };
148 self.root = Member::Axis(PaneAxis::new(direction.axis(), members));
149 self.mark_positions(cx);
150 Ok(true)
151 }
152
153 fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity<Pane>> {
154 match &self.root {
155 Member::Pane(pane) => Some(pane),
156 Member::Axis(axis) => axis.find_pane_at_border(direction),
157 }
158 }
159
160 /// Returns:
161 /// - Ok(true) if it found and removed a pane
162 /// - Ok(false) if it found but did not remove the pane
163 /// - Err(_) if it did not find the pane
164 pub fn remove(&mut self, pane: &Entity<Pane>, cx: &mut App) -> Result<bool> {
165 let result = self.remove_internal(pane);
166 if let Ok(true) = result {
167 self.mark_positions(cx);
168 }
169 result
170 }
171
172 fn remove_internal(&mut self, pane: &Entity<Pane>) -> Result<bool> {
173 match &mut self.root {
174 Member::Pane(_) => Ok(false),
175 Member::Axis(axis) => {
176 if let Some(last_pane) = axis.remove(pane)? {
177 self.root = last_pane;
178 }
179 Ok(true)
180 }
181 }
182 }
183
184 pub fn resize(
185 &mut self,
186 pane: &Entity<Pane>,
187 direction: Axis,
188 amount: Pixels,
189 bounds: &Bounds<Pixels>,
190 cx: &mut App,
191 ) {
192 match &mut self.root {
193 Member::Pane(_) => {}
194 Member::Axis(axis) => {
195 let _ = axis.resize(pane, direction, amount, bounds);
196 }
197 };
198 self.mark_positions(cx);
199 }
200
201 pub fn reset_pane_sizes(&mut self, cx: &mut App) {
202 match &mut self.root {
203 Member::Pane(_) => {}
204 Member::Axis(axis) => {
205 let _ = axis.reset_pane_sizes();
206 }
207 };
208 self.mark_positions(cx);
209 }
210
211 pub fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>, cx: &mut App) {
212 match &mut self.root {
213 Member::Pane(_) => {}
214 Member::Axis(axis) => axis.swap(from, to),
215 };
216 self.mark_positions(cx);
217 }
218
219 pub fn mark_positions(&mut self, cx: &mut App) {
220 self.root.mark_positions(self.is_center, cx);
221 }
222
223 pub fn render(
224 &self,
225 zoomed: Option<&AnyWeakView>,
226 render_cx: &dyn PaneLeaderDecorator,
227 window: &mut Window,
228 cx: &mut App,
229 ) -> impl IntoElement {
230 self.root.render(0, zoomed, render_cx, window, cx).element
231 }
232
233 pub fn panes(&self) -> Vec<&Entity<Pane>> {
234 let mut panes = Vec::new();
235 self.root.collect_panes(&mut panes);
236 panes
237 }
238
239 pub fn first_pane(&self) -> Entity<Pane> {
240 self.root.first_pane()
241 }
242
243 pub fn last_pane(&self) -> Entity<Pane> {
244 self.root.last_pane()
245 }
246
247 pub fn find_pane_in_direction(
248 &mut self,
249 active_pane: &Entity<Pane>,
250 direction: SplitDirection,
251 cx: &App,
252 ) -> Option<&Entity<Pane>> {
253 let bounding_box = self.bounding_box_for_pane(active_pane)?;
254 let cursor = active_pane.read(cx).pixel_position_of_cursor(cx);
255 let center = match cursor {
256 Some(cursor) if bounding_box.contains(&cursor) => cursor,
257 _ => bounding_box.center(),
258 };
259
260 let distance_to_next = crate::HANDLE_HITBOX_SIZE;
261
262 let target = match direction {
263 SplitDirection::Left => {
264 Point::new(bounding_box.left() - distance_to_next.into(), center.y)
265 }
266 SplitDirection::Right => {
267 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
268 }
269 SplitDirection::Up => {
270 Point::new(center.x, bounding_box.top() - distance_to_next.into())
271 }
272 SplitDirection::Down => {
273 Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
274 }
275 };
276 self.pane_at_pixel_position(target)
277 }
278
279 pub fn invert_axies(&mut self, cx: &mut App) {
280 self.root.invert_pane_axies();
281 self.mark_positions(cx);
282 }
283}
284
285#[derive(Debug, Clone)]
286pub enum Member {
287 Axis(PaneAxis),
288 Pane(Entity<Pane>),
289}
290
291impl Member {
292 pub fn mark_positions(&mut self, in_center_group: bool, cx: &mut App) {
293 match self {
294 Member::Axis(pane_axis) => {
295 for member in pane_axis.members.iter_mut() {
296 member.mark_positions(in_center_group, cx);
297 }
298 }
299 Member::Pane(entity) => entity.update(cx, |pane, _| {
300 pane.in_center_group = in_center_group;
301 }),
302 }
303 }
304}
305
306#[derive(Clone, Copy)]
307pub struct PaneRenderContext<'a> {
308 pub project: &'a Entity<Project>,
309 pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
310 pub active_call: Option<&'a dyn AnyActiveCall>,
311 pub active_pane: &'a Entity<Pane>,
312 pub app_state: &'a Arc<AppState>,
313 pub workspace: &'a WeakEntity<Workspace>,
314}
315
316#[derive(Default)]
317pub struct LeaderDecoration {
318 border: Option<Hsla>,
319 status_box: Option<AnyElement>,
320}
321
322pub trait PaneLeaderDecorator {
323 fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration;
324 fn active_pane(&self) -> &Entity<Pane>;
325 fn workspace(&self) -> &WeakEntity<Workspace>;
326}
327
328pub struct ActivePaneDecorator<'a> {
329 active_pane: &'a Entity<Pane>,
330 workspace: &'a WeakEntity<Workspace>,
331}
332
333impl<'a> ActivePaneDecorator<'a> {
334 pub fn new(active_pane: &'a Entity<Pane>, workspace: &'a WeakEntity<Workspace>) -> Self {
335 Self {
336 active_pane,
337 workspace,
338 }
339 }
340}
341
342impl PaneLeaderDecorator for ActivePaneDecorator<'_> {
343 fn decorate(&self, _: &Entity<Pane>, _: &App) -> LeaderDecoration {
344 LeaderDecoration::default()
345 }
346 fn active_pane(&self) -> &Entity<Pane> {
347 self.active_pane
348 }
349
350 fn workspace(&self) -> &WeakEntity<Workspace> {
351 self.workspace
352 }
353}
354
355impl PaneLeaderDecorator for PaneRenderContext<'_> {
356 fn decorate(&self, pane: &Entity<Pane>, cx: &App) -> LeaderDecoration {
357 let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| {
358 if state.center_pane == *pane {
359 Some((*leader_id, state))
360 } else {
361 None
362 }
363 });
364 let Some((leader_id, follower_state)) = follower_state else {
365 return LeaderDecoration::default();
366 };
367
368 let mut leader_color;
369 let status_box;
370 match leader_id {
371 CollaboratorId::PeerId(peer_id) => {
372 let Some(leader) = self
373 .active_call
374 .as_ref()
375 .and_then(|call| call.remote_participant_for_peer_id(peer_id, cx))
376 else {
377 return LeaderDecoration::default();
378 };
379
380 let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| {
381 !follower_state
382 .items_by_leader_view_id
383 .contains_key(&view_id)
384 });
385
386 let mut leader_join_data = None;
387 let leader_status_box = match leader.location {
388 ParticipantLocation::SharedProject {
389 project_id: leader_project_id,
390 } => {
391 if Some(leader_project_id) == self.project.read(cx).remote_id() {
392 is_in_unshared_view.then(|| {
393 Label::new(format!(
394 "{} is in an unshared pane",
395 leader.user.github_login
396 ))
397 })
398 } else {
399 leader_join_data = Some((leader_project_id, leader.user.id));
400 Some(Label::new(format!(
401 "Follow {} to their active project",
402 leader.user.github_login,
403 )))
404 }
405 }
406 ParticipantLocation::UnsharedProject => Some(Label::new(format!(
407 "{} is viewing an unshared Zed project",
408 leader.user.github_login
409 ))),
410 ParticipantLocation::External => Some(Label::new(format!(
411 "{} is viewing a window outside of Zed",
412 leader.user.github_login
413 ))),
414 };
415 status_box = leader_status_box.map(|status| {
416 div()
417 .absolute()
418 .w_96()
419 .bottom_3()
420 .right_3()
421 .elevation_2(cx)
422 .p_1()
423 .child(status)
424 .when_some(
425 leader_join_data,
426 |this, (leader_project_id, leader_user_id)| {
427 let app_state = self.app_state.clone();
428 this.cursor_pointer().on_mouse_down(
429 MouseButton::Left,
430 move |_, _, cx| {
431 crate::join_in_room_project(
432 leader_project_id,
433 leader_user_id,
434 app_state.clone(),
435 cx,
436 )
437 .detach_and_log_err(cx);
438 },
439 )
440 },
441 )
442 .into_any_element()
443 });
444 leader_color = cx
445 .theme()
446 .players()
447 .color_for_participant(leader.participant_index.0)
448 .cursor;
449 }
450 CollaboratorId::Agent => {
451 status_box = None;
452 leader_color = cx.theme().players().agent().cursor;
453 }
454 }
455
456 let is_in_panel = follower_state.dock_pane.is_some();
457 if is_in_panel {
458 leader_color.fade_out(0.75);
459 } else {
460 leader_color.fade_out(0.3);
461 }
462
463 LeaderDecoration {
464 status_box,
465 border: Some(leader_color),
466 }
467 }
468
469 fn active_pane(&self) -> &Entity<Pane> {
470 self.active_pane
471 }
472
473 fn workspace(&self) -> &WeakEntity<Workspace> {
474 self.workspace
475 }
476}
477
478impl Member {
479 fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
480 use Axis::*;
481 use SplitDirection::*;
482
483 let axis = match direction {
484 Up | Down => Vertical,
485 Left | Right => Horizontal,
486 };
487
488 let members = match direction {
489 Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
490 Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
491 };
492
493 Member::Axis(PaneAxis::new(axis, members))
494 }
495
496 fn first_pane(&self) -> Entity<Pane> {
497 match self {
498 Member::Axis(axis) => axis.members[0].first_pane(),
499 Member::Pane(pane) => pane.clone(),
500 }
501 }
502
503 fn last_pane(&self) -> Entity<Pane> {
504 match self {
505 Member::Axis(axis) => axis.members.last().unwrap().last_pane(),
506 Member::Pane(pane) => pane.clone(),
507 }
508 }
509
510 pub fn render(
511 &self,
512 basis: usize,
513 zoomed: Option<&AnyWeakView>,
514 render_cx: &dyn PaneLeaderDecorator,
515 window: &mut Window,
516 cx: &mut App,
517 ) -> PaneRenderResult {
518 match self {
519 Member::Pane(pane) => {
520 if zoomed == Some(&pane.downgrade().into()) {
521 return PaneRenderResult {
522 element: div().into_any(),
523 contains_active_pane: false,
524 };
525 }
526
527 let decoration = render_cx.decorate(pane, cx);
528 let is_active = pane == render_cx.active_pane();
529
530 PaneRenderResult {
531 element: div()
532 .relative()
533 .flex_1()
534 .size_full()
535 .child(
536 AnyView::from(pane.clone())
537 .cached(StyleRefinement::default().v_flex().size_full()),
538 )
539 .when_some(decoration.border, |this, color| {
540 this.child(
541 div()
542 .absolute()
543 .size_full()
544 .left_0()
545 .top_0()
546 .border_2()
547 .border_color(color),
548 )
549 })
550 .children(decoration.status_box)
551 .into_any(),
552 contains_active_pane: is_active,
553 }
554 }
555 Member::Axis(axis) => axis.render(basis + 1, zoomed, render_cx, window, cx),
556 }
557 }
558
559 fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a Entity<Pane>>) {
560 match self {
561 Member::Axis(axis) => {
562 for member in &axis.members {
563 member.collect_panes(panes);
564 }
565 }
566 Member::Pane(pane) => panes.push(pane),
567 }
568 }
569
570 fn invert_pane_axies(&mut self) {
571 match self {
572 Self::Axis(axis) => {
573 axis.axis = axis.axis.invert();
574 for member in axis.members.iter_mut() {
575 member.invert_pane_axies();
576 }
577 }
578 Self::Pane(_) => {}
579 }
580 }
581}
582
583#[derive(Debug, Clone)]
584pub struct PaneAxis {
585 pub axis: Axis,
586 pub members: Vec<Member>,
587 pub flexes: Arc<Mutex<Vec<f32>>>,
588 pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
589}
590
591impl PaneAxis {
592 pub fn new(axis: Axis, members: Vec<Member>) -> Self {
593 let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
594 let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
595 Self {
596 axis,
597 members,
598 flexes,
599 bounding_boxes,
600 }
601 }
602
603 pub fn load(axis: Axis, members: Vec<Member>, flexes: Option<Vec<f32>>) -> Self {
604 let mut flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]);
605 if flexes.len() != members.len()
606 || (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() >= 0.001
607 {
608 flexes = vec![1.; members.len()];
609 }
610
611 let flexes = Arc::new(Mutex::new(flexes));
612 let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()]));
613 Self {
614 axis,
615 members,
616 flexes,
617 bounding_boxes,
618 }
619 }
620
621 fn split(
622 &mut self,
623 old_pane: &Entity<Pane>,
624 new_pane: &Entity<Pane>,
625 direction: SplitDirection,
626 ) -> bool {
627 for (mut idx, member) in self.members.iter_mut().enumerate() {
628 match member {
629 Member::Axis(axis) => {
630 if axis.split(old_pane, new_pane, direction) {
631 return true;
632 }
633 }
634 Member::Pane(pane) => {
635 if pane == old_pane {
636 if direction.axis() == self.axis {
637 if direction.increasing() {
638 idx += 1;
639 }
640 self.insert_pane(idx, new_pane);
641 } else {
642 *member =
643 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
644 }
645 return true;
646 }
647 }
648 }
649 }
650 false
651 }
652
653 fn insert_pane(&mut self, idx: usize, new_pane: &Entity<Pane>) {
654 self.members.insert(idx, Member::Pane(new_pane.clone()));
655 *self.flexes.lock() = vec![1.; self.members.len()];
656 }
657
658 fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity<Pane>> {
659 if self.axis != direction.axis() {
660 return None;
661 }
662 let member = if direction.increasing() {
663 self.members.last()
664 } else {
665 self.members.first()
666 };
667 member.and_then(|e| match e {
668 Member::Pane(pane) => Some(pane),
669 Member::Axis(_) => None,
670 })
671 }
672
673 fn remove(&mut self, pane_to_remove: &Entity<Pane>) -> Result<Option<Member>> {
674 let mut found_pane = false;
675 let mut remove_member = None;
676 for (idx, member) in self.members.iter_mut().enumerate() {
677 match member {
678 Member::Axis(axis) => {
679 if let Ok(last_pane) = axis.remove(pane_to_remove) {
680 if let Some(last_pane) = last_pane {
681 *member = last_pane;
682 }
683 found_pane = true;
684 break;
685 }
686 }
687 Member::Pane(pane) => {
688 if pane == pane_to_remove {
689 found_pane = true;
690 remove_member = Some(idx);
691 break;
692 }
693 }
694 }
695 }
696
697 if found_pane {
698 if let Some(idx) = remove_member {
699 self.members.remove(idx);
700 *self.flexes.lock() = vec![1.; self.members.len()];
701 }
702
703 if self.members.len() == 1 {
704 let result = self.members.pop();
705 *self.flexes.lock() = vec![1.; self.members.len()];
706 Ok(result)
707 } else {
708 Ok(None)
709 }
710 } else {
711 anyhow::bail!("Pane not found");
712 }
713 }
714
715 fn reset_pane_sizes(&self) {
716 *self.flexes.lock() = vec![1.; self.members.len()];
717 for member in self.members.iter() {
718 if let Member::Axis(axis) = member {
719 axis.reset_pane_sizes();
720 }
721 }
722 }
723
724 fn resize(
725 &mut self,
726 pane: &Entity<Pane>,
727 axis: Axis,
728 amount: Pixels,
729 bounds: &Bounds<Pixels>,
730 ) -> Option<bool> {
731 let container_size = self
732 .bounding_boxes
733 .lock()
734 .iter()
735 .filter_map(|e| *e)
736 .reduce(|acc, e| acc.union(&e))
737 .unwrap_or(*bounds)
738 .size;
739
740 let found_pane = self
741 .members
742 .iter()
743 .any(|member| matches!(member, Member::Pane(p) if p == pane));
744
745 if found_pane && self.axis != axis {
746 return Some(false); // pane found but this is not the correct axis direction
747 }
748 let mut found_axis_index: Option<usize> = None;
749 if !found_pane {
750 for (i, pa) in self.members.iter_mut().enumerate() {
751 if let Member::Axis(pa) = pa
752 && let Some(done) = pa.resize(pane, axis, amount, bounds)
753 {
754 if done {
755 return Some(true); // pane found and operations already done
756 } else if self.axis != axis {
757 return Some(false); // pane found but this is not the correct axis direction
758 } else {
759 found_axis_index = Some(i); // pane found and this is correct direction
760 }
761 }
762 }
763 found_axis_index?; // no pane found
764 }
765
766 let min_size = match axis {
767 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
768 Axis::Vertical => px(VERTICAL_MIN_SIZE),
769 };
770 let mut flexes = self.flexes.lock();
771
772 let ix = if found_pane {
773 self.members.iter().position(|m| {
774 if let Member::Pane(p) = m {
775 p == pane
776 } else {
777 false
778 }
779 })
780 } else {
781 found_axis_index
782 };
783
784 if ix.is_none() {
785 return Some(true);
786 }
787
788 let ix = ix.unwrap_or(0);
789
790 let size = move |ix, flexes: &[f32]| {
791 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
792 };
793
794 // Don't allow resizing to less than the minimum size, if elements are already too small
795 if min_size - px(1.) > size(ix, flexes.as_slice()) {
796 return Some(true);
797 }
798
799 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
800 let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis);
801 let current_target_flex = flexes[target_ix] + flex_change;
802 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
803 (current_target_flex, next_target_flex)
804 };
805
806 let apply_changes =
807 |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| {
808 let next_target_size = Pixels::max(
809 size(current_ix + 1, flexes) - proposed_current_pixel_change,
810 min_size,
811 );
812 let current_target_size = Pixels::max(
813 size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size,
814 min_size,
815 );
816
817 let current_pixel_change = current_target_size - size(current_ix, flexes);
818
819 let (current_target_flex, next_target_flex) =
820 flex_changes(current_pixel_change, current_ix, 1, flexes);
821
822 flexes[current_ix] = current_target_flex;
823 flexes[current_ix + 1] = next_target_flex;
824 };
825
826 if ix + 1 == flexes.len() {
827 apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice());
828 } else {
829 apply_changes(ix, amount, flexes.as_mut_slice());
830 }
831 Some(true)
832 }
833
834 fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>) {
835 for member in self.members.iter_mut() {
836 match member {
837 Member::Axis(axis) => axis.swap(from, to),
838 Member::Pane(pane) => {
839 if pane == from {
840 *member = Member::Pane(to.clone());
841 } else if pane == to {
842 *member = Member::Pane(from.clone())
843 }
844 }
845 }
846 }
847 }
848
849 fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
850 debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
851
852 for (idx, member) in self.members.iter().enumerate() {
853 match member {
854 Member::Pane(found) => {
855 if pane == found {
856 return self.bounding_boxes.lock()[idx];
857 }
858 }
859 Member::Axis(axis) => {
860 if let Some(rect) = axis.bounding_box_for_pane(pane) {
861 return Some(rect);
862 }
863 }
864 }
865 }
866 None
867 }
868
869 fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
870 debug_assert!(self.members.len() == self.bounding_boxes.lock().len());
871
872 let bounding_boxes = self.bounding_boxes.lock();
873
874 for (idx, member) in self.members.iter().enumerate() {
875 if let Some(coordinates) = bounding_boxes[idx]
876 && coordinates.contains(&coordinate)
877 {
878 return match member {
879 Member::Pane(found) => Some(found),
880 Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
881 };
882 }
883 }
884 None
885 }
886
887 fn render(
888 &self,
889 basis: usize,
890 zoomed: Option<&AnyWeakView>,
891 render_cx: &dyn PaneLeaderDecorator,
892 window: &mut Window,
893 cx: &mut App,
894 ) -> PaneRenderResult {
895 debug_assert!(self.members.len() == self.flexes.lock().len());
896 let mut active_pane_ix = None;
897 let mut contains_active_pane = false;
898 let mut is_leaf_pane = vec![false; self.members.len()];
899
900 let rendered_children = self
901 .members
902 .iter()
903 .enumerate()
904 .map(|(ix, member)| {
905 match member {
906 Member::Pane(pane) => {
907 is_leaf_pane[ix] = true;
908 if pane == render_cx.active_pane() {
909 active_pane_ix = Some(ix);
910 contains_active_pane = true;
911 }
912 }
913 Member::Axis(_) => {
914 is_leaf_pane[ix] = false;
915 }
916 }
917
918 let result = member.render((basis + ix) * 10, zoomed, render_cx, window, cx);
919 if result.contains_active_pane {
920 contains_active_pane = true;
921 }
922 result.element.into_any_element()
923 })
924 .collect::<Vec<_>>();
925
926 let element = pane_axis(
927 self.axis,
928 basis,
929 self.flexes.clone(),
930 self.bounding_boxes.clone(),
931 render_cx.workspace().clone(),
932 )
933 .with_is_leaf_pane_mask(is_leaf_pane)
934 .children(rendered_children)
935 .with_active_pane(active_pane_ix)
936 .into_any_element();
937
938 PaneRenderResult {
939 element,
940 contains_active_pane,
941 }
942 }
943}
944
945#[derive(Clone, Copy, Debug, Deserialize, PartialEq, JsonSchema)]
946#[serde(rename_all = "snake_case")]
947pub enum SplitDirection {
948 Up,
949 Down,
950 Left,
951 Right,
952}
953
954impl std::fmt::Display for SplitDirection {
955 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
956 match self {
957 SplitDirection::Up => write!(f, "up"),
958 SplitDirection::Down => write!(f, "down"),
959 SplitDirection::Left => write!(f, "left"),
960 SplitDirection::Right => write!(f, "right"),
961 }
962 }
963}
964
965impl SplitDirection {
966 pub fn all() -> [Self; 4] {
967 [Self::Up, Self::Down, Self::Left, Self::Right]
968 }
969
970 pub fn vertical(cx: &mut App) -> Self {
971 match WorkspaceSettings::get_global(cx).pane_split_direction_vertical {
972 PaneSplitDirectionVertical::Left => SplitDirection::Left,
973 PaneSplitDirectionVertical::Right => SplitDirection::Right,
974 }
975 }
976
977 pub fn horizontal(cx: &mut App) -> Self {
978 match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal {
979 PaneSplitDirectionHorizontal::Down => SplitDirection::Down,
980 PaneSplitDirectionHorizontal::Up => SplitDirection::Up,
981 }
982 }
983
984 pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
985 match self {
986 Self::Up => rect.origin.y,
987 Self::Down => rect.bottom_left().y,
988 Self::Left => rect.bottom_left().x,
989 Self::Right => rect.bottom_right().x,
990 }
991 }
992
993 pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
994 match self {
995 Self::Up => Bounds {
996 origin: bounds.origin,
997 size: size(bounds.size.width, length),
998 },
999 Self::Down => Bounds {
1000 origin: point(bounds.bottom_left().x, bounds.bottom_left().y - length),
1001 size: size(bounds.size.width, length),
1002 },
1003 Self::Left => Bounds {
1004 origin: bounds.origin,
1005 size: size(length, bounds.size.height),
1006 },
1007 Self::Right => Bounds {
1008 origin: point(bounds.bottom_right().x - length, bounds.bottom_left().y),
1009 size: size(length, bounds.size.height),
1010 },
1011 }
1012 }
1013
1014 pub fn axis(&self) -> Axis {
1015 match self {
1016 Self::Up | Self::Down => Axis::Vertical,
1017 Self::Left | Self::Right => Axis::Horizontal,
1018 }
1019 }
1020
1021 pub fn increasing(&self) -> bool {
1022 match self {
1023 Self::Left | Self::Up => false,
1024 Self::Down | Self::Right => true,
1025 }
1026 }
1027
1028 pub fn opposite(&self) -> SplitDirection {
1029 match self {
1030 Self::Down => Self::Up,
1031 Self::Up => Self::Down,
1032 Self::Left => Self::Right,
1033 Self::Right => Self::Left,
1034 }
1035 }
1036}
1037
1038mod element {
1039 use std::mem;
1040 use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
1041
1042 use gpui::{
1043 Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId,
1044 HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
1045 Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size,
1046 };
1047 use gpui::{CursorStyle, Hitbox};
1048 use parking_lot::Mutex;
1049 use settings::Settings;
1050 use smallvec::SmallVec;
1051 use ui::prelude::*;
1052 use util::ResultExt;
1053
1054 use crate::Workspace;
1055
1056 use crate::WorkspaceSettings;
1057
1058 use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
1059
1060 const DIVIDER_SIZE: f32 = 1.0;
1061
1062 pub(super) fn pane_axis(
1063 axis: Axis,
1064 basis: usize,
1065 flexes: Arc<Mutex<Vec<f32>>>,
1066 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1067 workspace: WeakEntity<Workspace>,
1068 ) -> PaneAxisElement {
1069 PaneAxisElement {
1070 axis,
1071 basis,
1072 flexes,
1073 bounding_boxes,
1074 children: SmallVec::new(),
1075 active_pane_ix: None,
1076 workspace,
1077 is_leaf_pane_mask: Vec::new(),
1078 }
1079 }
1080
1081 pub struct PaneAxisElement {
1082 axis: Axis,
1083 basis: usize,
1084 /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages)
1085 /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%"
1086 flexes: Arc<Mutex<Vec<f32>>>,
1087 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1088 children: SmallVec<[AnyElement; 2]>,
1089 active_pane_ix: Option<usize>,
1090 workspace: WeakEntity<Workspace>,
1091 // Track which children are leaf panes (Member::Pane) vs axes (Member::Axis)
1092 is_leaf_pane_mask: Vec<bool>,
1093 }
1094
1095 pub struct PaneAxisLayout {
1096 dragged_handle: Rc<RefCell<Option<usize>>>,
1097 children: Vec<PaneAxisChildLayout>,
1098 }
1099
1100 struct PaneAxisChildLayout {
1101 bounds: Bounds<Pixels>,
1102 element: AnyElement,
1103 handle: Option<PaneAxisHandleLayout>,
1104 is_leaf_pane: bool,
1105 }
1106
1107 struct PaneAxisHandleLayout {
1108 hitbox: Hitbox,
1109 divider_bounds: Bounds<Pixels>,
1110 }
1111
1112 impl PaneAxisElement {
1113 pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
1114 self.active_pane_ix = active_pane_ix;
1115 self
1116 }
1117
1118 pub fn with_is_leaf_pane_mask(mut self, mask: Vec<bool>) -> Self {
1119 self.is_leaf_pane_mask = mask;
1120 self
1121 }
1122
1123 fn compute_resize(
1124 flexes: &Arc<Mutex<Vec<f32>>>,
1125 e: &MouseMoveEvent,
1126 ix: usize,
1127 axis: Axis,
1128 child_start: Point<Pixels>,
1129 container_size: Size<Pixels>,
1130 workspace: WeakEntity<Workspace>,
1131 window: &mut Window,
1132 cx: &mut App,
1133 ) {
1134 let min_size = match axis {
1135 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
1136 Axis::Vertical => px(VERTICAL_MIN_SIZE),
1137 };
1138 let mut flexes = flexes.lock();
1139 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1140
1141 // Math to convert a flex value to a pixel value
1142 let size = move |ix, flexes: &[f32]| {
1143 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
1144 };
1145
1146 // Don't allow resizing to less than the minimum size, if elements are already too small
1147 if min_size - px(1.) > size(ix, flexes.as_slice()) {
1148 return;
1149 }
1150
1151 // This is basically a "bucket" of pixel changes that need to be applied in response to this
1152 // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels
1153 let mut proposed_current_pixel_change =
1154 (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
1155
1156 // This takes a pixel change, and computes the flex changes that correspond to this pixel change
1157 // as well as the next one, for some reason
1158 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
1159 let flex_change = pixel_dx / container_size.along(axis);
1160 let current_target_flex = flexes[target_ix] + flex_change;
1161 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
1162 (current_target_flex, next_target_flex)
1163 };
1164
1165 // Generate the list of flex successors, from the current index.
1166 // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6]
1167 // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0]
1168 let mut successors = iter::from_fn({
1169 let forward = proposed_current_pixel_change > px(0.);
1170 let mut ix_offset = 0;
1171 let len = flexes.len();
1172 move || {
1173 let result = if forward {
1174 (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
1175 } else {
1176 (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
1177 };
1178
1179 ix_offset += 1;
1180
1181 result
1182 }
1183 });
1184
1185 // Now actually loop over these, and empty our bucket of pixel changes
1186 while proposed_current_pixel_change.abs() > px(0.) {
1187 let Some(current_ix) = successors.next() else {
1188 break;
1189 };
1190
1191 let next_target_size = Pixels::max(
1192 size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
1193 min_size,
1194 );
1195
1196 let current_target_size = Pixels::max(
1197 size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
1198 - next_target_size,
1199 min_size,
1200 );
1201
1202 let current_pixel_change =
1203 current_target_size - size(current_ix, flexes.as_slice());
1204
1205 let (current_target_flex, next_target_flex) =
1206 flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
1207
1208 flexes[current_ix] = current_target_flex;
1209 flexes[current_ix + 1] = next_target_flex;
1210
1211 proposed_current_pixel_change -= current_pixel_change;
1212 }
1213
1214 workspace
1215 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1216 .log_err();
1217 cx.stop_propagation();
1218 window.refresh();
1219 }
1220
1221 fn layout_handle(
1222 axis: Axis,
1223 pane_bounds: Bounds<Pixels>,
1224 window: &mut Window,
1225 _cx: &mut App,
1226 ) -> PaneAxisHandleLayout {
1227 let handle_bounds = Bounds {
1228 origin: pane_bounds.origin.apply_along(axis, |origin| {
1229 origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.)
1230 }),
1231 size: pane_bounds
1232 .size
1233 .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)),
1234 };
1235 let divider_bounds = Bounds {
1236 origin: pane_bounds
1237 .origin
1238 .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)),
1239 size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)),
1240 };
1241
1242 PaneAxisHandleLayout {
1243 hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::BlockMouse),
1244 divider_bounds,
1245 }
1246 }
1247 }
1248
1249 impl IntoElement for PaneAxisElement {
1250 type Element = Self;
1251
1252 fn into_element(self) -> Self::Element {
1253 self
1254 }
1255 }
1256
1257 impl Element for PaneAxisElement {
1258 type RequestLayoutState = ();
1259 type PrepaintState = PaneAxisLayout;
1260
1261 fn id(&self) -> Option<ElementId> {
1262 Some(self.basis.into())
1263 }
1264
1265 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1266 None
1267 }
1268
1269 fn request_layout(
1270 &mut self,
1271 _global_id: Option<&GlobalElementId>,
1272 _inspector_id: Option<&gpui::InspectorElementId>,
1273 window: &mut Window,
1274 cx: &mut App,
1275 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1276 let style = Style {
1277 flex_grow: 1.,
1278 flex_shrink: 1.,
1279 flex_basis: relative(0.).into(),
1280 size: size(relative(1.).into(), relative(1.).into()),
1281 ..Style::default()
1282 };
1283 (window.request_layout(style, None, cx), ())
1284 }
1285
1286 fn prepaint(
1287 &mut self,
1288 global_id: Option<&GlobalElementId>,
1289 _inspector_id: Option<&gpui::InspectorElementId>,
1290 bounds: Bounds<Pixels>,
1291 _state: &mut Self::RequestLayoutState,
1292 window: &mut Window,
1293 cx: &mut App,
1294 ) -> PaneAxisLayout {
1295 let dragged_handle = window.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
1296 global_id.unwrap(),
1297 |state, _cx| {
1298 let state = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
1299 (state.clone(), state)
1300 },
1301 );
1302 let flexes = self.flexes.lock().clone();
1303 let len = self.children.len();
1304 debug_assert!(flexes.len() == len);
1305 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1306
1307 let total_flex = len as f32;
1308
1309 let mut origin = bounds.origin;
1310 let space_per_flex = bounds.size.along(self.axis) / total_flex;
1311
1312 let mut bounding_boxes = self.bounding_boxes.lock();
1313 bounding_boxes.clear();
1314
1315 let mut layout = PaneAxisLayout {
1316 dragged_handle,
1317 children: Vec::new(),
1318 };
1319 for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() {
1320 let child_flex = flexes[ix];
1321
1322 let child_size = bounds
1323 .size
1324 .apply_along(self.axis, |_| space_per_flex * child_flex)
1325 .map(|d| d.round());
1326
1327 let child_bounds = Bounds {
1328 origin,
1329 size: child_size,
1330 };
1331
1332 bounding_boxes.push(Some(child_bounds));
1333 child.layout_as_root(child_size.into(), window, cx);
1334 child.prepaint_at(origin, window, cx);
1335
1336 origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
1337
1338 let is_leaf_pane = self.is_leaf_pane_mask.get(ix).copied().unwrap_or(true);
1339
1340 layout.children.push(PaneAxisChildLayout {
1341 bounds: child_bounds,
1342 element: child,
1343 handle: None,
1344 is_leaf_pane,
1345 })
1346 }
1347
1348 for (ix, child_layout) in layout.children.iter_mut().enumerate() {
1349 if ix < len - 1 {
1350 child_layout.handle = Some(Self::layout_handle(
1351 self.axis,
1352 child_layout.bounds,
1353 window,
1354 cx,
1355 ));
1356 }
1357 }
1358
1359 layout
1360 }
1361
1362 fn paint(
1363 &mut self,
1364 _id: Option<&GlobalElementId>,
1365 _inspector_id: Option<&gpui::InspectorElementId>,
1366 bounds: gpui::Bounds<ui::prelude::Pixels>,
1367 _: &mut Self::RequestLayoutState,
1368 layout: &mut Self::PrepaintState,
1369 window: &mut Window,
1370 cx: &mut App,
1371 ) {
1372 for child in &mut layout.children {
1373 child.element.paint(window, cx);
1374 }
1375
1376 let overlay_opacity = WorkspaceSettings::get(None, cx)
1377 .active_pane_modifiers
1378 .inactive_opacity
1379 .map(|val| val.0.clamp(0.0, 1.0))
1380 .and_then(|val| (val <= 1.).then_some(val));
1381
1382 let mut overlay_background = cx.theme().colors().editor_background;
1383 if let Some(opacity) = overlay_opacity {
1384 overlay_background.fade_out(opacity);
1385 }
1386
1387 let overlay_border = WorkspaceSettings::get(None, cx)
1388 .active_pane_modifiers
1389 .border_size
1390 .and_then(|val| (val >= 0.).then_some(val));
1391
1392 for (ix, child) in &mut layout.children.iter_mut().enumerate() {
1393 if overlay_opacity.is_some() || overlay_border.is_some() {
1394 // the overlay has to be painted in origin+1px with size width-1px
1395 // in order to accommodate the divider between panels
1396 let overlay_bounds = Bounds {
1397 origin: child
1398 .bounds
1399 .origin
1400 .apply_along(Axis::Horizontal, |val| val + px(1.)),
1401 size: child
1402 .bounds
1403 .size
1404 .apply_along(Axis::Horizontal, |val| val - px(1.)),
1405 };
1406
1407 if overlay_opacity.is_some()
1408 && child.is_leaf_pane
1409 && self.active_pane_ix != Some(ix)
1410 {
1411 window.paint_quad(gpui::fill(overlay_bounds, overlay_background));
1412 }
1413
1414 if let Some(border) = overlay_border
1415 && self.active_pane_ix == Some(ix)
1416 && child.is_leaf_pane
1417 {
1418 window.paint_quad(gpui::quad(
1419 overlay_bounds,
1420 0.,
1421 gpui::transparent_black(),
1422 border,
1423 cx.theme().colors().border_selected,
1424 BorderStyle::Solid,
1425 ));
1426 }
1427 }
1428
1429 if let Some(handle) = child.handle.as_mut() {
1430 let cursor_style = match self.axis {
1431 Axis::Vertical => CursorStyle::ResizeRow,
1432 Axis::Horizontal => CursorStyle::ResizeColumn,
1433 };
1434
1435 if layout
1436 .dragged_handle
1437 .borrow()
1438 .is_some_and(|dragged_ix| dragged_ix == ix)
1439 {
1440 window.set_window_cursor_style(cursor_style);
1441 } else {
1442 window.set_cursor_style(cursor_style, &handle.hitbox);
1443 }
1444
1445 window.paint_quad(gpui::fill(
1446 handle.divider_bounds,
1447 cx.theme().colors().pane_group_border,
1448 ));
1449
1450 window.on_mouse_event({
1451 let dragged_handle = layout.dragged_handle.clone();
1452 let flexes = self.flexes.clone();
1453 let workspace = self.workspace.clone();
1454 let handle_hitbox = handle.hitbox.clone();
1455 move |e: &MouseDownEvent, phase, window, cx| {
1456 if phase.bubble() && handle_hitbox.is_hovered(window) {
1457 dragged_handle.replace(Some(ix));
1458 if e.click_count >= 2 {
1459 let mut borrow = flexes.lock();
1460 *borrow = vec![1.; borrow.len()];
1461 workspace
1462 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1463 .log_err();
1464
1465 window.refresh();
1466 }
1467 cx.stop_propagation();
1468 }
1469 }
1470 });
1471 window.on_mouse_event({
1472 let workspace = self.workspace.clone();
1473 let dragged_handle = layout.dragged_handle.clone();
1474 let flexes = self.flexes.clone();
1475 let child_bounds = child.bounds;
1476 let axis = self.axis;
1477 move |e: &MouseMoveEvent, phase, window, cx| {
1478 let dragged_handle = dragged_handle.borrow();
1479 if phase.bubble() && *dragged_handle == Some(ix) {
1480 Self::compute_resize(
1481 &flexes,
1482 e,
1483 ix,
1484 axis,
1485 child_bounds.origin,
1486 bounds.size,
1487 workspace.clone(),
1488 window,
1489 cx,
1490 )
1491 }
1492 }
1493 });
1494 }
1495 }
1496
1497 window.on_mouse_event({
1498 let dragged_handle = layout.dragged_handle.clone();
1499 move |_: &MouseUpEvent, phase, _window, _cx| {
1500 if phase.bubble() {
1501 dragged_handle.replace(None);
1502 }
1503 }
1504 });
1505 }
1506 }
1507
1508 impl ParentElement for PaneAxisElement {
1509 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1510 self.children.extend(elements)
1511 }
1512 }
1513
1514 fn flex_values_in_bounds(flexes: &[f32]) -> bool {
1515 (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
1516 }
1517}