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