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