pane_group.rs

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