pane_group.rs

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