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