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