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