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