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