//! The pane module contains the interfaces that must be implemented and followed by any concrete pane type. //! //! Each kind of pane involves both a [`BackingView`] implementation and a [`PaneContent`] implementation. //! APIs for managing a pane as part of a pane group, like getting its title or moving it from one //! tab to another, belong in [`PaneContent`]. APIs for rendering or interacting with an individual pane, //! like building its context menu, belong in [`BackingView`]. //! //! The [`PaneContent`] interface requires implementers to maintain a [`PaneId`] for their pane. //! The [`PaneId`] must be created via a [`PaneView`]. The [`PaneId`] is consequently //! used to render a [`PaneView`] which internally renders the pane, including the [`BackingView`]. pub(super) mod ai_document_pane; pub(super) mod ai_fact_pane; pub(super) mod code_diff_pane; pub(super) mod code_diff_pane_model; pub(super) mod code_pane; pub(super) mod custom_router_editor_pane; pub(super) mod env_var_collection_pane; pub(crate) mod environment_management_pane; pub(super) mod execution_profile_editor_pane; pub(super) mod file_pane; pub(super) mod get_started_pane; pub(super) mod get_started_view; #[cfg(not(target_family = "wasm"))] pub(super) mod local_harness_launch; pub(super) mod network_log_pane; pub(super) mod notebook_pane; pub(super) mod settings_pane; pub(super) mod terminal_pane; pub mod view; pub mod workflow_pane; use std::any::Any; use std::fmt::Display; use galaxy_util::remote_path::RemotePath; use galaxyui::elements::{DispatchEventResult, EventHandler, MouseInBehavior}; use galaxyui::presenter::ChildView; use galaxyui::{ Action, AppContext, Element, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity, View, ViewContext, ViewHandle, WeakModelHandle, }; use serde::{Deserialize, Serialize}; use url::Url; pub use self::view::{PaneHeaderAction, PaneHeaderCustomAction, PaneView, PaneViewEvent}; use super::{ActivationReason, LeafContents, PaneGroup, PaneGroupAction}; use crate::ai::ai_document_view::AIDocumentView; use crate::ai::blocklist::inline_action::code_diff_view::CodeDiffView; use crate::ai::execution_profiles::editor::ExecutionProfileEditorView; use crate::ai::facts::AIFactView; #[cfg(feature = "local_fs")] use crate::code::buffer_location::LocalOrRemotePath; use crate::code::view::CodeView; use crate::drive::sharing::ShareableObject; use crate::env_vars::view::env_var_collection::EnvVarCollectionView; use crate::menu::MenuItem; use crate::notebooks::file::FileNotebookView; use crate::notebooks::notebook::NotebookView; use crate::pane_group::focus_state::PaneFocusHandle; use crate::pane_group::pane::get_started_view::GetStartedView; use crate::server::network_log_view::NetworkLogView; use crate::server::telemetry::SharingDialogSource; use crate::settings::PaneSettings; use crate::settings_view::SettingsView; use crate::terminal::available_shells::AvailableShell; use crate::terminal::TerminalView; use crate::view_components::action_button::ActionButton; use crate::workflows::workflow_view::WorkflowView; pub(super) fn init(app: &mut AppContext) { self::view::init(app); get_started_view::init(app); } /// The opaque identifier for an arbitrary pane. Consumers /// should not be concerned with the internal IDs that are used; /// instead, consumers should use the [`PaneGroup`] APIs to get /// panes of concrete types from a pane ID. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct PaneId(IPaneId); #[derive(Debug, Clone, Copy)] pub enum ActionOrigin { // If a drag/drop action started from an editor tab, we pass its index forward. EditorTab(usize), Pane, } /// A [`PaneId`] that is known to belong to a terminal pane. /// Generally, prefer [`PaneId`], except for logic/features that will only /// ever apply to terminal sessions (like synced inputs and the block-sharing modal). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct TerminalPaneId(EntityId); impl From for PaneId { fn from(terminal_pane: TerminalPaneId) -> Self { PaneId(IPaneId { pane_type: IPaneType::Terminal, pane_view_id: terminal_pane.0, }) } } impl TerminalPaneId { /// Creates a [`TerminalPaneId`] for a dummy terminal pane. #[cfg(test)] pub fn dummy_terminal_pane_id() -> Self { Self(EntityId::new()) } } /// An internal representation of a pane ID. Specifically, we don't want to allow /// consumers to derive the underlying view ID from a pane ID. Instead, consumers /// should use the relevant [`crate::PaneGroup`] APIs to access pane content (which /// can provide the underlying view). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] struct IPaneId { /// The type of pane. Needs to match the BackingView. pane_type: IPaneType, /// The entity id of the PaneView. pane_view_id: EntityId, } impl Display for IPaneId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Pane {} ({})", self.pane_type, self.pane_view_id) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub(crate) enum IPaneType { Terminal, Notebook, File, Code, CodeDiff, EnvVarCollection, EnvironmentManagement, Workflow, Settings, AIFact, AIDocument, CustomRouterEditor, ExecutionProfileEditor, GetStarted, NetworkLog, DeferredPlaceholder, /// A pane type only for tests. #[cfg(test)] Dummy, } impl Display for IPaneType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { IPaneType::Terminal => write!(f, "Terminal"), IPaneType::Notebook => write!(f, "Notebook"), IPaneType::File => write!(f, "File"), IPaneType::Code => write!(f, "Code"), IPaneType::CodeDiff => write!(f, "Code Diff"), IPaneType::EnvVarCollection => write!(f, "Environment Variable Collection"), IPaneType::EnvironmentManagement => write!(f, "Environment Management"), IPaneType::Workflow => write!(f, "Workflow"), IPaneType::Settings => write!(f, "Settings"), IPaneType::AIFact => write!(f, "AI Fact"), IPaneType::AIDocument => write!(f, "AI Document"), IPaneType::CustomRouterEditor => write!(f, "Custom Router Editor"), IPaneType::ExecutionProfileEditor => write!(f, "Execution Profile Editor"), IPaneType::GetStarted => write!(f, "GetStarted"), IPaneType::NetworkLog => write!(f, "Network Log"), IPaneType::DeferredPlaceholder => write!(f, "Placeholder"), #[cfg(test)] IPaneType::Dummy => write!(f, "Dummy"), } } } impl PaneId { fn new(pane_type: IPaneType, pane_view: &ViewHandle>) -> Self { Self(IPaneId { pane_type, pane_view_id: pane_view.id(), }) } fn new_from_ctx(pane_type: IPaneType, ctx: &ViewContext>) -> Self { Self(IPaneId { pane_type, pane_view_id: ctx.view_id(), }) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_terminal_pane_ctx(ctx: &ViewContext) -> Self { Self::new_from_ctx(IPaneType::Terminal, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_file_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::File, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_notebook_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::Notebook, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_env_var_collection_pane_ctx( ctx: &ViewContext>, ) -> Self { Self::new_from_ctx(IPaneType::EnvVarCollection, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] (environment management stub) pub fn from_environment_management_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::EnvironmentManagement, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_workflow_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::Workflow, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_code_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::Code, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_code_diff_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::CodeDiff, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_settings_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::Settings, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_ai_fact_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::AIFact, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_ai_document_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::AIDocument, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_custom_router_editor_pane_ctx( ctx: &ViewContext>, ) -> Self { Self::new_from_ctx(IPaneType::CustomRouterEditor, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`] pub fn from_execution_profile_editor_pane_ctx( ctx: &ViewContext>, ) -> Self { Self::new_from_ctx(IPaneType::ExecutionProfileEditor, ctx) } pub fn from_get_started_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::GetStarted, ctx) } /// Creates a [`PaneId`] from a [`ViewContext>`]. pub fn from_network_log_pane_ctx(ctx: &ViewContext>) -> Self { Self::new_from_ctx(IPaneType::NetworkLog, ctx) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_terminal_pane_view( terminal_pane_view: &ViewHandle, ) -> Self { Self::new(IPaneType::Terminal, terminal_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_notebook_pane_view( notebook_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::Notebook, notebook_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_file_pane_view(file_pane_view: &ViewHandle>) -> Self { Self::new(IPaneType::File, file_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_code_pane_view(code_pane_view: &ViewHandle>) -> Self { Self::new(IPaneType::Code, code_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_code_diff_pane_view( code_diff_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::CodeDiff, code_diff_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_env_var_collection_view( env_var_collection_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::EnvVarCollection, env_var_collection_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID (environment management stub). pub fn from_environment_management_pane_view( environment_management_pane_view: &ViewHandle>, ) -> Self { Self::new( IPaneType::EnvironmentManagement, environment_management_pane_view, ) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_workflow_pane_view( workflow_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::Workflow, workflow_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_settings_pane_view( settings_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::Settings, settings_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_ai_fact_pane_view(ai_fact_pane_view: &ViewHandle>) -> Self { Self::new(IPaneType::AIFact, ai_fact_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_ai_document_pane_view( ai_document_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::AIDocument, ai_document_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_custom_router_editor_pane_view( view: &ViewHandle>, ) -> Self { Self::new(IPaneType::CustomRouterEditor, view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_execution_profile_editor_pane_view( execution_profile_editor_pane_view: &ViewHandle>, ) -> Self { Self::new( IPaneType::ExecutionProfileEditor, execution_profile_editor_pane_view, ) } pub fn from_get_started_pane_view( get_started_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::GetStarted, get_started_pane_view) } /// Creates a [`PaneId`] from a [`PaneView`] entity ID. pub fn from_network_log_pane_view( network_log_pane_view: &ViewHandle>, ) -> Self { Self::new(IPaneType::NetworkLog, network_log_pane_view) } #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] pub(super) fn deferred_placeholder_pane_id() -> Self { Self(IPaneId { pane_type: IPaneType::DeferredPlaceholder, pane_view_id: galaxyui::EntityId::new(), }) } /// Creates a [`PaneId`] for a dummy pane. #[cfg(test)] pub fn dummy_pane_id() -> Self { Self(IPaneId { pane_type: IPaneType::Dummy, pane_view_id: galaxyui::EntityId::new(), }) } /// Returns a [`TerminalPaneId`] for the pane, if this is a terminal pane ID. pub fn as_terminal_pane_id(&self) -> Option { if matches!(self.0.pane_type, IPaneType::Terminal) { Some(TerminalPaneId(self.0.pane_view_id)) } else { None } } pub(crate) fn pane_type(&self) -> IPaneType { self.0.pane_type } pub(crate) fn creation_order_id(&self) -> EntityId { self.0.pane_view_id } pub fn is_terminal_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::Terminal) } pub fn is_notebook_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::Notebook) } pub fn is_code_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::Code) } pub fn is_file_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::File) } pub fn is_code_diff_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::CodeDiff) } pub fn is_environment_management_pane(&self) -> bool { matches!(self.0.pane_type, IPaneType::EnvironmentManagement) } /// Returns true if this pane contains a Warp Drive object (notebook, workflow, etc.). pub fn is_warp_drive_object_pane(&self) -> bool { matches!( self.0.pane_type, IPaneType::Notebook | IPaneType::Workflow | IPaneType::EnvVarCollection | IPaneType::AIFact ) } /// Renders the child view backing this pane. pub fn render(self, app: &AppContext) -> Box { let mut element = match self.0.pane_type { IPaneType::Terminal => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::Notebook => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::File => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::Code => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::CodeDiff => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::EnvVarCollection => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::EnvironmentManagement => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::Workflow => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::Settings => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::AIFact => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::AIDocument => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::CustomRouterEditor => ChildView::< PaneView, >::with_id(self.0.pane_view_id) .finish(), IPaneType::ExecutionProfileEditor => { ChildView::>::with_id(self.0.pane_view_id) .finish() } IPaneType::GetStarted => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::NetworkLog => { ChildView::>::with_id(self.0.pane_view_id).finish() } IPaneType::DeferredPlaceholder => galaxyui::elements::Empty::new().finish(), #[cfg(test)] IPaneType::Dummy => galaxyui::elements::Empty::new().finish(), }; if *PaneSettings::as_ref(app).focus_panes_on_hover { element = EventHandler::new(element) .on_mouse_in( move |ctx, _, _| { ctx.dispatch_typed_action(PaneGroupAction::Activate( self, ActivationReason::Hover, )); DispatchEventResult::PropagateToParent }, Some(MouseInBehavior { // Don't fire on synthetic events because we don't want to steal focus // when a user creates a new pane. fire_on_synthetic_events: false, // Don't fire when covered because we don't want to steal focus when // modals are open on top of panes. fire_when_covered: false, }), ) .finish(); } element } pub fn position_id(&self) -> String { self.to_string() } } impl Display for PaneId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Pane {}", self.0) } } #[derive(Debug, Clone, Copy)] pub enum DetachType { // Pane has been permanently closed and should have resources cleaned up. Closed, // Pane has been temporarily hidden for close. HiddenForClose, // Pane detached during a move. Moved, } pub enum ShareableLink { /// The base app url should be used for the browser url bar Base, /// The url for the active pane to use for the browser url bar Pane { url: Url }, } #[derive(Debug)] pub enum ShareableLinkError { /// An expected error occurred when attempting to get the shareable link of the active pane. /// For example the pane is not yet in a state where it has a shareable link but will soon. Expected, /// An unexpected error while trying to get the shareable link of the active pane. Unexpected(String), } /// The contents of a leaf pane. /// /// The [`PaneData`] tree references panes by their [`PaneId`], while the [`PaneGroup`] view owns /// all their contents through [`PaneContent`]. /// /// See [`BackingView`] for pub trait PaneContent: 'static { /// The corresponding identifier for this pane. fn id(&self) -> PaneId; /// Pre-attachment hook that allows panes to perform some work before attachment. /// Returns true if the pane can be attached normally, false if attachment should be prevented. fn pre_attach(&self, _group: &PaneGroup, _ctx: &mut ViewContext) -> bool { // Default implementation allows all panes to attach true } /// Attempts to attach this pane to a group, calling pre_attach first. /// Returns true if attachment succeeded, false if pre_attach prevented it. fn try_attach( &self, group: &PaneGroup, focus_handle: PaneFocusHandle, ctx: &mut ViewContext, ) -> bool { if self.pre_attach(group, ctx) { self.attach(group, focus_handle, ctx); true } else { false } } /// Callback for when this leaf pane is added to a pane group. /// /// This is called after the pane is added to the group's set of leaf panes, but before the /// new pane is focused. fn attach( &self, group: &PaneGroup, focus_handle: PaneFocusHandle, ctx: &mut ViewContext, ); /// Callback for when this leaf pane is removed from a pane group. /// /// This is called when: /// - The pane is about to be closed /// - The pane group is closed, but may be restored /// - The pane is being moved to another tab, or upgraded to its own tab fn detach(&self, group: &PaneGroup, detach_type: DetachType, ctx: &mut ViewContext); /// Snapshot this pane for session restoration. fn snapshot(&self, app: &AppContext) -> LeafContents; /// Returns whether or not application focus is within this pane. fn has_application_focus(&self, ctx: &mut ViewContext) -> bool; /// Focus this pane's contents. fn focus(&self, ctx: &mut ViewContext); /// Get the shareable link for the pane. /// /// This is called when the focused pane changes. It is used to get the link to the /// for the active pane (if there is one). This link is used to update the browser's /// url bar. fn shareable_link( &self, ctx: &mut ViewContext, ) -> Result; /// Pane-agnostic state that all panes have. fn pane_configuration(&self) -> ModelHandle; fn is_pane_being_dragged(&self, ctx: &AppContext) -> bool; } /// Trait for untyped pane contents. This is a workaround for trait upcasting being /// unstable: https://github.com/rust-lang/rust/issues/65991 pub trait AnyPaneContent { /// This pane's contents, as [`Any`], to allow downcasting. fn as_any(&self) -> &dyn Any; fn as_pane(&self) -> &dyn PaneContent; fn pre_attach(&self, group: &PaneGroup, ctx: &mut ViewContext) -> bool; fn try_attach( &self, group: &PaneGroup, focus_handle: PaneFocusHandle, ctx: &mut ViewContext, ) -> bool; } impl AnyPaneContent for T where T: PaneContent, { fn as_any(&self) -> &dyn Any { self } fn as_pane(&self) -> &dyn PaneContent { self } fn pre_attach(&self, group: &PaneGroup, ctx: &mut ViewContext) -> bool { PaneContent::pre_attach(self, group, ctx) } fn try_attach( &self, group: &PaneGroup, focus_handle: PaneFocusHandle, ctx: &mut ViewContext, ) -> bool { PaneContent::try_attach(self, group, focus_handle, ctx) } } /// A helper struct to group together pane-agnostic properties. /// Implemented as a model so that underlying panes and the generic /// views ([`PaneView`] and [`PaneHeader`]) can communicate changes /// to one another. pub struct PaneConfiguration { title: String, title_secondary: String, custom_vertical_tabs_title: Option, show_active_pane_indicator: bool, /// If true, we draw an accent border around the pane. show_accent_border: bool, /// Pane views set this when they have an open modal. We dim the pane header along with the /// pane contents. has_open_modal: bool, /// Although the pane can think its focused, we actually now allow for left and right panels to be focused so the pane should be dimmed instead dim_even_if_focused: bool, /// Extra left inset (in px) for the pane header's left-side controls. /// Used to shift controls right to make room for a floating button overlay. pub header_left_inset: f32, } impl Entity for PaneConfiguration { type Event = PaneConfigurationEvent; } impl PaneConfiguration { pub fn new(title: impl Into) -> Self { Self { title: title.into(), title_secondary: String::from(""), custom_vertical_tabs_title: None, show_active_pane_indicator: false, show_accent_border: false, has_open_modal: false, dim_even_if_focused: false, header_left_inset: 0., } } pub fn title(&self) -> &str { &self.title } pub fn title_secondary(&self) -> &str { &self.title_secondary } pub fn custom_vertical_tabs_title(&self) -> Option<&str> { self.custom_vertical_tabs_title.as_deref() } pub fn dim_even_if_focused(&self) -> bool { self.dim_even_if_focused } pub fn set_show_active_pane_indicator( &mut self, show_active_pane_indicator: bool, ctx: &mut ModelContext, ) { self.show_active_pane_indicator = show_active_pane_indicator; ctx.emit(PaneConfigurationEvent::ShowActivePaneIndicatorUpdated); ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } pub fn set_title(&mut self, title: impl Into, ctx: &mut ModelContext) { let title = title.into(); if self.title != title { self.title = title; ctx.emit(PaneConfigurationEvent::TitleUpdated); ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } } pub fn set_title_secondary( &mut self, secondary: impl Into, ctx: &mut ModelContext, ) { let secondary = secondary.into(); if self.title_secondary != secondary { self.title_secondary = secondary; ctx.emit(PaneConfigurationEvent::TitleUpdated); ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } } pub fn set_custom_vertical_tabs_title( &mut self, title: impl Into, ctx: &mut ModelContext, ) { let title = title.into(); let title = title.trim(); let title = (!title.is_empty()).then(|| title.to_string()); if self.custom_vertical_tabs_title != title { self.custom_vertical_tabs_title = title; ctx.emit(PaneConfigurationEvent::VerticalTabsTitleUpdated); } } pub fn clear_custom_vertical_tabs_title(&mut self, ctx: &mut ModelContext) { if self.custom_vertical_tabs_title.take().is_some() { ctx.emit(PaneConfigurationEvent::VerticalTabsTitleUpdated); } } pub fn set_dim_even_if_focused( &mut self, dim_even_if_focused: bool, ctx: &mut ModelContext, ) { if self.dim_even_if_focused != dim_even_if_focused { self.dim_even_if_focused = dim_even_if_focused; ctx.emit(PaneConfigurationEvent::DimEvenIfFocusedUpdated); } } pub fn set_show_accent_border( &mut self, show_accent_border: bool, ctx: &mut ModelContext, ) { self.show_accent_border = show_accent_border; ctx.emit(PaneConfigurationEvent::ShowAccentBorderUpdated); } pub fn set_has_open_modal(&mut self, has_open_modal: bool, ctx: &mut ModelContext) { self.has_open_modal = has_open_modal; ctx.emit(PaneConfigurationEvent::OpenModalUpdated); ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } pub fn refresh_pane_header_overflow_menu_items(&mut self, ctx: &mut ModelContext) { ctx.emit(PaneConfigurationEvent::RefreshPaneHeaderOverflowMenuItems); ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } /// Sets the shareable object in the current pane. If `None`, the share button is removed. pub fn set_shareable_object( &mut self, shareable_object: Option, ctx: &mut ModelContext, ) { ctx.emit(PaneConfigurationEvent::ShareableObjectChanged( shareable_object, )); } pub fn toggle_sharing_dialog( &mut self, source: SharingDialogSource, ctx: &mut ModelContext, ) { ctx.emit(PaneConfigurationEvent::ToggleSharingDialog(source)); } pub fn open_sharing_qr_code( &mut self, source: SharingDialogSource, ctx: &mut ModelContext, ) { ctx.emit(PaneConfigurationEvent::OpenSharingQrCode(source)); } /// Notifies that the header content has changed and the pane header should re-render. /// Use this when the backing view's state has changed in a way that affects the header /// content returned by `render_header_content()`. pub fn notify_header_content_changed(&mut self, ctx: &mut ModelContext) { ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } pub fn set_header_left_inset(&mut self, inset: f32, ctx: &mut ModelContext) { if (self.header_left_inset - inset).abs() > f32::EPSILON { self.header_left_inset = inset; ctx.emit(PaneConfigurationEvent::HeaderContentChanged); } } } pub enum PaneConfigurationEvent { TitleUpdated, VerticalTabsTitleUpdated, ShowActivePaneIndicatorUpdated, RenderElementFnUpdated, ShowAccentBorderUpdated, OpenModalUpdated, RefreshPaneHeaderOverflowMenuItems, ShareableObjectChanged(Option), ToggleSharingDialog(SharingDialogSource), OpenSharingQrCode(SharingDialogSource), DimEvenIfFocusedUpdated, /// The header content has changed and should be re-rendered. /// This is used when the backing view's state changes in a way that /// affects what `render_header_content()` returns. HeaderContentChanged, } /// Event emitted when the pane stack changes. pub enum PaneStackEvent { /// A view was added to the stack. ViewAdded(ViewHandle

), /// A view was removed from the stack. ViewRemoved(ViewHandle

), } /// A navigation stack of backing views for a pane. /// /// This model allows panes to support a stack of views, where only the topmost /// view is active/rendered. Views can be pushed onto the stack and popped to /// return to the previous view. /// /// The stack is guaranteed to always have at least one view. /// /// Each view in the stack can have associated data of type [`BackingView::AssociatedData`]. /// This is useful for storing per-view metadata that should be tied to the view's own lifetime. pub struct PaneStack { /// The stack of backing views with associated data. The last element is the active (topmost) view. children: vec1::Vec1<(P::AssociatedData, ViewHandle

)>, } impl Entity for PaneStack

