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:
@ -35,8 +35,8 @@ use std::sync::Arc;
|
||||
|
||||
use application::{AppError, GetAppExitWorkGuardStateInput, SnapshotOpenWindowsInput};
|
||||
use domain::{
|
||||
PersistedMonitorState, PersistedWindowKind, PersistedWindowPosition, PersistedWindowSize,
|
||||
PersistedWindowState, ProjectId,
|
||||
PersistedMonitorState, PersistedPluginLayoutWindow, PersistedWindowKind,
|
||||
PersistedWindowPosition, PersistedWindowSize, PersistedWindowState, ProjectId,
|
||||
};
|
||||
use tauri::{
|
||||
Emitter, Manager, PhysicalPosition, PhysicalSize, WebviewUrl, WebviewWindow,
|
||||
@ -383,6 +383,7 @@ pub fn run() {
|
||||
commands::set_focused_project,
|
||||
commands::get_focused_project,
|
||||
commands::list_open_view_windows,
|
||||
commands::open_plugin_layout_window,
|
||||
commands::open_view_window,
|
||||
commands::close_view_window,
|
||||
commands::move_tab_to_new_window,
|
||||
@ -550,12 +551,23 @@ fn snapshot_open_webview_windows(handle: &tauri::AppHandle) -> Vec<PersistedWind
|
||||
handle
|
||||
.webview_windows()
|
||||
.into_iter()
|
||||
.filter_map(|(label, window)| snapshot_webview_window(&label, &window))
|
||||
.filter_map(|(label, window)| snapshot_webview_window(handle, &label, &window))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn snapshot_webview_window(label: &str, window: &WebviewWindow) -> Option<PersistedWindowState> {
|
||||
let (kind, panel, project_id, url) = persisted_window_identity(label)?;
|
||||
fn snapshot_webview_window(
|
||||
handle: &tauri::AppHandle,
|
||||
label: &str,
|
||||
window: &WebviewWindow,
|
||||
) -> Option<PersistedWindowState> {
|
||||
let (kind, panel, project_id, mut plugin_layout, url) =
|
||||
persisted_window_identity_from_label(label)?;
|
||||
if kind == PersistedWindowKind::PluginLayout {
|
||||
plugin_layout = handle
|
||||
.try_state::<AppState>()
|
||||
.and_then(|state| state.get_plugin_window_surface(label))
|
||||
.or(plugin_layout);
|
||||
}
|
||||
let outer_position = window
|
||||
.outer_position()
|
||||
.ok()
|
||||
@ -586,6 +598,7 @@ fn snapshot_webview_window(label: &str, window: &WebviewWindow) -> Option<Persis
|
||||
kind,
|
||||
panel,
|
||||
project_id,
|
||||
plugin_layout,
|
||||
url,
|
||||
visible: window.is_visible().unwrap_or(true),
|
||||
maximized: window.is_maximized().unwrap_or(false),
|
||||
@ -597,16 +610,28 @@ fn snapshot_webview_window(label: &str, window: &WebviewWindow) -> Option<Persis
|
||||
})
|
||||
}
|
||||
|
||||
fn persisted_window_identity(
|
||||
fn persisted_window_identity_from_label(
|
||||
label: &str,
|
||||
) -> Option<(
|
||||
PersistedWindowKind,
|
||||
Option<String>,
|
||||
Option<ProjectId>,
|
||||
Option<PersistedPluginLayoutWindow>,
|
||||
Option<String>,
|
||||
)> {
|
||||
if label == "main" {
|
||||
return Some((PersistedWindowKind::Main, None, None, None));
|
||||
return Some((PersistedWindowKind::Main, None, None, None, None));
|
||||
}
|
||||
|
||||
if let Some(surface) = commands::plugin_layout_window_from_label(label) {
|
||||
let url = commands::plugin_layout_window_url(&surface);
|
||||
return Some((
|
||||
PersistedWindowKind::PluginLayout,
|
||||
None,
|
||||
None,
|
||||
Some(surface),
|
||||
Some(url),
|
||||
));
|
||||
}
|
||||
|
||||
let panel = persisted_view_identity_from_label(label)?;
|
||||
@ -614,6 +639,7 @@ fn persisted_window_identity(
|
||||
PersistedWindowKind::View,
|
||||
Some(panel.as_str().to_owned()),
|
||||
None,
|
||||
None,
|
||||
Some(commands::view_window_url(panel)),
|
||||
))
|
||||
}
|
||||
@ -650,6 +676,9 @@ fn restore_open_webview_windows(handle: &tauri::AppHandle) {
|
||||
PersistedWindowKind::View => {
|
||||
restore_view_window(handle, &window_state);
|
||||
}
|
||||
PersistedWindowKind::PluginLayout => {
|
||||
restore_plugin_layout_window(handle, &window_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -696,6 +725,51 @@ fn restore_view_window(handle: &tauri::AppHandle, state: &PersistedWindowState)
|
||||
commands::emit_view_window_lifecycle(handle, "opened", panel, &label);
|
||||
}
|
||||
|
||||
fn restore_plugin_layout_window(handle: &tauri::AppHandle, state: &PersistedWindowState) {
|
||||
let Some(surface) = state
|
||||
.plugin_layout
|
||||
.clone()
|
||||
.or_else(|| commands::plugin_layout_window_from_label(&state.label))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let label = commands::plugin_layout_window_label(&surface.plugin_id, &surface.layout_type);
|
||||
if handle.get_webview_window(&label).is_some() {
|
||||
return;
|
||||
}
|
||||
let url = commands::plugin_layout_window_url(&surface);
|
||||
|
||||
let Ok(window) = WebviewWindowBuilder::new(handle, &label, WebviewUrl::App(url.into()))
|
||||
.title(format!("IdeA - {}", surface.layout_type.as_str()))
|
||||
.inner_size(1120.0, 760.0)
|
||||
.min_inner_size(720.0, 480.0)
|
||||
.resizable(true)
|
||||
.maximizable(true)
|
||||
.minimizable(true)
|
||||
.closable(true)
|
||||
.decorations(true)
|
||||
.visible(state.visible)
|
||||
.build()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(app_state) = handle.try_state::<AppState>() {
|
||||
app_state.set_plugin_window_surface(label.clone(), surface);
|
||||
}
|
||||
let event_app = handle.clone();
|
||||
let event_label = label.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||
if let Some(state) = event_app.try_state::<AppState>() {
|
||||
state.clear_plugin_window_surface(&event_label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
apply_persisted_window_state(handle, &window, state);
|
||||
}
|
||||
|
||||
fn apply_persisted_window_state(
|
||||
handle: &tauri::AppHandle,
|
||||
window: &WebviewWindow,
|
||||
@ -760,8 +834,8 @@ mod tests {
|
||||
use super::plugin_workspace_invoke_handler;
|
||||
use super::{
|
||||
apply_main_close_decision, confirm_next_main_window_close, consume_exit_guard_confirmation,
|
||||
decide_main_close_action, persisted_view_identity_from_label, persisted_window_identity,
|
||||
should_install_exit_guard, MainCloseAction,
|
||||
decide_main_close_action, persisted_view_identity_from_label,
|
||||
persisted_window_identity_from_label, should_install_exit_guard, MainCloseAction,
|
||||
};
|
||||
use super::{should_close_with_main_window, PersistedWindowKind};
|
||||
use application::AppExitWorkGuardState;
|
||||
@ -889,18 +963,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn persisted_identity_accepts_main_and_stable_view_labels() {
|
||||
let (kind, panel, project_id, url) = persisted_window_identity("main").unwrap();
|
||||
let (kind, panel, project_id, plugin_layout, url) =
|
||||
persisted_window_identity_from_label("main").unwrap();
|
||||
assert_eq!(kind, PersistedWindowKind::Main);
|
||||
assert!(panel.is_none());
|
||||
assert!(project_id.is_none());
|
||||
assert!(plugin_layout.is_none());
|
||||
assert!(url.is_none());
|
||||
|
||||
let (kind, panel, project_id, url) =
|
||||
persisted_window_identity("view-tickets-0000000000000000000000000000002a").unwrap();
|
||||
let (kind, panel, project_id, plugin_layout, url) =
|
||||
persisted_window_identity_from_label("view-tickets-0000000000000000000000000000002a")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(kind, PersistedWindowKind::View);
|
||||
assert_eq!(panel.as_deref(), Some("tickets"));
|
||||
assert!(project_id.is_none());
|
||||
assert!(plugin_layout.is_none());
|
||||
assert_eq!(url.as_deref(), Some("index.html?panel=tickets"));
|
||||
assert_eq!(
|
||||
persisted_view_identity_from_label("view-tickets-0000000000000000000000000000002a")
|
||||
@ -912,21 +990,45 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn persisted_identity_accepts_new_panel_only_view_labels() {
|
||||
let (kind, panel, project_id, url) = persisted_window_identity("view-agents").unwrap();
|
||||
let (kind, panel, project_id, plugin_layout, url) =
|
||||
persisted_window_identity_from_label("view-agents").unwrap();
|
||||
assert_eq!(kind, PersistedWindowKind::View);
|
||||
assert_eq!(panel.as_deref(), Some("agents"));
|
||||
assert!(project_id.is_none());
|
||||
assert!(plugin_layout.is_none());
|
||||
assert_eq!(url.as_deref(), Some("index.html?panel=agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_identity_accepts_plugin_layout_view_labels() {
|
||||
let plugin_id = domain::PluginId::new("dev.idea.android-plugin").unwrap();
|
||||
let layout_type = domain::PluginLayoutType::new("idea-android.health").unwrap();
|
||||
let label = crate::commands::plugin_layout_window_label(&plugin_id, &layout_type);
|
||||
|
||||
let (kind, panel, project_id, plugin_layout, url) =
|
||||
persisted_window_identity_from_label(&label).unwrap();
|
||||
|
||||
assert_eq!(kind, PersistedWindowKind::PluginLayout);
|
||||
assert!(panel.is_none());
|
||||
assert!(project_id.is_none());
|
||||
let plugin_layout = plugin_layout.unwrap();
|
||||
assert_eq!(plugin_layout.plugin_id, plugin_id);
|
||||
assert_eq!(plugin_layout.layout_type, layout_type);
|
||||
assert!(url
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.starts_with("index.html?pluginLayout=1&"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_identity_filters_unknown_or_headless_labels() {
|
||||
assert!(persisted_window_identity("mcp-server").is_none());
|
||||
assert!(persisted_window_identity("settings").is_none());
|
||||
assert!(
|
||||
persisted_window_identity("view-unknown-0000000000000000000000000000002a").is_none()
|
||||
);
|
||||
assert!(persisted_window_identity("view-tickets-not-a-project").is_none());
|
||||
assert!(persisted_window_identity_from_label("mcp-server").is_none());
|
||||
assert!(persisted_window_identity_from_label("settings").is_none());
|
||||
assert!(persisted_window_identity_from_label(
|
||||
"view-unknown-0000000000000000000000000000002a"
|
||||
)
|
||||
.is_none());
|
||||
assert!(persisted_window_identity_from_label("view-tickets-not-a-project").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user