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