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