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