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