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