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