pane_group.rs

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