feat(window): support des fenêtres plugin-hébergées (backend)

Étend le port/usecases window et layout.customPluginLayout pour ouvrir et
piloter des fenêtres OS hébergeant un layout plugin, en réutilisant
contributes.layouts plutôt qu'un système de fenêtres parallèle. Câble la
commande Tauri et l'état app-tauri correspondants.

Cargo test -p domain --test window : 5/5
Cargo test -p application --test window_usecases : 7/7
Cargo test -p infrastructure --test window_state_store : 1/1
Cargo test -p app-tauri view_window_tests : 8/8

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 00:39:42 +02:00
parent 3ea1d58b38
commit 551eb09ad2
14 changed files with 884 additions and 95 deletions

View File

@ -4,6 +4,7 @@
mod usecases;
pub use usecases::{
MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput, RestoreOpenWindows,
MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput, OpenPluginLayoutWindow,
OpenPluginLayoutWindowInput, OpenPluginLayoutWindowOutput, RestoreOpenWindows,
RestoreOpenWindowsOutput, SnapshotOpenWindows, SnapshotOpenWindowsInput,
};

View File

@ -9,10 +9,18 @@ use std::sync::Arc;
use domain::ids::{TabId, WindowId};
use std::collections::HashSet;
use domain::layout::{PersistedWindowKind, PersistedWindowState, WindowStateSnapshot, Workspace};
use domain::ports::{IdGenerator, ProjectStore, WindowStateStore};
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)]
@ -116,15 +124,27 @@ pub struct RestoreOpenWindowsOutput {
pub struct RestoreOpenWindows {
windows: Arc<dyn WindowStateStore>,
_projects: Arc<dyn ProjectStore>,
packages: Arc<dyn PluginPackageStore>,
registry: Arc<dyn PluginRegistryStore>,
validator: Arc<dyn PluginManifestValidator>,
}
impl RestoreOpenWindows {
/// Builds the use case from its ports.
#[must_use]
pub fn new(windows: Arc<dyn WindowStateStore>, projects: Arc<dyn ProjectStore>) -> Self {
pub fn new(
windows: Arc<dyn WindowStateStore>,
projects: Arc<dyn ProjectStore>,
packages: Arc<dyn PluginPackageStore>,
registry: Arc<dyn PluginRegistryStore>,
validator: Arc<dyn PluginManifestValidator>,
) -> Self {
Self {
windows,
_projects: projects,
packages,
registry,
validator,
}
}
@ -154,9 +174,103 @@ impl RestoreOpenWindows {
}
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<dyn PluginPackageStore>,
registry: Arc<dyn PluginRegistryStore>,
validator: Arc<dyn PluginManifestValidator>,
}
impl OpenPluginLayoutWindow {
/// Builds the use case from plugin runtime ports.
#[must_use]
pub fn new(
packages: Arc<dyn PluginPackageStore>,
registry: Arc<dyn PluginRegistryStore>,
validator: Arc<dyn PluginManifestValidator>,
) -> 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<OpenPluginLayoutWindowOutput, AppError> {
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,
})
}
}