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