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