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