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