pane.rs

   1use super::{ItemHandle, SplitDirection};
   2use crate::{
   3    dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
   4    toolbar::Toolbar,
   5    Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
   6};
   7use anyhow::Result;
   8use collections::{HashMap, HashSet, VecDeque};
   9use context_menu::{ContextMenu, ContextMenuItem};
  10use drag_and_drop::{DragAndDrop, Draggable};
  11use futures::StreamExt;
  12use gpui::{
  13    actions,
  14    color::Color,
  15    elements::*,
  16    geometry::{
  17        rect::RectF,
  18        vector::{vec2f, Vector2F},
  19    },
  20    impl_actions, impl_internal_actions,
  21    platform::{CursorStyle, NavigationDirection},
  22    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
  23    ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
  24    ViewContext, ViewHandle, WeakViewHandle,
  25};
  26use project::{Project, ProjectEntryId, ProjectPath};
  27use serde::Deserialize;
  28use settings::{Autosave, DockAnchor, Settings};
  29use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
  30use theme::Theme;
  31use util::ResultExt;
  32
  33#[derive(Clone, Deserialize, PartialEq)]
  34pub struct ActivateItem(pub usize);
  35
  36actions!(
  37    pane,
  38    [
  39        ActivatePrevItem,
  40        ActivateNextItem,
  41        ActivateLastItem,
  42        CloseActiveItem,
  43        CloseInactiveItems,
  44        ReopenClosedItem,
  45        SplitLeft,
  46        SplitUp,
  47        SplitRight,
  48        SplitDown,
  49    ]
  50);
  51
  52#[derive(Clone, PartialEq)]
  53pub struct CloseItem {
  54    pub item_id: usize,
  55    pub pane: WeakViewHandle<Pane>,
  56}
  57
  58#[derive(Clone, PartialEq)]
  59pub struct MoveItem {
  60    pub item_id: usize,
  61    pub from: WeakViewHandle<Pane>,
  62    pub to: WeakViewHandle<Pane>,
  63    pub destination_index: usize,
  64}
  65
  66#[derive(Clone, Deserialize, PartialEq)]
  67pub struct GoBack {
  68    #[serde(skip_deserializing)]
  69    pub pane: Option<WeakViewHandle<Pane>>,
  70}
  71
  72#[derive(Clone, Deserialize, PartialEq)]
  73pub struct GoForward {
  74    #[serde(skip_deserializing)]
  75    pub pane: Option<WeakViewHandle<Pane>>,
  76}
  77
  78#[derive(Clone, PartialEq)]
  79pub struct DeploySplitMenu {
  80    position: Vector2F,
  81}
  82
  83#[derive(Clone, PartialEq)]
  84pub struct DeployDockMenu {
  85    position: Vector2F,
  86}
  87
  88#[derive(Clone, PartialEq)]
  89pub struct DeployNewMenu {
  90    position: Vector2F,
  91}
  92
  93impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
  94impl_internal_actions!(
  95    pane,
  96    [
  97        CloseItem,
  98        DeploySplitMenu,
  99        DeployNewMenu,
 100        DeployDockMenu,
 101        MoveItem
 102    ]
 103);
 104
 105const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 106
 107pub fn init(cx: &mut MutableAppContext) {
 108    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
 109        pane.activate_item(action.0, true, true, cx);
 110    });
 111    cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
 112        pane.activate_item(pane.items.len() - 1, true, true, cx);
 113    });
 114    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 115        pane.activate_prev_item(cx);
 116    });
 117    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 118        pane.activate_next_item(cx);
 119    });
 120    cx.add_async_action(Pane::close_active_item);
 121    cx.add_async_action(Pane::close_inactive_items);
 122    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
 123        let pane = action.pane.upgrade(cx)?;
 124        let task = Pane::close_item(workspace, pane, action.item_id, cx);
 125        Some(cx.foreground().spawn(async move {
 126            task.await?;
 127            Ok(())
 128        }))
 129    });
 130    cx.add_action(
 131        |workspace,
 132         MoveItem {
 133             from,
 134             to,
 135             item_id,
 136             destination_index,
 137         },
 138         cx| {
 139            // Get item handle to move
 140            let from = if let Some(from) = from.upgrade(cx) {
 141                from
 142            } else {
 143                return;
 144            };
 145
 146            // Add item to new pane at given index
 147            let to = if let Some(to) = to.upgrade(cx) {
 148                to
 149            } else {
 150                return;
 151            };
 152
 153            Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
 154        },
 155    );
 156    cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
 157    cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
 158    cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
 159    cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
 160    cx.add_action(Pane::deploy_split_menu);
 161    cx.add_action(Pane::deploy_new_menu);
 162    cx.add_action(Pane::deploy_dock_menu);
 163    cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
 164        Pane::reopen_closed_item(workspace, cx).detach();
 165    });
 166    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
 167        Pane::go_back(
 168            workspace,
 169            action
 170                .pane
 171                .as_ref()
 172                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 173            cx,
 174        )
 175        .detach();
 176    });
 177    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
 178        Pane::go_forward(
 179            workspace,
 180            action
 181                .pane
 182                .as_ref()
 183                .and_then(|weak_handle| weak_handle.upgrade(cx)),
 184            cx,
 185        )
 186        .detach();
 187    });
 188}
 189
 190#[derive(Debug)]
 191pub enum Event {
 192    Focused,
 193    ActivateItem { local: bool },
 194    Remove,
 195    RemoveItem { item_id: usize },
 196    Split(SplitDirection),
 197    ChangeItemTitle,
 198}
 199
 200pub struct Pane {
 201    items: Vec<Box<dyn ItemHandle>>,
 202    is_active: bool,
 203    active_item_index: usize,
 204    last_focused_view: Option<AnyWeakViewHandle>,
 205    autoscroll: bool,
 206    nav_history: Rc<RefCell<NavHistory>>,
 207    toolbar: ViewHandle<Toolbar>,
 208    tab_bar_context_menu: ViewHandle<ContextMenu>,
 209    docked: Option<DockAnchor>,
 210}
 211
 212pub struct ItemNavHistory {
 213    history: Rc<RefCell<NavHistory>>,
 214    item: Rc<dyn WeakItemHandle>,
 215}
 216
 217struct NavHistory {
 218    mode: NavigationMode,
 219    backward_stack: VecDeque<NavigationEntry>,
 220    forward_stack: VecDeque<NavigationEntry>,
 221    closed_stack: VecDeque<NavigationEntry>,
 222    paths_by_item: HashMap<usize, ProjectPath>,
 223    pane: WeakViewHandle<Pane>,
 224}
 225
 226#[derive(Copy, Clone)]
 227enum NavigationMode {
 228    Normal,
 229    GoingBack,
 230    GoingForward,
 231    ClosingItem,
 232    ReopeningClosedItem,
 233    Disabled,
 234}
 235
 236impl Default for NavigationMode {
 237    fn default() -> Self {
 238        Self::Normal
 239    }
 240}
 241
 242pub struct NavigationEntry {
 243    pub item: Rc<dyn WeakItemHandle>,
 244    pub data: Option<Box<dyn Any>>,
 245}
 246
 247struct DraggedItem {
 248    item: Box<dyn ItemHandle>,
 249    pane: WeakViewHandle<Pane>,
 250}
 251
 252pub enum ReorderBehavior {
 253    None,
 254    MoveAfterActive,
 255    MoveToIndex(usize),
 256}
 257
 258impl Pane {
 259    pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
 260        let handle = cx.weak_handle();
 261        let context_menu = cx.add_view(ContextMenu::new);
 262        Self {
 263            items: Vec::new(),
 264            is_active: true,
 265            active_item_index: 0,
 266            last_focused_view: None,
 267            autoscroll: false,
 268            nav_history: Rc::new(RefCell::new(NavHistory {
 269                mode: NavigationMode::Normal,
 270                backward_stack: Default::default(),
 271                forward_stack: Default::default(),
 272                closed_stack: Default::default(),
 273                paths_by_item: Default::default(),
 274                pane: handle.clone(),
 275            })),
 276            toolbar: cx.add_view(|_| Toolbar::new(handle)),
 277            tab_bar_context_menu: context_menu,
 278            docked,
 279        }
 280    }
 281
 282    pub fn is_active(&self) -> bool {
 283        self.is_active
 284    }
 285
 286    pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
 287        self.is_active = is_active;
 288        cx.notify();
 289    }
 290
 291    pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
 292        self.docked = docked;
 293        cx.notify();
 294    }
 295
 296    pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
 297        ItemNavHistory {
 298            history: self.nav_history.clone(),
 299            item: Rc::new(item.downgrade()),
 300        }
 301    }
 302
 303    pub fn go_back(
 304        workspace: &mut Workspace,
 305        pane: Option<ViewHandle<Pane>>,
 306        cx: &mut ViewContext<Workspace>,
 307    ) -> Task<()> {
 308        Self::navigate_history(
 309            workspace,
 310            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 311            NavigationMode::GoingBack,
 312            cx,
 313        )
 314    }
 315
 316    pub fn go_forward(
 317        workspace: &mut Workspace,
 318        pane: Option<ViewHandle<Pane>>,
 319        cx: &mut ViewContext<Workspace>,
 320    ) -> Task<()> {
 321        Self::navigate_history(
 322            workspace,
 323            pane.unwrap_or_else(|| workspace.active_pane().clone()),
 324            NavigationMode::GoingForward,
 325            cx,
 326        )
 327    }
 328
 329    pub fn reopen_closed_item(
 330        workspace: &mut Workspace,
 331        cx: &mut ViewContext<Workspace>,
 332    ) -> Task<()> {
 333        Self::navigate_history(
 334            workspace,
 335            workspace.active_pane().clone(),
 336            NavigationMode::ReopeningClosedItem,
 337            cx,
 338        )
 339    }
 340
 341    pub fn disable_history(&mut self) {
 342        self.nav_history.borrow_mut().disable();
 343    }
 344
 345    pub fn enable_history(&mut self) {
 346        self.nav_history.borrow_mut().enable();
 347    }
 348
 349    pub fn can_navigate_backward(&self) -> bool {
 350        !self.nav_history.borrow().backward_stack.is_empty()
 351    }
 352
 353    pub fn can_navigate_forward(&self) -> bool {
 354        !self.nav_history.borrow().forward_stack.is_empty()
 355    }
 356
 357    fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
 358        self.toolbar.update(cx, |_, cx| cx.notify());
 359    }
 360
 361    fn navigate_history(
 362        workspace: &mut Workspace,
 363        pane: ViewHandle<Pane>,
 364        mode: NavigationMode,
 365        cx: &mut ViewContext<Workspace>,
 366    ) -> Task<()> {
 367        cx.focus(pane.clone());
 368
 369        let to_load = pane.update(cx, |pane, cx| {
 370            loop {
 371                // Retrieve the weak item handle from the history.
 372                let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
 373
 374                // If the item is still present in this pane, then activate it.
 375                if let Some(index) = entry
 376                    .item
 377                    .upgrade(cx)
 378                    .and_then(|v| pane.index_for_item(v.as_ref()))
 379                {
 380                    let prev_active_item_index = pane.active_item_index;
 381                    pane.nav_history.borrow_mut().set_mode(mode);
 382                    pane.activate_item(index, true, true, cx);
 383                    pane.nav_history
 384                        .borrow_mut()
 385                        .set_mode(NavigationMode::Normal);
 386
 387                    let mut navigated = prev_active_item_index != pane.active_item_index;
 388                    if let Some(data) = entry.data {
 389                        navigated |= pane.active_item()?.navigate(data, cx);
 390                    }
 391
 392                    if navigated {
 393                        break None;
 394                    }
 395                }
 396                // If the item is no longer present in this pane, then retrieve its
 397                // project path in order to reopen it.
 398                else {
 399                    break pane
 400                        .nav_history
 401                        .borrow()
 402                        .paths_by_item
 403                        .get(&entry.item.id())
 404                        .cloned()
 405                        .map(|project_path| (project_path, entry));
 406                }
 407            }
 408        });
 409
 410        if let Some((project_path, entry)) = to_load {
 411            // If the item was no longer present, then load it again from its previous path.
 412            let pane = pane.downgrade();
 413            let task = workspace.load_path(project_path, cx);
 414            cx.spawn(|workspace, mut cx| async move {
 415                let task = task.await;
 416                if let Some(pane) = pane.upgrade(&cx) {
 417                    let mut navigated = false;
 418                    if let Some((project_entry_id, build_item)) = task.log_err() {
 419                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
 420                            pane.nav_history.borrow_mut().set_mode(mode);
 421                            pane.active_item().map(|p| p.id())
 422                        });
 423
 424                        let item = workspace.update(&mut cx, |workspace, cx| {
 425                            Self::open_item(
 426                                workspace,
 427                                pane.clone(),
 428                                project_entry_id,
 429                                true,
 430                                cx,
 431                                build_item,
 432                            )
 433                        });
 434
 435                        pane.update(&mut cx, |pane, cx| {
 436                            navigated |= Some(item.id()) != prev_active_item_id;
 437                            pane.nav_history
 438                                .borrow_mut()
 439                                .set_mode(NavigationMode::Normal);
 440                            if let Some(data) = entry.data {
 441                                navigated |= item.navigate(data, cx);
 442                            }
 443                        });
 444                    }
 445
 446                    if !navigated {
 447                        workspace
 448                            .update(&mut cx, |workspace, cx| {
 449                                Self::navigate_history(workspace, pane, mode, cx)
 450                            })
 451                            .await;
 452                    }
 453                }
 454            })
 455        } else {
 456            Task::ready(())
 457        }
 458    }
 459
 460    pub(crate) fn open_item(
 461        workspace: &mut Workspace,
 462        pane: ViewHandle<Pane>,
 463        project_entry_id: ProjectEntryId,
 464        focus_item: bool,
 465        cx: &mut ViewContext<Workspace>,
 466        build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
 467    ) -> Box<dyn ItemHandle> {
 468        let existing_item = pane.update(cx, |pane, cx| {
 469            for item in pane.items.iter() {
 470                if item.project_path(cx).is_some()
 471                    && item.project_entry_ids(cx).as_slice() == [project_entry_id]
 472                {
 473                    let item = item.boxed_clone();
 474                    return Some(item);
 475                }
 476            }
 477            None
 478        });
 479
 480        // Even if the item exists, we re-add it to reorder it after the active item.
 481        // We may revisit this behavior after adding an "activation history" for pane items.
 482        let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx)));
 483        Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx);
 484        item
 485    }
 486
 487    pub(crate) fn add_item(
 488        workspace: &mut Workspace,
 489        pane: &ViewHandle<Pane>,
 490        item: Box<dyn ItemHandle>,
 491        activate_pane: bool,
 492        focus_item: bool,
 493        destination_index: Option<usize>,
 494        cx: &mut ViewContext<Workspace>,
 495    ) {
 496        // If no destination index is specified, add or move the item after the active item.
 497        let mut insertion_index = {
 498            let pane = pane.read(cx);
 499            cmp::min(
 500                if let Some(destination_index) = destination_index {
 501                    destination_index
 502                } else {
 503                    pane.active_item_index + 1
 504                },
 505                pane.items.len(),
 506            )
 507        };
 508
 509        item.added_to_pane(workspace, pane.clone(), cx);
 510
 511        // Does the item already exist?
 512        let project_entry_id = if item.is_singleton(cx) {
 513            item.project_entry_ids(cx).get(0).copied()
 514        } else {
 515            None
 516        };
 517
 518        let existing_item_index = pane.read(cx).items.iter().position(|existing_item| {
 519            if existing_item.id() == item.id() {
 520                true
 521            } else if existing_item.is_singleton(cx) {
 522                existing_item
 523                    .project_entry_ids(cx)
 524                    .get(0)
 525                    .map_or(false, |existing_entry_id| {
 526                        Some(existing_entry_id) == project_entry_id.as_ref()
 527                    })
 528            } else {
 529                false
 530            }
 531        });
 532
 533        if let Some(existing_item_index) = existing_item_index {
 534            // If the item already exists, move it to the desired destination and activate it
 535            pane.update(cx, |pane, cx| {
 536                if existing_item_index != insertion_index {
 537                    cx.reparent(&item);
 538                    let existing_item_is_active = existing_item_index == pane.active_item_index;
 539
 540                    // If the caller didn't specify a destination and the added item is already
 541                    // the active one, don't move it
 542                    if existing_item_is_active && destination_index.is_none() {
 543                        insertion_index = existing_item_index;
 544                    } else {
 545                        pane.items.remove(existing_item_index);
 546                        if existing_item_index < pane.active_item_index {
 547                            pane.active_item_index -= 1;
 548                        }
 549                        insertion_index = insertion_index.min(pane.items.len());
 550
 551                        pane.items.insert(insertion_index, item.clone());
 552
 553                        if existing_item_is_active {
 554                            pane.active_item_index = insertion_index;
 555                        } else if insertion_index <= pane.active_item_index {
 556                            pane.active_item_index += 1;
 557                        }
 558                    }
 559
 560                    cx.notify();
 561                }
 562
 563                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
 564            });
 565        } else {
 566            pane.update(cx, |pane, cx| {
 567                cx.reparent(&item);
 568                pane.items.insert(insertion_index, item);
 569                if insertion_index <= pane.active_item_index {
 570                    pane.active_item_index += 1;
 571                }
 572
 573                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
 574                cx.notify();
 575            });
 576        }
 577    }
 578
 579    pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
 580        self.items.iter()
 581    }
 582
 583    pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
 584        self.items
 585            .iter()
 586            .filter_map(|item| item.to_any().downcast())
 587    }
 588
 589    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
 590        self.items.get(self.active_item_index).cloned()
 591    }
 592
 593    pub fn item_for_entry(
 594        &self,
 595        entry_id: ProjectEntryId,
 596        cx: &AppContext,
 597    ) -> Option<Box<dyn ItemHandle>> {
 598        self.items.iter().find_map(|item| {
 599            if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
 600                Some(item.boxed_clone())
 601            } else {
 602                None
 603            }
 604        })
 605    }
 606
 607    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
 608        self.items.iter().position(|i| i.id() == item.id())
 609    }
 610
 611    pub fn activate_item(
 612        &mut self,
 613        index: usize,
 614        activate_pane: bool,
 615        focus_item: bool,
 616        cx: &mut ViewContext<Self>,
 617    ) {
 618        use NavigationMode::{GoingBack, GoingForward};
 619        if index < self.items.len() {
 620            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
 621            if prev_active_item_ix != self.active_item_index
 622                || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
 623            {
 624                if let Some(prev_item) = self.items.get(prev_active_item_ix) {
 625                    prev_item.deactivated(cx);
 626                }
 627                cx.emit(Event::ActivateItem {
 628                    local: activate_pane,
 629                });
 630            }
 631            self.update_toolbar(cx);
 632            if focus_item {
 633                self.focus_active_item(cx);
 634            }
 635            if activate_pane {
 636                cx.emit(Event::Focused);
 637            }
 638            self.autoscroll = true;
 639            cx.notify();
 640        }
 641    }
 642
 643    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
 644        let mut index = self.active_item_index;
 645        if index > 0 {
 646            index -= 1;
 647        } else if !self.items.is_empty() {
 648            index = self.items.len() - 1;
 649        }
 650        self.activate_item(index, true, true, cx);
 651    }
 652
 653    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
 654        let mut index = self.active_item_index;
 655        if index + 1 < self.items.len() {
 656            index += 1;
 657        } else {
 658            index = 0;
 659        }
 660        self.activate_item(index, true, true, cx);
 661    }
 662
 663    pub fn close_active_item(
 664        workspace: &mut Workspace,
 665        _: &CloseActiveItem,
 666        cx: &mut ViewContext<Workspace>,
 667    ) -> Option<Task<Result<()>>> {
 668        let pane_handle = workspace.active_pane().clone();
 669        let pane = pane_handle.read(cx);
 670        if pane.items.is_empty() {
 671            None
 672        } else {
 673            let item_id_to_close = pane.items[pane.active_item_index].id();
 674            let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
 675                item_id == item_id_to_close
 676            });
 677            Some(cx.foreground().spawn(async move {
 678                task.await?;
 679                Ok(())
 680            }))
 681        }
 682    }
 683
 684    pub fn close_inactive_items(
 685        workspace: &mut Workspace,
 686        _: &CloseInactiveItems,
 687        cx: &mut ViewContext<Workspace>,
 688    ) -> Option<Task<Result<()>>> {
 689        let pane_handle = workspace.active_pane().clone();
 690        let pane = pane_handle.read(cx);
 691        if pane.items.is_empty() {
 692            None
 693        } else {
 694            let active_item_id = pane.items[pane.active_item_index].id();
 695            let task =
 696                Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
 697            Some(cx.foreground().spawn(async move {
 698                task.await?;
 699                Ok(())
 700            }))
 701        }
 702    }
 703
 704    pub fn close_item(
 705        workspace: &mut Workspace,
 706        pane: ViewHandle<Pane>,
 707        item_id_to_close: usize,
 708        cx: &mut ViewContext<Workspace>,
 709    ) -> Task<Result<()>> {
 710        Self::close_items(workspace, pane, cx, move |view_id| {
 711            view_id == item_id_to_close
 712        })
 713    }
 714
 715    pub fn close_items(
 716        workspace: &mut Workspace,
 717        pane: ViewHandle<Pane>,
 718        cx: &mut ViewContext<Workspace>,
 719        should_close: impl 'static + Fn(usize) -> bool,
 720    ) -> Task<Result<()>> {
 721        let project = workspace.project().clone();
 722
 723        // Find the items to close.
 724        let mut items_to_close = Vec::new();
 725        for item in &pane.read(cx).items {
 726            if should_close(item.id()) {
 727                items_to_close.push(item.boxed_clone());
 728            }
 729        }
 730
 731        // If a buffer is open both in a singleton editor and in a multibuffer, make sure
 732        // to focus the singleton buffer when prompting to save that buffer, as opposed
 733        // to focusing the multibuffer, because this gives the user a more clear idea
 734        // of what content they would be saving.
 735        items_to_close.sort_by_key(|item| !item.is_singleton(cx));
 736
 737        cx.spawn(|workspace, mut cx| async move {
 738            let mut saved_project_entry_ids = HashSet::default();
 739            for item in items_to_close.clone() {
 740                // Find the item's current index and its set of project entries. Avoid
 741                // storing these in advance, in case they have changed since this task
 742                // was started.
 743                let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
 744                    (pane.index_for_item(&*item), item.project_entry_ids(cx))
 745                });
 746                let item_ix = if let Some(ix) = item_ix {
 747                    ix
 748                } else {
 749                    continue;
 750                };
 751
 752                // If an item hasn't yet been associated with a project entry, then always
 753                // prompt to save it before closing it. Otherwise, check if the item has
 754                // any project entries that are not open anywhere else in the workspace,
 755                // AND that the user has not already been prompted to save. If there are
 756                // any such project entries, prompt the user to save this item.
 757                let should_save = if project_entry_ids.is_empty() {
 758                    true
 759                } else {
 760                    workspace.read_with(&cx, |workspace, cx| {
 761                        for item in workspace.items(cx) {
 762                            if !items_to_close
 763                                .iter()
 764                                .any(|item_to_close| item_to_close.id() == item.id())
 765                            {
 766                                let other_project_entry_ids = item.project_entry_ids(cx);
 767                                project_entry_ids
 768                                    .retain(|id| !other_project_entry_ids.contains(id));
 769                            }
 770                        }
 771                    });
 772                    project_entry_ids
 773                        .iter()
 774                        .any(|id| saved_project_entry_ids.insert(*id))
 775                };
 776
 777                if should_save
 778                    && !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
 779                        .await?
 780                {
 781                    break;
 782                }
 783
 784                // Remove the item from the pane.
 785                pane.update(&mut cx, |pane, cx| {
 786                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
 787                        pane.remove_item(item_ix, cx);
 788                    }
 789                });
 790            }
 791
 792            pane.update(&mut cx, |_, cx| cx.notify());
 793            Ok(())
 794        })
 795    }
 796
 797    fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
 798        if item_ix == self.active_item_index {
 799            // Activate the previous item if possible.
 800            // This returns the user to the previously opened tab if they closed
 801            // a new item they just navigated to.
 802            if item_ix > 0 {
 803                self.activate_prev_item(cx);
 804            } else if item_ix + 1 < self.items.len() {
 805                self.activate_next_item(cx);
 806            }
 807        }
 808
 809        let item = self.items.remove(item_ix);
 810        cx.emit(Event::RemoveItem { item_id: item.id() });
 811        if self.items.is_empty() {
 812            item.deactivated(cx);
 813            self.update_toolbar(cx);
 814            cx.emit(Event::Remove);
 815        }
 816
 817        if item_ix < self.active_item_index {
 818            self.active_item_index -= 1;
 819        }
 820
 821        self.nav_history
 822            .borrow_mut()
 823            .set_mode(NavigationMode::ClosingItem);
 824        item.deactivated(cx);
 825        self.nav_history
 826            .borrow_mut()
 827            .set_mode(NavigationMode::Normal);
 828
 829        if let Some(path) = item.project_path(cx) {
 830            self.nav_history
 831                .borrow_mut()
 832                .paths_by_item
 833                .insert(item.id(), path);
 834        } else {
 835            self.nav_history
 836                .borrow_mut()
 837                .paths_by_item
 838                .remove(&item.id());
 839        }
 840
 841        cx.notify();
 842    }
 843
 844    pub async fn save_item(
 845        project: ModelHandle<Project>,
 846        pane: &ViewHandle<Pane>,
 847        item_ix: usize,
 848        item: &dyn ItemHandle,
 849        should_prompt_for_save: bool,
 850        cx: &mut AsyncAppContext,
 851    ) -> Result<bool> {
 852        const CONFLICT_MESSAGE: &str =
 853            "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 854        const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 855
 856        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
 857            (
 858                item.has_conflict(cx),
 859                item.is_dirty(cx),
 860                item.can_save(cx),
 861                item.is_singleton(cx),
 862            )
 863        });
 864
 865        if has_conflict && can_save {
 866            let mut answer = pane.update(cx, |pane, cx| {
 867                pane.activate_item(item_ix, true, true, cx);
 868                cx.prompt(
 869                    PromptLevel::Warning,
 870                    CONFLICT_MESSAGE,
 871                    &["Overwrite", "Discard", "Cancel"],
 872                )
 873            });
 874            match answer.next().await {
 875                Some(0) => cx.update(|cx| item.save(project, cx)).await?,
 876                Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
 877                _ => return Ok(false),
 878            }
 879        } else if is_dirty && (can_save || is_singleton) {
 880            let will_autosave = cx.read(|cx| {
 881                matches!(
 882                    cx.global::<Settings>().autosave,
 883                    Autosave::OnFocusChange | Autosave::OnWindowChange
 884                ) && Self::can_autosave_item(&*item, cx)
 885            });
 886            let should_save = if should_prompt_for_save && !will_autosave {
 887                let mut answer = pane.update(cx, |pane, cx| {
 888                    pane.activate_item(item_ix, true, true, cx);
 889                    cx.prompt(
 890                        PromptLevel::Warning,
 891                        DIRTY_MESSAGE,
 892                        &["Save", "Don't Save", "Cancel"],
 893                    )
 894                });
 895                match answer.next().await {
 896                    Some(0) => true,
 897                    Some(1) => false,
 898                    _ => return Ok(false),
 899                }
 900            } else {
 901                true
 902            };
 903
 904            if should_save {
 905                if can_save {
 906                    cx.update(|cx| item.save(project, cx)).await?;
 907                } else if is_singleton {
 908                    let start_abs_path = project
 909                        .read_with(cx, |project, cx| {
 910                            let worktree = project.visible_worktrees(cx).next()?;
 911                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 912                        })
 913                        .unwrap_or_else(|| Path::new("").into());
 914
 915                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
 916                    if let Some(abs_path) = abs_path.next().await.flatten() {
 917                        cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
 918                    } else {
 919                        return Ok(false);
 920                    }
 921                }
 922            }
 923        }
 924        Ok(true)
 925    }
 926
 927    fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
 928        let is_deleted = item.project_entry_ids(cx).is_empty();
 929        item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
 930    }
 931
 932    pub fn autosave_item(
 933        item: &dyn ItemHandle,
 934        project: ModelHandle<Project>,
 935        cx: &mut MutableAppContext,
 936    ) -> Task<Result<()>> {
 937        if Self::can_autosave_item(item, cx) {
 938            item.save(project, cx)
 939        } else {
 940            Task::ready(Ok(()))
 941        }
 942    }
 943
 944    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
 945        if let Some(active_item) = self.active_item() {
 946            cx.focus(active_item);
 947        }
 948    }
 949
 950    fn move_item(
 951        workspace: &mut Workspace,
 952        from: ViewHandle<Pane>,
 953        to: ViewHandle<Pane>,
 954        item_to_move: usize,
 955        destination_index: usize,
 956        cx: &mut ViewContext<Workspace>,
 957    ) {
 958        let item_to_move = from
 959            .read(cx)
 960            .items()
 961            .enumerate()
 962            .find(|(_, item_handle)| item_handle.id() == item_to_move);
 963
 964        if item_to_move.is_none() {
 965            log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
 966            return;
 967        }
 968
 969        let (item_ix, item_handle) = item_to_move.unwrap();
 970        // This automatically removes duplicate items in the pane
 971        Pane::add_item(
 972            workspace,
 973            &to,
 974            item_handle.clone(),
 975            true,
 976            true,
 977            Some(destination_index),
 978            cx,
 979        );
 980
 981        if from != to {
 982            // Close item from previous pane
 983            from.update(cx, |from, cx| {
 984                from.remove_item(item_ix, cx);
 985            });
 986        }
 987
 988        cx.focus(to);
 989    }
 990
 991    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
 992        cx.emit(Event::Split(direction));
 993    }
 994
 995    fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
 996        self.tab_bar_context_menu.update(cx, |menu, cx| {
 997            menu.show(
 998                action.position,
 999                AnchorCorner::TopRight,
1000                vec![
1001                    ContextMenuItem::item("Split Right", SplitRight),
1002                    ContextMenuItem::item("Split Left", SplitLeft),
1003                    ContextMenuItem::item("Split Up", SplitUp),
1004                    ContextMenuItem::item("Split Down", SplitDown),
1005                ],
1006                cx,
1007            );
1008        });
1009    }
1010
1011    fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
1012        self.tab_bar_context_menu.update(cx, |menu, cx| {
1013            menu.show(
1014                action.position,
1015                AnchorCorner::TopRight,
1016                vec![
1017                    ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
1018                    ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
1019                    ContextMenuItem::item("Expand Dock", ExpandDock),
1020                ],
1021                cx,
1022            );
1023        });
1024    }
1025
1026    fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
1027        self.tab_bar_context_menu.update(cx, |menu, cx| {
1028            menu.show(
1029                action.position,
1030                AnchorCorner::TopRight,
1031                vec![
1032                    ContextMenuItem::item("New File", NewFile),
1033                    ContextMenuItem::item("New Terminal", NewTerminal),
1034                    ContextMenuItem::item("New Search", NewSearch),
1035                ],
1036                cx,
1037            );
1038        });
1039    }
1040
1041    pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
1042        &self.toolbar
1043    }
1044
1045    fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
1046        let active_item = self
1047            .items
1048            .get(self.active_item_index)
1049            .map(|item| item.as_ref());
1050        self.toolbar.update(cx, |toolbar, cx| {
1051            toolbar.set_active_pane_item(active_item, cx);
1052        });
1053    }
1054
1055    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
1056        let theme = cx.global::<Settings>().theme.clone();
1057        let filler_index = self.items.len();
1058
1059        enum Tabs {}
1060        enum Tab {}
1061        enum Filler {}
1062        let pane = cx.handle();
1063        MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
1064            let autoscroll = if mem::take(&mut self.autoscroll) {
1065                Some(self.active_item_index)
1066            } else {
1067                None
1068            };
1069
1070            let pane_active = self.is_active;
1071
1072            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
1073            for (ix, (item, detail)) in self
1074                .items
1075                .iter()
1076                .cloned()
1077                .zip(self.tab_details(cx))
1078                .enumerate()
1079            {
1080                let detail = if detail == 0 { None } else { Some(detail) };
1081                let tab_active = ix == self.active_item_index;
1082
1083                row.add_child({
1084                    MouseEventHandler::<Tab>::new(ix, cx, {
1085                        let item = item.clone();
1086                        let pane = pane.clone();
1087                        let detail = detail.clone();
1088
1089                        let theme = cx.global::<Settings>().theme.clone();
1090
1091                        move |mouse_state, cx| {
1092                            let tab_style =
1093                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
1094                            let hovered = mouse_state.hovered;
1095                            Self::render_tab(
1096                                &item,
1097                                pane,
1098                                ix == 0,
1099                                detail,
1100                                hovered,
1101                                Self::tab_overlay_color(hovered, theme.as_ref(), cx),
1102                                tab_style,
1103                                cx,
1104                            )
1105                        }
1106                    })
1107                    .with_cursor_style(if pane_active && tab_active {
1108                        CursorStyle::Arrow
1109                    } else {
1110                        CursorStyle::PointingHand
1111                    })
1112                    .on_down(MouseButton::Left, move |_, cx| {
1113                        cx.dispatch_action(ActivateItem(ix));
1114                    })
1115                    .on_click(MouseButton::Middle, {
1116                        let item = item.clone();
1117                        let pane = pane.clone();
1118                        move |_, cx: &mut EventContext| {
1119                            cx.dispatch_action(CloseItem {
1120                                item_id: item.id(),
1121                                pane: pane.clone(),
1122                            })
1123                        }
1124                    })
1125                    .on_up(MouseButton::Left, {
1126                        let pane = pane.clone();
1127                        move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
1128                    })
1129                    .as_draggable(
1130                        DraggedItem {
1131                            item,
1132                            pane: pane.clone(),
1133                        },
1134                        {
1135                            let theme = cx.global::<Settings>().theme.clone();
1136
1137                            let detail = detail.clone();
1138                            move |dragged_item, cx: &mut RenderContext<Workspace>| {
1139                                let tab_style = &theme.workspace.tab_bar.dragged_tab;
1140                                Self::render_tab(
1141                                    &dragged_item.item,
1142                                    dragged_item.pane.clone(),
1143                                    false,
1144                                    detail,
1145                                    false,
1146                                    None,
1147                                    &tab_style,
1148                                    cx,
1149                                )
1150                            }
1151                        },
1152                    )
1153                    .boxed()
1154                })
1155            }
1156
1157            // Use the inactive tab style along with the current pane's active status to decide how to render
1158            // the filler
1159            let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
1160            row.add_child(
1161                MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
1162                    let mut filler = Empty::new()
1163                        .contained()
1164                        .with_style(filler_style.container)
1165                        .with_border(filler_style.container.border);
1166
1167                    if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
1168                    {
1169                        filler = filler.with_overlay_color(overlay);
1170                    }
1171
1172                    filler.boxed()
1173                })
1174                .flex(1., true)
1175                .named("filler"),
1176            );
1177
1178            row.boxed()
1179        })
1180        .on_up(MouseButton::Left, move |_, cx| {
1181            Pane::handle_dropped_item(&pane, filler_index, cx)
1182        })
1183    }
1184
1185    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
1186        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
1187
1188        let mut tab_descriptions = HashMap::default();
1189        let mut done = false;
1190        while !done {
1191            done = true;
1192
1193            // Store item indices by their tab description.
1194            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
1195                if let Some(description) = item.tab_description(*detail, cx) {
1196                    if *detail == 0
1197                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
1198                    {
1199                        tab_descriptions
1200                            .entry(description)
1201                            .or_insert(Vec::new())
1202                            .push(ix);
1203                    }
1204                }
1205            }
1206
1207            // If two or more items have the same tab description, increase their level
1208            // of detail and try again.
1209            for (_, item_ixs) in tab_descriptions.drain() {
1210                if item_ixs.len() > 1 {
1211                    done = false;
1212                    for ix in item_ixs {
1213                        tab_details[ix] += 1;
1214                    }
1215                }
1216            }
1217        }
1218
1219        tab_details
1220    }
1221
1222    fn render_tab<V: View>(
1223        item: &Box<dyn ItemHandle>,
1224        pane: WeakViewHandle<Pane>,
1225        first: bool,
1226        detail: Option<usize>,
1227        hovered: bool,
1228        overlay: Option<Color>,
1229        tab_style: &theme::Tab,
1230        cx: &mut RenderContext<V>,
1231    ) -> ElementBox {
1232        let title = item.tab_content(detail, &tab_style, cx);
1233        let mut container = tab_style.container.clone();
1234        if first {
1235            container.border.left = false;
1236        }
1237
1238        let mut tab = Flex::row()
1239            .with_child(
1240                Align::new({
1241                    let diameter = 7.0;
1242                    let icon_color = if item.has_conflict(cx) {
1243                        Some(tab_style.icon_conflict)
1244                    } else if item.is_dirty(cx) {
1245                        Some(tab_style.icon_dirty)
1246                    } else {
1247                        None
1248                    };
1249
1250                    ConstrainedBox::new(
1251                        Canvas::new(move |bounds, _, cx| {
1252                            if let Some(color) = icon_color {
1253                                let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
1254                                cx.scene.push_quad(Quad {
1255                                    bounds: square,
1256                                    background: Some(color),
1257                                    border: Default::default(),
1258                                    corner_radius: diameter / 2.,
1259                                });
1260                            }
1261                        })
1262                        .boxed(),
1263                    )
1264                    .with_width(diameter)
1265                    .with_height(diameter)
1266                    .boxed()
1267                })
1268                .boxed(),
1269            )
1270            .with_child(
1271                Container::new(Align::new(title).boxed())
1272                    .with_style(ContainerStyle {
1273                        margin: Margin {
1274                            left: tab_style.spacing,
1275                            right: tab_style.spacing,
1276                            ..Default::default()
1277                        },
1278                        ..Default::default()
1279                    })
1280                    .boxed(),
1281            )
1282            .with_child(
1283                Align::new(
1284                    ConstrainedBox::new(if hovered {
1285                        let item_id = item.id();
1286                        enum TabCloseButton {}
1287                        let icon = Svg::new("icons/x_mark_thin_8.svg");
1288                        MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
1289                            if mouse_state.hovered {
1290                                icon.with_color(tab_style.icon_close_active).boxed()
1291                            } else {
1292                                icon.with_color(tab_style.icon_close).boxed()
1293                            }
1294                        })
1295                        .with_padding(Padding::uniform(4.))
1296                        .with_cursor_style(CursorStyle::PointingHand)
1297                        .on_click(MouseButton::Left, {
1298                            let pane = pane.clone();
1299                            move |_, cx| {
1300                                cx.dispatch_action(CloseItem {
1301                                    item_id,
1302                                    pane: pane.clone(),
1303                                })
1304                            }
1305                        })
1306                        .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
1307                        .named("close-tab-icon")
1308                    } else {
1309                        Empty::new().boxed()
1310                    })
1311                    .with_width(tab_style.icon_width)
1312                    .boxed(),
1313                )
1314                .boxed(),
1315            )
1316            .contained()
1317            .with_style(container);
1318
1319        if let Some(overlay) = overlay {
1320            tab = tab.with_overlay_color(overlay);
1321        }
1322
1323        tab.constrained().with_height(tab_style.height).boxed()
1324    }
1325
1326    fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
1327        if let Some((_, dragged_item)) = cx
1328            .global::<DragAndDrop<Workspace>>()
1329            .currently_dragged::<DraggedItem>(cx.window_id)
1330        {
1331            cx.dispatch_action(MoveItem {
1332                item_id: dragged_item.item.id(),
1333                from: dragged_item.pane.clone(),
1334                to: pane.clone(),
1335                destination_index: index,
1336            })
1337        } else {
1338            cx.propogate_event();
1339        }
1340    }
1341
1342    fn tab_overlay_color(
1343        hovered: bool,
1344        theme: &Theme,
1345        cx: &mut RenderContext<Self>,
1346    ) -> Option<Color> {
1347        if hovered
1348            && cx
1349                .global::<DragAndDrop<Workspace>>()
1350                .currently_dragged::<DraggedItem>(cx.window_id())
1351                .is_some()
1352        {
1353            Some(theme.workspace.tab_bar.drop_target_overlay_color)
1354        } else {
1355            None
1356        }
1357    }
1358}
1359
1360impl Entity for Pane {
1361    type Event = Event;
1362}
1363
1364impl View for Pane {
1365    fn ui_name() -> &'static str {
1366        "Pane"
1367    }
1368
1369    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
1370        let this = cx.handle();
1371
1372        enum MouseNavigationHandler {}
1373
1374        Stack::new()
1375            .with_child(
1376                MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
1377                    if let Some(active_item) = self.active_item() {
1378                        Flex::column()
1379                            .with_child({
1380                                let mut tab_row = Flex::row()
1381                                    .with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
1382
1383                                // Render pane buttons
1384                                let theme = cx.global::<Settings>().theme.clone();
1385                                if self.is_active {
1386                                    tab_row.add_child(
1387                                        Flex::row()
1388                                            // New menu
1389                                            .with_child(tab_bar_button(
1390                                                0,
1391                                                "icons/plus_12.svg",
1392                                                cx,
1393                                                |position| DeployNewMenu { position },
1394                                            ))
1395                                            .with_child(
1396                                                self.docked
1397                                                    .map(|anchor| {
1398                                                        // Add the dock menu button if this pane is a dock
1399                                                        let dock_icon =
1400                                                            icon_for_dock_anchor(anchor);
1401
1402                                                        tab_bar_button(
1403                                                            1,
1404                                                            dock_icon,
1405                                                            cx,
1406                                                            |position| DeployDockMenu { position },
1407                                                        )
1408                                                    })
1409                                                    .unwrap_or_else(|| {
1410                                                        // Add the split menu if this pane is not a dock
1411                                                        tab_bar_button(
1412                                                            2,
1413                                                            "icons/split_12.svg",
1414                                                            cx,
1415                                                            |position| DeploySplitMenu { position },
1416                                                        )
1417                                                    }),
1418                                            )
1419                                            // Add the close dock button if this pane is a dock
1420                                            .with_children(self.docked.map(|_| {
1421                                                tab_bar_button(
1422                                                    3,
1423                                                    "icons/x_mark_thin_8.svg",
1424                                                    cx,
1425                                                    |_| HideDock,
1426                                                )
1427                                            }))
1428                                            .contained()
1429                                            .with_style(
1430                                                theme.workspace.tab_bar.pane_button_container,
1431                                            )
1432                                            .flex(1., false)
1433                                            .boxed(),
1434                                    )
1435                                }
1436
1437                                tab_row
1438                                    .constrained()
1439                                    .with_height(theme.workspace.tab_bar.height)
1440                                    .contained()
1441                                    .with_style(theme.workspace.tab_bar.container)
1442                                    .flex(1., false)
1443                                    .named("tab bar")
1444                            })
1445                            .with_child(ChildView::new(&self.toolbar).expanded().boxed())
1446                            .with_child(ChildView::new(active_item).flex(1., true).boxed())
1447                            .boxed()
1448                    } else {
1449                        enum EmptyPane {}
1450                        let theme = cx.global::<Settings>().theme.clone();
1451
1452                        MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
1453                            Empty::new()
1454                                .contained()
1455                                .with_background_color(theme.workspace.background)
1456                                .boxed()
1457                        })
1458                        .on_down(MouseButton::Left, |_, cx| {
1459                            cx.focus_parent_view();
1460                        })
1461                        .on_up(MouseButton::Left, {
1462                            let pane = this.clone();
1463                            move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
1464                        })
1465                        .boxed()
1466                    }
1467                })
1468                .on_down(MouseButton::Navigate(NavigationDirection::Back), {
1469                    let this = this.clone();
1470                    move |_, cx| {
1471                        cx.dispatch_action(GoBack {
1472                            pane: Some(this.clone()),
1473                        });
1474                    }
1475                })
1476                .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
1477                    let this = this.clone();
1478                    move |_, cx| {
1479                        cx.dispatch_action(GoForward {
1480                            pane: Some(this.clone()),
1481                        })
1482                    }
1483                })
1484                .boxed(),
1485            )
1486            .with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
1487            .named("pane")
1488    }
1489
1490    fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
1491        if cx.is_self_focused() {
1492            if let Some(last_focused_view) = self
1493                .last_focused_view
1494                .as_ref()
1495                .and_then(|handle| handle.upgrade(cx))
1496                .filter(|handle| handle.id() != self.tab_bar_context_menu.id())
1497            {
1498                cx.focus(last_focused_view);
1499            } else {
1500                self.focus_active_item(cx);
1501            }
1502        } else {
1503            self.last_focused_view = Some(focused.downgrade());
1504        }
1505        cx.emit(Event::Focused);
1506    }
1507}
1508
1509fn tab_bar_button<A: Action>(
1510    index: usize,
1511    icon: &'static str,
1512    cx: &mut RenderContext<Pane>,
1513    action_builder: impl 'static + Fn(Vector2F) -> A,
1514) -> ElementBox {
1515    enum TabBarButton {}
1516
1517    MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
1518        let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
1519        let style = theme.pane_button.style_for(mouse_state, false);
1520        Svg::new(icon)
1521            .with_color(style.color)
1522            .constrained()
1523            .with_width(style.icon_width)
1524            .aligned()
1525            .constrained()
1526            .with_width(style.button_width)
1527            .with_height(style.button_width)
1528            // .aligned()
1529            .boxed()
1530    })
1531    .with_cursor_style(CursorStyle::PointingHand)
1532    .on_click(MouseButton::Left, move |e, cx| {
1533        cx.dispatch_action(action_builder(e.region.lower_right()));
1534    })
1535    .flex(1., false)
1536    .boxed()
1537}
1538
1539impl ItemNavHistory {
1540    pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
1541        self.history.borrow_mut().push(data, self.item.clone(), cx);
1542    }
1543
1544    pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1545        self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
1546    }
1547
1548    pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1549        self.history
1550            .borrow_mut()
1551            .pop(NavigationMode::GoingForward, cx)
1552    }
1553}
1554
1555impl NavHistory {
1556    fn set_mode(&mut self, mode: NavigationMode) {
1557        self.mode = mode;
1558    }
1559
1560    fn disable(&mut self) {
1561        self.mode = NavigationMode::Disabled;
1562    }
1563
1564    fn enable(&mut self) {
1565        self.mode = NavigationMode::Normal;
1566    }
1567
1568    fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
1569        let entry = match mode {
1570            NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
1571                return None
1572            }
1573            NavigationMode::GoingBack => &mut self.backward_stack,
1574            NavigationMode::GoingForward => &mut self.forward_stack,
1575            NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
1576        }
1577        .pop_back();
1578        if entry.is_some() {
1579            self.did_update(cx);
1580        }
1581        entry
1582    }
1583
1584    fn push<D: 'static + Any>(
1585        &mut self,
1586        data: Option<D>,
1587        item: Rc<dyn WeakItemHandle>,
1588        cx: &mut MutableAppContext,
1589    ) {
1590        match self.mode {
1591            NavigationMode::Disabled => {}
1592            NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
1593                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1594                    self.backward_stack.pop_front();
1595                }
1596                self.backward_stack.push_back(NavigationEntry {
1597                    item,
1598                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1599                });
1600                self.forward_stack.clear();
1601            }
1602            NavigationMode::GoingBack => {
1603                if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1604                    self.forward_stack.pop_front();
1605                }
1606                self.forward_stack.push_back(NavigationEntry {
1607                    item,
1608                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1609                });
1610            }
1611            NavigationMode::GoingForward => {
1612                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1613                    self.backward_stack.pop_front();
1614                }
1615                self.backward_stack.push_back(NavigationEntry {
1616                    item,
1617                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1618                });
1619            }
1620            NavigationMode::ClosingItem => {
1621                if self.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
1622                    self.closed_stack.pop_front();
1623                }
1624                self.closed_stack.push_back(NavigationEntry {
1625                    item,
1626                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
1627                });
1628            }
1629        }
1630        self.did_update(cx);
1631    }
1632
1633    fn did_update(&self, cx: &mut MutableAppContext) {
1634        if let Some(pane) = self.pane.upgrade(cx) {
1635            cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
1636        }
1637    }
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642    use super::*;
1643    use crate::tests::TestItem;
1644    use gpui::TestAppContext;
1645    use project::FakeFs;
1646
1647    #[gpui::test]
1648    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
1649        cx.foreground().forbid_parking();
1650        Settings::test_async(cx);
1651        let fs = FakeFs::new(cx.background());
1652
1653        let project = Project::test(fs, None, cx).await;
1654        let (_, workspace) =
1655            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1656        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1657
1658        // 1. Add with a destination index
1659        //   a. Add before the active item
1660        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1661        workspace.update(cx, |workspace, cx| {
1662            Pane::add_item(
1663                workspace,
1664                &pane,
1665                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1666                false,
1667                false,
1668                Some(0),
1669                cx,
1670            );
1671        });
1672        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1673
1674        //   b. Add after the active item
1675        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1676        workspace.update(cx, |workspace, cx| {
1677            Pane::add_item(
1678                workspace,
1679                &pane,
1680                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1681                false,
1682                false,
1683                Some(2),
1684                cx,
1685            );
1686        });
1687        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1688
1689        //   c. Add at the end of the item list (including off the length)
1690        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1691        workspace.update(cx, |workspace, cx| {
1692            Pane::add_item(
1693                workspace,
1694                &pane,
1695                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1696                false,
1697                false,
1698                Some(5),
1699                cx,
1700            );
1701        });
1702        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1703
1704        // 2. Add without a destination index
1705        //   a. Add with active item at the start of the item list
1706        set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1707        workspace.update(cx, |workspace, cx| {
1708            Pane::add_item(
1709                workspace,
1710                &pane,
1711                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1712                false,
1713                false,
1714                None,
1715                cx,
1716            );
1717        });
1718        set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
1719
1720        //   b. Add with active item at the end of the item list
1721        set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1722        workspace.update(cx, |workspace, cx| {
1723            Pane::add_item(
1724                workspace,
1725                &pane,
1726                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
1727                false,
1728                false,
1729                None,
1730                cx,
1731            );
1732        });
1733        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
1734    }
1735
1736    #[gpui::test]
1737    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
1738        cx.foreground().forbid_parking();
1739        Settings::test_async(cx);
1740        let fs = FakeFs::new(cx.background());
1741
1742        let project = Project::test(fs, None, cx).await;
1743        let (_, workspace) =
1744            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1745        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1746
1747        // 1. Add with a destination index
1748        //   1a. Add before the active item
1749        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1750        workspace.update(cx, |workspace, cx| {
1751            Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
1752        });
1753        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
1754
1755        //   1b. Add after the active item
1756        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1757        workspace.update(cx, |workspace, cx| {
1758            Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
1759        });
1760        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
1761
1762        //   1c. Add at the end of the item list (including off the length)
1763        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
1764        workspace.update(cx, |workspace, cx| {
1765            Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
1766        });
1767        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1768
1769        //   1d. Add same item to active index
1770        let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1771        workspace.update(cx, |workspace, cx| {
1772            Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
1773        });
1774        assert_item_labels(&pane, ["A", "B*", "C"], cx);
1775
1776        //   1e. Add item to index after same item in last position
1777        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
1778        workspace.update(cx, |workspace, cx| {
1779            Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
1780        });
1781        assert_item_labels(&pane, ["A", "B", "C*"], cx);
1782
1783        // 2. Add without a destination index
1784        //   2a. Add with active item at the start of the item list
1785        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
1786        workspace.update(cx, |workspace, cx| {
1787            Pane::add_item(workspace, &pane, d, false, false, None, cx);
1788        });
1789        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
1790
1791        //   2b. Add with active item at the end of the item list
1792        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
1793        workspace.update(cx, |workspace, cx| {
1794            Pane::add_item(workspace, &pane, a, false, false, None, cx);
1795        });
1796        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
1797
1798        //   2c. Add active item to active item at end of list
1799        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
1800        workspace.update(cx, |workspace, cx| {
1801            Pane::add_item(workspace, &pane, c, false, false, None, cx);
1802        });
1803        assert_item_labels(&pane, ["A", "B", "C*"], cx);
1804
1805        //   2d. Add active item to active item at start of list
1806        let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
1807        workspace.update(cx, |workspace, cx| {
1808            Pane::add_item(workspace, &pane, a, false, false, None, cx);
1809        });
1810        assert_item_labels(&pane, ["A*", "B", "C"], cx);
1811    }
1812
1813    #[gpui::test]
1814    async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
1815        cx.foreground().forbid_parking();
1816        Settings::test_async(cx);
1817        let fs = FakeFs::new(cx.background());
1818
1819        let project = Project::test(fs, None, cx).await;
1820        let (_, workspace) =
1821            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
1822        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1823
1824        // singleton view
1825        workspace.update(cx, |workspace, cx| {
1826            let item = TestItem::new()
1827                .with_singleton(true)
1828                .with_label("buffer 1")
1829                .with_project_entry_ids(&[1]);
1830
1831            Pane::add_item(
1832                workspace,
1833                &pane,
1834                Box::new(cx.add_view(|_| item)),
1835                false,
1836                false,
1837                None,
1838                cx,
1839            );
1840        });
1841        assert_item_labels(&pane, ["buffer 1*"], cx);
1842
1843        // new singleton view with the same project entry
1844        workspace.update(cx, |workspace, cx| {
1845            let item = TestItem::new()
1846                .with_singleton(true)
1847                .with_label("buffer 1")
1848                .with_project_entry_ids(&[1]);
1849
1850            Pane::add_item(
1851                workspace,
1852                &pane,
1853                Box::new(cx.add_view(|_| item)),
1854                false,
1855                false,
1856                None,
1857                cx,
1858            );
1859        });
1860        assert_item_labels(&pane, ["buffer 1*"], cx);
1861
1862        // new singleton view with different project entry
1863        workspace.update(cx, |workspace, cx| {
1864            let item = TestItem::new()
1865                .with_singleton(true)
1866                .with_label("buffer 2")
1867                .with_project_entry_ids(&[2]);
1868
1869            Pane::add_item(
1870                workspace,
1871                &pane,
1872                Box::new(cx.add_view(|_| item)),
1873                false,
1874                false,
1875                None,
1876                cx,
1877            );
1878        });
1879        assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx);
1880
1881        // new multibuffer view with the same project entry
1882        workspace.update(cx, |workspace, cx| {
1883            let item = TestItem::new()
1884                .with_singleton(false)
1885                .with_label("multibuffer 1")
1886                .with_project_entry_ids(&[1]);
1887
1888            Pane::add_item(
1889                workspace,
1890                &pane,
1891                Box::new(cx.add_view(|_| item)),
1892                false,
1893                false,
1894                None,
1895                cx,
1896            );
1897        });
1898        assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx);
1899
1900        // another multibuffer view with the same project entry
1901        workspace.update(cx, |workspace, cx| {
1902            let item = TestItem::new()
1903                .with_singleton(false)
1904                .with_label("multibuffer 1b")
1905                .with_project_entry_ids(&[1]);
1906
1907            Pane::add_item(
1908                workspace,
1909                &pane,
1910                Box::new(cx.add_view(|_| item)),
1911                false,
1912                false,
1913                None,
1914                cx,
1915            );
1916        });
1917        assert_item_labels(
1918            &pane,
1919            ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"],
1920            cx,
1921        );
1922    }
1923
1924    fn set_labeled_items<const COUNT: usize>(
1925        workspace: &ViewHandle<Workspace>,
1926        pane: &ViewHandle<Pane>,
1927        labels: [&str; COUNT],
1928        cx: &mut TestAppContext,
1929    ) -> [Box<ViewHandle<TestItem>>; COUNT] {
1930        pane.update(cx, |pane, _| {
1931            pane.items.clear();
1932        });
1933
1934        workspace.update(cx, |workspace, cx| {
1935            let mut active_item_index = 0;
1936
1937            let mut index = 0;
1938            let items = labels.map(|mut label| {
1939                if label.ends_with("*") {
1940                    label = label.trim_end_matches("*");
1941                    active_item_index = index;
1942                }
1943
1944                let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
1945                Pane::add_item(
1946                    workspace,
1947                    pane,
1948                    labeled_item.clone(),
1949                    false,
1950                    false,
1951                    None,
1952                    cx,
1953                );
1954                index += 1;
1955                labeled_item
1956            });
1957
1958            pane.update(cx, |pane, cx| {
1959                pane.activate_item(active_item_index, false, false, cx)
1960            });
1961
1962            items
1963        })
1964    }
1965
1966    // Assert the item label, with the active item label suffixed with a '*'
1967    fn assert_item_labels<const COUNT: usize>(
1968        pane: &ViewHandle<Pane>,
1969        expected_states: [&str; COUNT],
1970        cx: &mut TestAppContext,
1971    ) {
1972        pane.read_with(cx, |pane, cx| {
1973            let actual_states = pane
1974                .items
1975                .iter()
1976                .enumerate()
1977                .map(|(ix, item)| {
1978                    let mut state = item
1979                        .to_any()
1980                        .downcast::<TestItem>()
1981                        .unwrap()
1982                        .read(cx)
1983                        .label
1984                        .clone();
1985                    if ix == pane.active_item_index {
1986                        state.push('*');
1987                    }
1988                    state
1989                })
1990                .collect::<Vec<_>>();
1991
1992            assert_eq!(
1993                actual_states, expected_states,
1994                "pane items do not match expectation"
1995            );
1996        })
1997    }
1998}