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