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