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)]
728pub enum SplitDirection {
729 Up,
730 Down,
731 Left,
732 Right,
733}
734
735impl std::fmt::Display for SplitDirection {
736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737 match self {
738 SplitDirection::Up => write!(f, "up"),
739 SplitDirection::Down => write!(f, "down"),
740 SplitDirection::Left => write!(f, "left"),
741 SplitDirection::Right => write!(f, "right"),
742 }
743 }
744}
745
746impl SplitDirection {
747 pub fn all() -> [Self; 4] {
748 [Self::Up, Self::Down, Self::Left, Self::Right]
749 }
750
751 pub fn vertical(cx: &mut App) -> Self {
752 match WorkspaceSettings::get_global(cx).pane_split_direction_vertical {
753 PaneSplitDirectionVertical::Left => SplitDirection::Left,
754 PaneSplitDirectionVertical::Right => SplitDirection::Right,
755 }
756 }
757
758 pub fn horizontal(cx: &mut App) -> Self {
759 match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal {
760 PaneSplitDirectionHorizontal::Down => SplitDirection::Down,
761 PaneSplitDirectionHorizontal::Up => SplitDirection::Up,
762 }
763 }
764
765 pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
766 match self {
767 Self::Up => rect.origin.y,
768 Self::Down => rect.bottom_left().y,
769 Self::Left => rect.bottom_left().x,
770 Self::Right => rect.bottom_right().x,
771 }
772 }
773
774 pub fn along_edge(&self, bounds: Bounds<Pixels>, length: Pixels) -> Bounds<Pixels> {
775 match self {
776 Self::Up => Bounds {
777 origin: bounds.origin,
778 size: size(bounds.size.width, length),
779 },
780 Self::Down => Bounds {
781 origin: point(bounds.bottom_left().x, bounds.bottom_left().y - length),
782 size: size(bounds.size.width, length),
783 },
784 Self::Left => Bounds {
785 origin: bounds.origin,
786 size: size(length, bounds.size.height),
787 },
788 Self::Right => Bounds {
789 origin: point(bounds.bottom_right().x - length, bounds.bottom_left().y),
790 size: size(length, bounds.size.height),
791 },
792 }
793 }
794
795 pub fn axis(&self) -> Axis {
796 match self {
797 Self::Up | Self::Down => Axis::Vertical,
798 Self::Left | Self::Right => Axis::Horizontal,
799 }
800 }
801
802 pub fn increasing(&self) -> bool {
803 match self {
804 Self::Left | Self::Up => false,
805 Self::Down | Self::Right => true,
806 }
807 }
808}
809
810#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq)]
811pub enum ResizeIntent {
812 Lengthen,
813 Shorten,
814 Widen,
815 Narrow,
816}
817
818mod element {
819 use std::mem;
820 use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
821
822 use gpui::{
823 px, relative, size, Along, AnyElement, App, Axis, Bounds, Element, GlobalElementId,
824 IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
825 Size, Style, WeakEntity, Window,
826 };
827 use gpui::{CursorStyle, Hitbox};
828 use parking_lot::Mutex;
829 use settings::Settings;
830 use smallvec::SmallVec;
831 use ui::prelude::*;
832 use util::ResultExt;
833
834 use crate::Workspace;
835
836 use crate::WorkspaceSettings;
837
838 use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE};
839
840 const DIVIDER_SIZE: f32 = 1.0;
841
842 pub(super) fn pane_axis(
843 axis: Axis,
844 basis: usize,
845 flexes: Arc<Mutex<Vec<f32>>>,
846 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
847 workspace: WeakEntity<Workspace>,
848 ) -> PaneAxisElement {
849 PaneAxisElement {
850 axis,
851 basis,
852 flexes,
853 bounding_boxes,
854 children: SmallVec::new(),
855 active_pane_ix: None,
856 workspace,
857 }
858 }
859
860 pub struct PaneAxisElement {
861 axis: Axis,
862 basis: usize,
863 flexes: Arc<Mutex<Vec<f32>>>,
864 bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
865 children: SmallVec<[AnyElement; 2]>,
866 active_pane_ix: Option<usize>,
867 workspace: WeakEntity<Workspace>,
868 }
869
870 pub struct PaneAxisLayout {
871 dragged_handle: Rc<RefCell<Option<usize>>>,
872 children: Vec<PaneAxisChildLayout>,
873 }
874
875 struct PaneAxisChildLayout {
876 bounds: Bounds<Pixels>,
877 element: AnyElement,
878 handle: Option<PaneAxisHandleLayout>,
879 }
880
881 struct PaneAxisHandleLayout {
882 hitbox: Hitbox,
883 divider_bounds: Bounds<Pixels>,
884 }
885
886 impl PaneAxisElement {
887 pub fn with_active_pane(mut self, active_pane_ix: Option<usize>) -> Self {
888 self.active_pane_ix = active_pane_ix;
889 self
890 }
891
892 #[allow(clippy::too_many_arguments)]
893 fn compute_resize(
894 flexes: &Arc<Mutex<Vec<f32>>>,
895 e: &MouseMoveEvent,
896 ix: usize,
897 axis: Axis,
898 child_start: Point<Pixels>,
899 container_size: Size<Pixels>,
900 workspace: WeakEntity<Workspace>,
901 window: &mut Window,
902 cx: &mut App,
903 ) {
904 let min_size = match axis {
905 Axis::Horizontal => px(HORIZONTAL_MIN_SIZE),
906 Axis::Vertical => px(VERTICAL_MIN_SIZE),
907 };
908 let mut flexes = flexes.lock();
909 debug_assert!(flex_values_in_bounds(flexes.as_slice()));
910
911 let size = move |ix, flexes: &[f32]| {
912 container_size.along(axis) * (flexes[ix] / flexes.len() as f32)
913 };
914
915 // Don't allow resizing to less than the minimum size, if elements are already too small
916 if min_size - px(1.) > size(ix, flexes.as_slice()) {
917 return;
918 }
919
920 let mut proposed_current_pixel_change =
921 (e.position - child_start).along(axis) - size(ix, flexes.as_slice());
922
923 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
924 let flex_change = pixel_dx / container_size.along(axis);
925 let current_target_flex = flexes[target_ix] + flex_change;
926 let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change;
927 (current_target_flex, next_target_flex)
928 };
929
930 let mut successors = iter::from_fn({
931 let forward = proposed_current_pixel_change > px(0.);
932 let mut ix_offset = 0;
933 let len = flexes.len();
934 move || {
935 let result = if forward {
936 (ix + 1 + ix_offset < len).then(|| ix + ix_offset)
937 } else {
938 (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset)
939 };
940
941 ix_offset += 1;
942
943 result
944 }
945 });
946
947 while proposed_current_pixel_change.abs() > px(0.) {
948 let Some(current_ix) = successors.next() else {
949 break;
950 };
951
952 let next_target_size = Pixels::max(
953 size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
954 min_size,
955 );
956
957 let current_target_size = Pixels::max(
958 size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice())
959 - next_target_size,
960 min_size,
961 );
962
963 let current_pixel_change =
964 current_target_size - size(current_ix, flexes.as_slice());
965
966 let (current_target_flex, next_target_flex) =
967 flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
968
969 flexes[current_ix] = current_target_flex;
970 flexes[current_ix + 1] = next_target_flex;
971
972 proposed_current_pixel_change -= current_pixel_change;
973 }
974
975 workspace
976 .update(cx, |this, cx| this.serialize_workspace(window, cx))
977 .log_err();
978 cx.stop_propagation();
979 window.refresh();
980 }
981
982 #[allow(clippy::too_many_arguments)]
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 ));
1188 }
1189 }
1190 }
1191
1192 if let Some(handle) = child.handle.as_mut() {
1193 let cursor_style = match self.axis {
1194 Axis::Vertical => CursorStyle::ResizeRow,
1195 Axis::Horizontal => CursorStyle::ResizeColumn,
1196 };
1197 window.set_cursor_style(cursor_style, &handle.hitbox);
1198 window.paint_quad(gpui::fill(
1199 handle.divider_bounds,
1200 cx.theme().colors().pane_group_border,
1201 ));
1202
1203 window.on_mouse_event({
1204 let dragged_handle = layout.dragged_handle.clone();
1205 let flexes = self.flexes.clone();
1206 let workspace = self.workspace.clone();
1207 let handle_hitbox = handle.hitbox.clone();
1208 move |e: &MouseDownEvent, phase, window, cx| {
1209 if phase.bubble() && handle_hitbox.is_hovered(window) {
1210 dragged_handle.replace(Some(ix));
1211 if e.click_count >= 2 {
1212 let mut borrow = flexes.lock();
1213 *borrow = vec![1.; borrow.len()];
1214 workspace
1215 .update(cx, |this, cx| this.serialize_workspace(window, cx))
1216 .log_err();
1217
1218 window.refresh();
1219 }
1220 cx.stop_propagation();
1221 }
1222 }
1223 });
1224 window.on_mouse_event({
1225 let workspace = self.workspace.clone();
1226 let dragged_handle = layout.dragged_handle.clone();
1227 let flexes = self.flexes.clone();
1228 let child_bounds = child.bounds;
1229 let axis = self.axis;
1230 move |e: &MouseMoveEvent, phase, window, cx| {
1231 let dragged_handle = dragged_handle.borrow();
1232 if phase.bubble() && *dragged_handle == Some(ix) {
1233 Self::compute_resize(
1234 &flexes,
1235 e,
1236 ix,
1237 axis,
1238 child_bounds.origin,
1239 bounds.size,
1240 workspace.clone(),
1241 window,
1242 cx,
1243 )
1244 }
1245 }
1246 });
1247 }
1248 }
1249
1250 window.on_mouse_event({
1251 let dragged_handle = layout.dragged_handle.clone();
1252 move |_: &MouseUpEvent, phase, _window, _cx| {
1253 if phase.bubble() {
1254 dragged_handle.replace(None);
1255 }
1256 }
1257 });
1258 }
1259 }
1260
1261 impl ParentElement for PaneAxisElement {
1262 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1263 self.children.extend(elements)
1264 }
1265 }
1266
1267 fn flex_values_in_bounds(flexes: &[f32]) -> bool {
1268 (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
1269 }
1270}