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 pub fn opposite(&self) -> SplitDirection {
968 match self {
969 Self::Down => Self::Up,
970 Self::Up => Self::Down,
971 Self::Left => Self::Right,
972 Self::Right => Self::Left,
973 }
974 }
975}
976
977mod element {
978 use std::mem;
979 use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
980
981 use gpui::{
982 Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId,
983 HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
984 Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size,
985 };
986 use gpui::{CursorStyle, Hitbox};
987 use parking_lot::Mutex;
988 use settings::Settings;
989 use smallvec::SmallVec;
990 use ui::prelude::*;
991 use util::ResultExt;
992
993 use crate::Workspace;
994
995 use crate::WorkspaceSettings;
996
997 use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
998
999 const DIVIDER_SIZE: f32 = 1.0;
1000
1001 pub(super) fn pane_axis(
1002 axis: Axis,
1003 basis: usize,
1004 flexes: Arc<Mutex<Vec<f32>>>,
1005 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1006 workspace: WeakEntity<Workspace>,
1007 ) -> PaneAxisElement {
1008 PaneAxisElement {
1009 axis,
1010 basis,
1011 flexes,
1012 bounding_boxes,
1013 children: SmallVec::new(),
1014 active_pane_ix: None,
1015 workspace,
1016 is_leaf_pane_mask: Vec::new(),
1017 }
1018 }
1019
1020 pub struct PaneAxisElement {
1021 axis: Axis,
1022 basis: usize,
1023 /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages)
1024 /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%"
1025 flexes: Arc<Mutex<Vec<f32>>>,
1026 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
1027 children: SmallVec<[AnyElement; 2]>,
1028 active_pane_ix: Option<usize>,
1029 workspace: WeakEntity<Workspace>,
1030 // Track which children are leaf panes (Member::Pane) vs axes (Member::Axis)
1031 is_leaf_pane_mask: Vec<bool>,
1032 }
1033
1034 pub struct PaneAxisLayout {
1035 dragged_handle: Rc<RefCell<Option<usize>>>,
1036 children: Vec<PaneAxisChildLayout>,
1037 }
1038
1039 struct PaneAxisChildLayout {
1040 bounds: Bounds<Pixels>,
1041 element: AnyElement,
1042 handle: Option<PaneAxisHandleLayout>,
1043 is_leaf_pane: bool,
1044 }
1045
1046 struct PaneAxisHandleLayout {
1047 hitbox: Hitbox,
1048 divider_bounds: Bounds<Pixels>,
1049 }
1050
1051 impl PaneAxisElement {
1052 pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
1053 self.active_pane_ix = active_pane_ix;
1054 self
1055 }
1056
1057 pub fn with_is_leaf_pane_mask(mut self, mask: Vec<bool>) -> Self {
1058 self.is_leaf_pane_mask = mask;
1059 self
1060 }
1061
1062 fn compute_resize(
1063 flexes: &Arc<Mutex<Vec<f32>>>,
1064 e: &MouseMoveEvent,
1065 ix: usize,
1066 axis: Axis,
1067 child_start: Point<Pixels>,
1068 container_size: Size<Pixels>,
1069 workspace: WeakEntity<Workspace>,
1070 window: &mut Window,
1071 cx: &mut App,
1072 ) {
1073 let min_size = match axis {
1074 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
1075 Axis::Vertical => px(VERTICAL_MIN_SIZE),
1076 };
1077 let mut flexes = flexes.lock();
1078 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1079
1080 // Math to convert a flex value to a pixel value
1081 let size = move |ix, flexes: &[f32]| {
1082 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
1083 };
1084
1085 // Don't allow resizing to less than the minimum size, if elements are already too small
1086 if min_size - px(1.) > size(ix, flexes.as_slice()) {
1087 return;
1088 }
1089
1090 // This is basically a "bucket" of pixel changes that need to be applied in response to this
1091 // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels
1092 let mut proposed_current_pixel_change =
1093 (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
1094
1095 // This takes a pixel change, and computes the flex changes that correspond to this pixel change
1096 // as well as the next one, for some reason
1097 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
1098 let flex_change = pixel_dx / container_size.along(axis);
1099 let current_target_flex = flexes[target_ix] + flex_change;
1100 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
1101 (current_target_flex, next_target_flex)
1102 };
1103
1104 // Generate the list of flex successors, from the current index.
1105 // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6]
1106 // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0]
1107 let mut successors = iter::from_fn({
1108 let forward = proposed_current_pixel_change > px(0.);
1109 let mut ix_offset = 0;
1110 let len = flexes.len();
1111 move || {
1112 let result = if forward {
1113 (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
1114 } else {
1115 (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
1116 };
1117
1118 ix_offset += 1;
1119
1120 result
1121 }
1122 });
1123
1124 // Now actually loop over these, and empty our bucket of pixel changes
1125 while proposed_current_pixel_change.abs() > px(0.) {
1126 let Some(current_ix) = successors.next() else {
1127 break;
1128 };
1129
1130 let next_target_size = Pixels::max(
1131 size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
1132 min_size,
1133 );
1134
1135 let current_target_size = Pixels::max(
1136 size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
1137 - next_target_size,
1138 min_size,
1139 );
1140
1141 let current_pixel_change =
1142 current_target_size - size(current_ix, flexes.as_slice());
1143
1144 let (current_target_flex, next_target_flex) =
1145 flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
1146
1147 flexes[current_ix] = current_target_flex;
1148 flexes[current_ix + 1] = next_target_flex;
1149
1150 proposed_current_pixel_change -= current_pixel_change;
1151 }
1152
1153 workspace
1154 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1155 .log_err();
1156 cx.stop_propagation();
1157 window.refresh();
1158 }
1159
1160 fn layout_handle(
1161 axis: Axis,
1162 pane_bounds: Bounds<Pixels>,
1163 window: &mut Window,
1164 _cx: &mut App,
1165 ) -> PaneAxisHandleLayout {
1166 let handle_bounds = Bounds {
1167 origin: pane_bounds.origin.apply_along(axis, |origin| {
1168 origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.)
1169 }),
1170 size: pane_bounds
1171 .size
1172 .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)),
1173 };
1174 let divider_bounds = Bounds {
1175 origin: pane_bounds
1176 .origin
1177 .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)),
1178 size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)),
1179 };
1180
1181 PaneAxisHandleLayout {
1182 hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::BlockMouse),
1183 divider_bounds,
1184 }
1185 }
1186 }
1187
1188 impl IntoElement for PaneAxisElement {
1189 type Element = Self;
1190
1191 fn into_element(self) -> Self::Element {
1192 self
1193 }
1194 }
1195
1196 impl Element for PaneAxisElement {
1197 type RequestLayoutState = ();
1198 type PrepaintState = PaneAxisLayout;
1199
1200 fn id(&self) -> Option<ElementId> {
1201 Some(self.basis.into())
1202 }
1203
1204 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1205 None
1206 }
1207
1208 fn request_layout(
1209 &mut self,
1210 _global_id: Option<&GlobalElementId>,
1211 _inspector_id: Option<&gpui::InspectorElementId>,
1212 window: &mut Window,
1213 cx: &mut App,
1214 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1215 let style = Style {
1216 flex_grow: 1.,
1217 flex_shrink: 1.,
1218 flex_basis: relative(0.).into(),
1219 size: size(relative(1.).into(), relative(1.).into()),
1220 ..Style::default()
1221 };
1222 (window.request_layout(style, None, cx), ())
1223 }
1224
1225 fn prepaint(
1226 &mut self,
1227 global_id: Option<&GlobalElementId>,
1228 _inspector_id: Option<&gpui::InspectorElementId>,
1229 bounds: Bounds<Pixels>,
1230 _state: &mut Self::RequestLayoutState,
1231 window: &mut Window,
1232 cx: &mut App,
1233 ) -> PaneAxisLayout {
1234 let dragged_handle = window.with_element_state::<Rc<RefCell<Option<usize>>>, _>(
1235 global_id.unwrap(),
1236 |state, _cx| {
1237 let state = state.unwrap_or_else(|| Rc::new(RefCell::new(None)));
1238 (state.clone(), state)
1239 },
1240 );
1241 let flexes = self.flexes.lock().clone();
1242 let len = self.children.len();
1243 debug_assert!(flexes.len() == len);
1244 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
1245
1246 let total_flex = len as f32;
1247
1248 let mut origin = bounds.origin;
1249 let space_per_flex = bounds.size.along(self.axis) / total_flex;
1250
1251 let mut bounding_boxes = self.bounding_boxes.lock();
1252 bounding_boxes.clear();
1253
1254 let mut layout = PaneAxisLayout {
1255 dragged_handle,
1256 children: Vec::new(),
1257 };
1258 for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() {
1259 let child_flex = flexes[ix];
1260
1261 let child_size = bounds
1262 .size
1263 .apply_along(self.axis, |_| space_per_flex * child_flex)
1264 .map(|d| d.round());
1265
1266 let child_bounds = Bounds {
1267 origin,
1268 size: child_size,
1269 };
1270
1271 bounding_boxes.push(Some(child_bounds));
1272 child.layout_as_root(child_size.into(), window, cx);
1273 child.prepaint_at(origin, window, cx);
1274
1275 origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis));
1276
1277 let is_leaf_pane = self.is_leaf_pane_mask.get(ix).copied().unwrap_or(true);
1278
1279 layout.children.push(PaneAxisChildLayout {
1280 bounds: child_bounds,
1281 element: child,
1282 handle: None,
1283 is_leaf_pane,
1284 })
1285 }
1286
1287 for (ix, child_layout) in layout.children.iter_mut().enumerate() {
1288 if ix < len - 1 {
1289 child_layout.handle = Some(Self::layout_handle(
1290 self.axis,
1291 child_layout.bounds,
1292 window,
1293 cx,
1294 ));
1295 }
1296 }
1297
1298 layout
1299 }
1300
1301 fn paint(
1302 &mut self,
1303 _id: Option<&GlobalElementId>,
1304 _inspector_id: Option<&gpui::InspectorElementId>,
1305 bounds: gpui::Bounds<ui::prelude::Pixels>,
1306 _: &mut Self::RequestLayoutState,
1307 layout: &mut Self::PrepaintState,
1308 window: &mut Window,
1309 cx: &mut App,
1310 ) {
1311 for child in &mut layout.children {
1312 child.element.paint(window, cx);
1313 }
1314
1315 let overlay_opacity = WorkspaceSettings::get(None, cx)
1316 .active_pane_modifiers
1317 .inactive_opacity
1318 .map(|val| val.0.clamp(0.0, 1.0))
1319 .and_then(|val| (val <= 1.).then_some(val));
1320
1321 let mut overlay_background = cx.theme().colors().editor_background;
1322 if let Some(opacity) = overlay_opacity {
1323 overlay_background.fade_out(opacity);
1324 }
1325
1326 let overlay_border = WorkspaceSettings::get(None, cx)
1327 .active_pane_modifiers
1328 .border_size
1329 .and_then(|val| (val >= 0.).then_some(val));
1330
1331 for (ix, child) in &mut layout.children.iter_mut().enumerate() {
1332 if overlay_opacity.is_some() || overlay_border.is_some() {
1333 // the overlay has to be painted in origin+1px with size width-1px
1334 // in order to accommodate the divider between panels
1335 let overlay_bounds = Bounds {
1336 origin: child
1337 .bounds
1338 .origin
1339 .apply_along(Axis::Horizontal, |val| val + px(1.)),
1340 size: child
1341 .bounds
1342 .size
1343 .apply_along(Axis::Horizontal, |val| val - px(1.)),
1344 };
1345
1346 if overlay_opacity.is_some()
1347 && child.is_leaf_pane
1348 && self.active_pane_ix != Some(ix)
1349 {
1350 window.paint_quad(gpui::fill(overlay_bounds, overlay_background));
1351 }
1352
1353 if let Some(border) = overlay_border
1354 && self.active_pane_ix == Some(ix)
1355 && child.is_leaf_pane
1356 {
1357 window.paint_quad(gpui::quad(
1358 overlay_bounds,
1359 0.,
1360 gpui::transparent_black(),
1361 border,
1362 cx.theme().colors().border_selected,
1363 BorderStyle::Solid,
1364 ));
1365 }
1366 }
1367
1368 if let Some(handle) = child.handle.as_mut() {
1369 let cursor_style = match self.axis {
1370 Axis::Vertical => CursorStyle::ResizeRow,
1371 Axis::Horizontal => CursorStyle::ResizeColumn,
1372 };
1373
1374 if layout
1375 .dragged_handle
1376 .borrow()
1377 .is_some_and(|dragged_ix| dragged_ix == ix)
1378 {
1379 window.set_window_cursor_style(cursor_style);
1380 } else {
1381 window.set_cursor_style(cursor_style, &handle.hitbox);
1382 }
1383
1384 window.paint_quad(gpui::fill(
1385 handle.divider_bounds,
1386 cx.theme().colors().pane_group_border,
1387 ));
1388
1389 window.on_mouse_event({
1390 let dragged_handle = layout.dragged_handle.clone();
1391 let flexes = self.flexes.clone();
1392 let workspace = self.workspace.clone();
1393 let handle_hitbox = handle.hitbox.clone();
1394 move |e: &MouseDownEvent, phase, window, cx| {
1395 if phase.bubble() && handle_hitbox.is_hovered(window) {
1396 dragged_handle.replace(Some(ix));
1397 if e.click_count >= 2 {
1398 let mut borrow = flexes.lock();
1399 *borrow = vec![1.; borrow.len()];
1400 workspace
1401 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1402 .log_err();
1403
1404 window.refresh();
1405 }
1406 cx.stop_propagation();
1407 }
1408 }
1409 });
1410 window.on_mouse_event({
1411 let workspace = self.workspace.clone();
1412 let dragged_handle = layout.dragged_handle.clone();
1413 let flexes = self.flexes.clone();
1414 let child_bounds = child.bounds;
1415 let axis = self.axis;
1416 move |e: &MouseMoveEvent, phase, window, cx| {
1417 let dragged_handle = dragged_handle.borrow();
1418 if phase.bubble() && *dragged_handle == Some(ix) {
1419 Self::compute_resize(
1420 &flexes,
1421 e,
1422 ix,
1423 axis,
1424 child_bounds.origin,
1425 bounds.size,
1426 workspace.clone(),
1427 window,
1428 cx,
1429 )
1430 }
1431 }
1432 });
1433 }
1434 }
1435
1436 window.on_mouse_event({
1437 let dragged_handle = layout.dragged_handle.clone();
1438 move |_: &MouseUpEvent, phase, _window, _cx| {
1439 if phase.bubble() {
1440 dragged_handle.replace(None);
1441 }
1442 }
1443 });
1444 }
1445 }
1446
1447 impl ParentElement for PaneAxisElement {
1448 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1449 self.children.extend(elements)
1450 }
1451 }
1452
1453 fn flex_values_in_bounds(flexes: &[f32]) -> bool {
1454 (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
1455 }
1456}