//! Window/tab use cases (ARCHITECTURE §6, §10; L10). //! //! [`MoveTabToNewWindow`] detaches a tab into a new OS window. The topology //! change is the pure [`Workspace::move_tab_to_new_window`] domain operation; the //! use case only loads/persists the workspace and mints the new window id. use std::sync::Arc; use domain::ids::{TabId, WindowId}; use std::collections::HashSet; use domain::layout::{ PersistedPluginLayoutWindow, PersistedWindowKind, PersistedWindowState, WindowStateSnapshot, Workspace, }; use domain::ports::{ IdGenerator, PluginManifestValidator, PluginPackageStore, PluginRegistryStore, ProjectStore, WindowStateStore, }; use domain::{PluginId, PluginLayoutType}; use crate::error::AppError; use crate::plugin::{ensure_runtime_plugin_layout_contribution, PluginRuntimeLayoutContribution}; /// Input for [`MoveTabToNewWindow::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct MoveTabToNewWindowInput { /// The tab to detach into its own new window. pub tab_id: TabId, } /// Output of [`MoveTabToNewWindow::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct MoveTabToNewWindowOutput { /// The id minted for the new window. pub new_window_id: WindowId, /// The resulting workspace (already persisted). pub workspace: Workspace, } /// Detaches a tab into a freshly-created window and persists the workspace. pub struct MoveTabToNewWindow { store: Arc, ids: Arc, } impl MoveTabToNewWindow { /// Builds the use case from its ports. #[must_use] pub fn new(store: Arc, ids: Arc) -> Self { Self { store, ids } } /// Executes the detach. /// /// # Errors /// - [`AppError::NotFound`] if the tab is not in the workspace, /// - [`AppError::Invalid`] if the resulting window is invalid, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: MoveTabToNewWindowInput, ) -> Result { let workspace = self.store.load_workspace().await?; let new_window_id = WindowId::from_uuid(self.ids.new_uuid()); let workspace = workspace .move_tab_to_new_window(input.tab_id, new_window_id) .map_err(|e| match e { domain::layout::LayoutError::TabNotFound(t) => { AppError::NotFound(format!("tab {t}")) } other => AppError::Invalid(other.to_string()), })?; self.store.save_workspace(&workspace).await?; Ok(MoveTabToNewWindowOutput { new_window_id, workspace, }) } } /// Input for [`SnapshotOpenWindows::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct SnapshotOpenWindowsInput { /// Open windows captured by the presentation adapter. pub windows: Vec, } /// Persists the latest open OS window snapshot. pub struct SnapshotOpenWindows { store: Arc, } impl SnapshotOpenWindows { /// Builds the use case from its store port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Saves a versioned window snapshot. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: SnapshotOpenWindowsInput) -> Result<(), AppError> { self.store .save_window_state(&WindowStateSnapshot::new(input.windows)) .await?; Ok(()) } } /// Output of [`RestoreOpenWindows::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct RestoreOpenWindowsOutput { /// Restorable windows, deduplicated by stable label. pub windows: Vec, } /// Loads and filters the latest persisted OS window snapshot. pub struct RestoreOpenWindows { windows: Arc, _projects: Arc, packages: Arc, registry: Arc, validator: Arc, } impl RestoreOpenWindows { /// Builds the use case from its ports. #[must_use] pub fn new( windows: Arc, projects: Arc, packages: Arc, registry: Arc, validator: Arc, ) -> Self { Self { windows, _projects: projects, packages, registry, validator, } } /// Loads restorable windows. /// /// Views are panel-only: their persisted project id, including legacy /// `view--` identities, is ignored. Duplicate labels are ignored /// after the first occurrence. /// /// # Errors /// [`AppError::Store`] on snapshot read failure. pub async fn execute(&self) -> Result { let snapshot = self.windows.load_window_state().await?; let mut seen = HashSet::new(); let mut windows = Vec::new(); for window in snapshot.windows { if window.label.is_empty() || !seen.insert(window.label.clone()) { continue; } match window.kind { PersistedWindowKind::Main => windows.push(window), PersistedWindowKind::View => { if window.panel.is_none() { continue; } windows.push(window); } PersistedWindowKind::PluginLayout => { let Some(surface) = &window.plugin_layout else { continue; }; if self.validate_plugin_layout_surface(surface).await.is_ok() { windows.push(window); } } } } Ok(RestoreOpenWindowsOutput { windows }) } async fn validate_plugin_layout_surface( &self, surface: &PersistedPluginLayoutWindow, ) -> Result<(), AppError> { ensure_runtime_plugin_layout_contribution( self.packages.as_ref(), self.registry.as_ref(), self.validator.as_ref(), &surface.plugin_id, &surface.layout_type, ) .await .map(|_| ()) } } /// Input for [`OpenPluginLayoutWindow::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct OpenPluginLayoutWindowInput { /// Provider plugin id. pub plugin_id: PluginId, /// Layout type declared by the provider plugin. pub layout_type: PluginLayoutType, /// Opaque plugin-owned initial/window state. pub state: serde_json::Value, } /// Output of [`OpenPluginLayoutWindow::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct OpenPluginLayoutWindowOutput { /// Runtime contribution that was validated. pub contribution: PluginRuntimeLayoutContribution, /// Persistable plugin layout surface. pub surface: PersistedPluginLayoutWindow, } /// Validates a plugin layout contribution before a detached OS window hosts it. pub struct OpenPluginLayoutWindow { packages: Arc, registry: Arc, validator: Arc, } impl OpenPluginLayoutWindow { /// Builds the use case from plugin runtime ports. #[must_use] pub fn new( packages: Arc, registry: Arc, validator: Arc, ) -> Self { Self { packages, registry, validator, } } /// Executes the validation. /// /// # Errors /// [`AppError::Invalid`] when the plugin is inactive or does not declare the /// requested layout contribution; other errors bubble from plugin loading. pub async fn execute( &self, input: OpenPluginLayoutWindowInput, ) -> Result { let contribution = ensure_runtime_plugin_layout_contribution( self.packages.as_ref(), self.registry.as_ref(), self.validator.as_ref(), &input.plugin_id, &input.layout_type, ) .await?; let surface = PersistedPluginLayoutWindow { plugin_id: input.plugin_id, layout_type: input.layout_type, state: input.state, }; Ok(OpenPluginLayoutWindowOutput { contribution, surface, }) } }