pane.rs

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