pane_group.rs

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