pane_group.rs

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