{ type Event = PaneStackEvent

; } impl> PaneStack

{ /// Pushes a new view onto the stack, making it the active view. This is a convenience wrapper /// around [`Self::push`] for panes with no associated data. pub fn push_view(&mut self, view: ViewHandle

, ctx: &mut ModelContext) { self.push((), view, ctx); } } impl PaneStack

{ /// Creates a new pane stack with a single initial view and its associated data. pub fn new( data: P::AssociatedData, initial_view: ViewHandle

, ctx: &mut ModelContext, ) -> Self { let weak_handle = ctx.handle(); initial_view.update(ctx, |view, ctx| { view.set_pane_stack(weak_handle, ctx); }); Self { children: vec1::vec1![(data, initial_view)], } } /// Returns the topmost (active) view in the stack. pub fn active_view(&self) -> &ViewHandle

{ &self.children.last().1 } /// Returns the associated data for the topmost (active) view in the stack. pub fn active_data(&self) -> &P::AssociatedData { &self.children.last().0 } /// Returns a mutable reference to the associated data for the topmost (active) view. pub fn active_data_mut(&mut self) -> &mut P::AssociatedData { &mut self.children.last_mut().0 } /// Returns all views in the stack. pub fn views(&self) -> impl Iterator> { self.children.iter().map(|(_, view)| view) } /// Returns all entries in the stack as (data, view) pairs. pub fn entries(&self) -> &[(P::AssociatedData, ViewHandle

)] { &self.children } /// Returns the depth (number of views) in the stack. pub fn depth(&self) -> usize { self.children.len() } /// Pushes a new view with associated data onto the stack, making it the active view. pub fn push( &mut self, data: P::AssociatedData, view: ViewHandle

