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