pane_group.rs

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