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