, ctx: &mut ModelContext, ) { let weak_handle = ctx.handle(); view.update(ctx, |view, ctx| { view.set_pane_stack(weak_handle, ctx); }); self.children.push((data, view.clone())); ctx.emit(PaneStackEvent::ViewAdded(view)); } /// Pops the topmost view from the stack. /// Returns the popped view and its associated data, or `None` if the stack has only one view. pub fn pop( &mut self, ctx: &mut ModelContext, ) -> Option<(P::AssociatedData, ViewHandle

)> { let popped = self.children.pop().ok()?; ctx.emit(PaneStackEvent::ViewRemoved(popped.1.clone())); Some(popped) } } /// Toolbelt buttons appear in a group together on the left-hand side of the pane header. pub struct ToolbeltButton { pub action_button: ViewHandle, } /// The view that is rendered as the pane's contents. pub trait BackingView: View { type PaneHeaderOverflowMenuAction: Action + Clone; type CustomAction: Action + Clone; /// Associated data type stored with each view in the [`PaneStack`]. type AssociatedData: 'static + Send; /// Processes the corresponding action when one of the /// overflow menu items is selected. Allows implementers /// to add pre-/post-processing logic (e.g. telemetry). /// // Note: even if the [`PaneHeaderOverflowMenuAction`] was [`TypedActionView::Action`] // (assuming [`TypedActionView`] was one of the trait bounds for [`BackingView`]), // the pane header cannot simply dispatch the action because the underlying view // is not one of its ancestors and thus won't be in the responder chain. fn handle_pane_header_overflow_menu_action( &mut self, action: &Self::PaneHeaderOverflowMenuAction, ctx: &mut ViewContext, ); /// Handles a CustomAction in the pane header being clicked. fn handle_custom_action( &mut self, _custom_action: &Self::CustomAction, _ctx: &mut ViewContext, ) { } /// Closes the pane. fn close(&mut self, ctx: &mut ViewContext); /// Focus the pane contents. This is similar to [`PaneContent::focus`], but called from within /// the pane when focus shifts from the header or pane controls to the pane contents. fn focus_contents(&mut self, ctx: &mut ViewContext); /// The menu items that should be displayed in the pane header's overflow menu. /// When these should be updated, fire the [`PaneConfigurationEvent::RefreshPaneHeaderOverflowMenuItems`] /// and the pane header will fetch the new items via this API. fn pane_header_overflow_menu_items( &self, _ctx: &AppContext, ) -> Vec> { vec![] } fn on_pane_header_overflow_menu_toggled( &mut self, _is_open: bool, _ctx: &mut ViewContext, ) { } /// Action buttons rendered on the left side of the pane header in the returned order in the vec. fn pane_header_toolbelt_buttons(&self, _ctx: &AppContext) -> Vec { vec![] } fn should_render_header(&self, _app: &AppContext) -> bool { true } /// Called when this view is added to a [`PaneStack`]. Views are given a handle to their owning stack so that they can: /// * Check if they're part of a stack /// * Push/pop views from the stack /// /// Because the pane stack holds a strong reference to each view, views *must not* store a strong `ModelHandle` to the // stack. fn set_pane_stack( &mut self, _pane_stack: WeakModelHandle>, _ctx: &mut ViewContext, ) where Self: Sized, { } /// Returns pane header content for this view. /// /// The framework handles wrapping the returned content with draggable behavior, /// so implementations don't need to worry about drag-and-drop. /// /// # Return values /// - `HeaderContent::Standard { .. }`: Standard header with customization points /// - `HeaderContent::Custom(element)`: Fully custom header, auto-wrapped with draggable /// - `HeaderContent::CustomWithExplicitDraggable(element)`: Custom header where the view /// is responsible for calling `PaneHeader::render_pane_header_draggable()` on appropriate elements fn render_header_content( &self, ctx: &view::HeaderRenderContext<'_>, app: &AppContext, ) -> view::HeaderContent; /// Set the handle that tracks whether the pane containing this view is focused. fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, ctx: &mut ViewContext); } /// Common event type for all panes. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PaneEvent { Close, CloseAndFocus { pane_to_focus: PaneId, }, SplitLeft(Option), SplitRight(Option), SplitUp(Option), SplitDown(Option), ToggleMaximized, /// Make this pane the focused pane. FocusSelf, FocusActiveSession, /// The session-restoration state for this pane must be updated. AppStateChanged, /// Repo for this pane's terminal has changed RepoChanged, /// A remote server resolved the repo root for a session in this pane. RemoteRepoNavigated { remote_path: RemotePath, }, /// Split the current pane into two. If `initial_query` is `Some` fill the new pane's input with /// its value. NewPaneInAIMode { initial_query: Option, }, ClearHoveredTabIndex, #[cfg(feature = "local_fs")] ReplaceWithCodePane { path: LocalOrRemotePath, source: Option, }, #[cfg(feature = "local_fs")] ReplaceWithFilePane { path: LocalOrRemotePath, source: Option, }, }