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