mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
Problem: floating panels lived inside the main window only, so on a two-screen setup no panel could go onto the second monitor (#486). Fix: panel menu > Move Panel to New Window (and the agent/control command `window.panelWindow {panel, window?}`) undocks the panel if needed and shows its floating group in a native egui viewport the user can drag to any screen; Move Panel to Main Window or closing the window brings it back. The window's place and size are kept in the workspace (sanitised against hostile values) and restored on the next start. Browser and test builds, which embed viewports, show it as an in-app window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
5.0 KiB
Rust
88 lines
5.0 KiB
Rust
//! Multiple monitors (#486): a panel moves out of the main window into a native window of its
|
|
//! own (Move Panel to New Window), which can be placed on another screen, and back.
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_ui_egui::EffectcraftApp;
|
|
use effectcraft_ui_egui::dock::{PanelKind, PanelWindow};
|
|
use egui::vec2;
|
|
use egui_kittest::Harness;
|
|
use serde_json::json;
|
|
|
|
fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, params: serde_json::Value) -> Result<serde_json::Value, String> {
|
|
effectcraft_ui_egui::menus::invoke(app, ctx, "window.panelWindow", params)
|
|
}
|
|
|
|
#[test]
|
|
fn panel_window_command_moves_a_docked_panel_into_its_own_window_and_back() {
|
|
let mut app = EffectcraftApp::new(Session::default());
|
|
let ctx = egui::Context::default();
|
|
assert!(app.ui.dock.contains(PanelKind::Project));
|
|
assert_eq!(invoke(&mut app, &ctx, json!({"panel": "Project", "window": true})).unwrap(), json!({"window": true}));
|
|
// Undocked first, then given a window of its own.
|
|
assert!(!app.ui.dock.contains(PanelKind::Project));
|
|
assert!(app.ui.floating.iter().any(|group| group.panels == [PanelKind::Project]));
|
|
assert!(app.in_panel_window(PanelKind::Project));
|
|
let window = app.ui.panel_windows.iter().find(|w| w.panel == PanelKind::Project).copied().unwrap();
|
|
assert_eq!(window.rect, PanelWindow::sanitized(window.rect));
|
|
// Asking again changes nothing; no `window` toggles.
|
|
assert_eq!(invoke(&mut app, &ctx, json!({"panel": "Project", "window": true})).unwrap(), json!({"window": true}));
|
|
assert_eq!(app.ui.panel_windows.len(), 1);
|
|
assert_eq!(invoke(&mut app, &ctx, json!({"panel": "Project"})).unwrap(), json!({"window": false}));
|
|
// Back in the main window, still floating where it was.
|
|
assert!(!app.in_panel_window(PanelKind::Project));
|
|
assert!(app.ui.panel_windows.is_empty());
|
|
assert!(app.ui.floating.iter().any(|group| group.panels == [PanelKind::Project]));
|
|
// The serialized panel form works too.
|
|
assert_eq!(invoke(&mut app, &ctx, json!({"panel": PanelKind::Project})).unwrap(), json!({"window": true}));
|
|
// Bad requests are errors and change nothing.
|
|
let before = app.ui.panel_windows.clone();
|
|
for params in [json!({}), json!({"panel": "Nope"}), json!({"panel": 7}), json!({"panel": "Project", "window": "yes"}), json!({"panel": {"Viewer": 9}})] {
|
|
assert!(invoke(&mut app, &ctx, params.clone()).is_err(), "{params} must be rejected");
|
|
}
|
|
assert_eq!(app.ui.panel_windows, before);
|
|
}
|
|
|
|
#[test]
|
|
fn a_panel_in_its_own_window_leaves_the_main_window() {
|
|
let mut app = EffectcraftApp::new(Session::default());
|
|
app.ui.start_screen = false;
|
|
let mut h = Harness::builder().with_size(vec2(1200.0, 760.0)).build_eframe(move |_| app);
|
|
h.run_steps(3);
|
|
let ctx = h.ctx.clone();
|
|
invoke(h.state_mut(), &ctx, json!({"panel": "Project", "window": true})).unwrap();
|
|
h.run_steps(3);
|
|
// No in-window floating group is drawn for it (no move strip, no floating area)...
|
|
assert!(!h.state().auto.elements.iter().any(|e| e.id.starts_with("panel.float.")));
|
|
let area = egui::Id::new(("floating-panel", PanelKind::Project.id()));
|
|
assert!(!h.ctx.memory(|m| m.layer_ids().any(|layer| layer.id == area && m.areas().is_visible(&layer))));
|
|
// ...the window draws it (here embedded: the test harness has no native windows).
|
|
assert!(h.state().auto.elements.iter().any(|e| e.id == "panel.Project"));
|
|
// Docking it back into the main window closes the window entry.
|
|
let ctx = h.ctx.clone();
|
|
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.dockPanel", json!({"panel": "Project", "anchor": "Timeline"})).unwrap();
|
|
h.run_steps(2);
|
|
assert!(h.state().ui.panel_windows.is_empty());
|
|
assert!(!h.state().in_panel_window(PanelKind::Project));
|
|
}
|
|
|
|
#[test]
|
|
fn panel_windows_survive_restart_with_hostile_places_made_safe() {
|
|
use effectcraft_engine::config::MemoryConfig;
|
|
use std::sync::Arc;
|
|
let store = Arc::new(MemoryConfig::default());
|
|
let mut app = EffectcraftApp::new(Session { config: Some(store.clone()), ..Default::default() });
|
|
let ctx = egui::Context::default();
|
|
invoke(&mut app, &ctx, json!({"panel": "Project", "window": true})).unwrap();
|
|
let hostile = [1.0e9, -3.0e12, 1.0e9, 0.5];
|
|
if let Some(w) = app.ui.panel_windows.first_mut() {
|
|
w.rect = hostile;
|
|
}
|
|
// A window for a panel that doesn't float is dropped on load.
|
|
app.ui.panel_windows.push(PanelWindow { panel: PanelKind::Timeline, rect: [10.0, 10.0, 300.0, 200.0] });
|
|
eframe::App::on_exit(&mut app);
|
|
let restarted = EffectcraftApp::new(Session { config: Some(store), ..Default::default() });
|
|
assert_eq!(restarted.ui.panel_windows, vec![PanelWindow { panel: PanelKind::Project, rect: PanelWindow::sanitized(hostile) }]);
|
|
let [x, y, w, h] = PanelWindow::sanitized(hostile);
|
|
assert!(x <= 100_000.0 && y >= -100_000.0 && (160.0..=16_384.0).contains(&w) && (100.0..=16_384.0).contains(&h));
|
|
assert!(PanelWindow::sanitized([f32::NAN, f32::INFINITY, f32::NAN, f32::NEG_INFINITY]).iter().all(|v| v.is_finite()));
|
|
}
|