mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
577 lines
30 KiB
Rust
577 lines
30 KiB
Rust
use super::*;
|
|
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
|
|
use egui_kittest::{Harness, kittest::Queryable};
|
|
|
|
fn app() -> VectorcraftApp {
|
|
VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default())
|
|
}
|
|
|
|
#[test]
|
|
fn panel_commands_group_split_reorder_float_and_restore_without_duplicate_ownership() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap();
|
|
app.run("window.panel.move", json!({"panel":"properties", "anchor":"swatches", "before":"swatches"})).unwrap();
|
|
let before_float = app.ui.docking.clone().unwrap();
|
|
app.run("window.panel.float", json!({"panel":"properties", "x":50,"y":70,"width":330,"height":410})).unwrap();
|
|
assert!(app.ui.docking.as_ref().unwrap().floating.iter().any(|g| g.panels == ["properties"]));
|
|
app.run("window.panel.dock", json!({"panel":"properties"})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap(), &before_float);
|
|
app.run("window.panel.close", json!({"panel":"properties"})).unwrap();
|
|
assert!(!app.ui.docking.as_ref().unwrap().contains(&"properties".into()));
|
|
app.run("window.panel.activate", json!({"panel":"properties"})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap(), &before_float);
|
|
let layout = app.ui.docking.as_ref().unwrap();
|
|
assert!(valid(layout));
|
|
let unique: std::collections::HashSet<_> = layout.panels().into_iter().collect();
|
|
assert_eq!(unique.len(), layout.panels().len());
|
|
let saved = serde_json::to_value(&app.ui).unwrap();
|
|
let restored: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
|
assert_eq!(restored.docking, app.ui.docking);
|
|
assert_eq!(restored.docking_hidden, app.ui.docking_hidden);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_panel_requests_are_atomic_and_do_not_enter_a_custom_workspace() {
|
|
let mut app = app();
|
|
for (id, params) in [
|
|
("window.panel.move", json!({"panel":"layers","anchor":"unknown"})),
|
|
("window.panel.move", json!({"panel":"layers","anchor":"properties","before":"unknown"})),
|
|
("window.panel.move", json!({"panel":"layers","anchor":"properties","zone":42})),
|
|
("window.panel.float", json!({"panel":"layers","width":-10})),
|
|
("window.panel.float", json!({"panel":"unknown"})),
|
|
("window.panel.float", json!({"panel":"layers","x":"invalid"})),
|
|
] {
|
|
let old = app.ui.docking.clone();
|
|
let hidden = app.ui.docking_hidden.clone();
|
|
assert!(app.run(id, params).is_err(), "{id}");
|
|
assert_eq!(app.ui.docking, old);
|
|
assert_eq!(app.ui.docking_hidden, hidden);
|
|
}
|
|
}
|
|
|
|
fn harness(app: VectorcraftApp, size: egui::Vec2) -> Harness<'static, VectorcraftApp> {
|
|
let mut ready = false;
|
|
let mut harness = Harness::builder().with_size(size).with_step_dt(1.0 / 60.0).build_ui_state(
|
|
move |ui, app: &mut VectorcraftApp| {
|
|
if !ready {
|
|
crate::theme::install_fonts(ui.ctx());
|
|
egui_extras::install_image_loaders(ui.ctx());
|
|
crate::theme::apply(ui.ctx(), crate::theme::Brightness::MediumDark);
|
|
ready = true;
|
|
return;
|
|
}
|
|
crate::floating::track(app, ui.ctx());
|
|
crate::dock::show(app, ui);
|
|
egui::CentralPanel::default().show(ui, |_| {});
|
|
crate::floating::show(app, ui.ctx());
|
|
},
|
|
app,
|
|
);
|
|
harness.run_steps(4);
|
|
harness
|
|
}
|
|
|
|
fn drag(h: &mut Harness<'static, VectorcraftApp>, from: Pos2, to: Pos2) {
|
|
h.event(Event::PointerMoved(from));
|
|
h.run_steps(1);
|
|
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
for step in 1..=8 {
|
|
h.event(Event::PointerMoved(from + (to - from) * (step as f32 / 8.0)));
|
|
h.run_steps(1);
|
|
}
|
|
h.event(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(3);
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_panel_tab_drag_enters_shared_docking_and_can_redock_into_another_tab_group() {
|
|
let mut h = harness(app(), vec2(1280.0, 800.0));
|
|
let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
|
drag(&mut h, source, egui::pos2(160.0, 140.0));
|
|
let layout = h.state().ui.docking.as_ref().expect("ordinary tab drag starts shared docking");
|
|
assert!(layout.floating.iter().any(|g| g.panels.contains(&"layers".into())), "{layout:?}");
|
|
let source = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"layers".to_string()))).unwrap().rect.center();
|
|
let destination = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"properties".to_string()))).unwrap().rect.center();
|
|
drag(&mut h, source, destination);
|
|
let layout = h.state().ui.docking.as_ref().unwrap();
|
|
assert!(!layout.floating.iter().any(|g| g.panels.contains(&"layers".into())));
|
|
assert!(valid(layout));
|
|
}
|
|
|
|
#[test]
|
|
fn saved_workspace_restores_the_custom_tree_and_hidden_panel_placement() {
|
|
let mut app = app();
|
|
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"properties","x":50,"y":80})).unwrap();
|
|
app.run("window.panel.close", json!({"panel":"swatches"})).unwrap();
|
|
let layout = app.ui.docking.clone();
|
|
let hidden = app.ui.docking_hidden.clone();
|
|
app.run("window.workspace.new", json!({"name":"Custom docking"})).unwrap();
|
|
app.run("window.workspace", json!({"name":"Essentials"})).unwrap();
|
|
assert!(app.ui.docking.is_none());
|
|
app.run("window.workspace", json!({"name":"Custom docking"})).unwrap();
|
|
assert_eq!(app.ui.docking, layout);
|
|
assert_eq!(app.ui.docking_hidden, hidden);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_float_group_aliases_use_atomic_shared_groups_and_preserve_tools() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"layers","group":true,"x":30,"y":50})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap().floating[0].panels.len(), 3);
|
|
app.run("window.panel.float", json!({"panel":"swatches","onto":"layers"})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap().floating[0].panels.len(), 4);
|
|
let before = app.ui.docking.clone();
|
|
assert!(app.run("window.panel.float", json!({"panel":"properties","onto":"missing","group":true})).is_err());
|
|
assert_eq!(app.ui.docking, before);
|
|
app.run("window.panel.float", json!({"panel":"tools","x":70,"y":90})).unwrap();
|
|
assert_eq!(app.ui.toolbar_pos, Some([70.0, 90.0]));
|
|
assert_eq!(app.ui.docking, before);
|
|
app.run("window.panel.dock", json!({"panel":"layers","group":true})).unwrap();
|
|
assert!(app.ui.docking.as_ref().unwrap().floating.is_empty());
|
|
}
|
|
|
|
/// Explicit offscreen evidence; this creates no native windows.
|
|
#[test]
|
|
fn capture_custom_panel_docking_visual_fixtures() {
|
|
let Some(directory) = std::env::var_os("CRAFT_UI_DOCKING_FIXTURES").map(std::path::PathBuf::from) else { return };
|
|
std::fs::create_dir_all(&directory).unwrap();
|
|
for theme in crate::theme::Brightness::ALL {
|
|
for width in [800.0, 1280.0] {
|
|
for scale in [1.0, 1.5, 2.0] {
|
|
let mut app = app();
|
|
app.run("file.new", json!({})).unwrap();
|
|
app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"properties","x":30,"y":90,"width":300,"height":440})).unwrap();
|
|
let mut ready = false;
|
|
let mut harness = Harness::builder().with_size(vec2(width, 800.0)).with_pixels_per_point(scale).wgpu().build_ui_state(
|
|
move |ui, app: &mut VectorcraftApp| {
|
|
if !ready {
|
|
egui_extras::install_image_loaders(ui.ctx());
|
|
crate::theme::install_fonts(ui.ctx());
|
|
crate::theme::apply(ui.ctx(), theme);
|
|
ready = true;
|
|
return;
|
|
}
|
|
crate::dock::show(app, ui);
|
|
egui::CentralPanel::default().show(ui, |ui| {
|
|
ui.label("Custom panel workspace");
|
|
});
|
|
},
|
|
app,
|
|
);
|
|
harness.input_mut().max_texture_side = Some(8192);
|
|
harness.run_steps(5);
|
|
assert!(valid(harness.state().ui.docking.as_ref().unwrap()));
|
|
harness.render().unwrap().save(directory.join(format!("vectorcraft-shared-docking-{}-{width}-{scale}x.png", theme.id()))).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn escape_cancels_the_first_drag_without_replacing_the_legacy_workspace() {
|
|
let mut h = harness(app(), vec2(1280.0, 800.0));
|
|
let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
|
h.event(Event::PointerMoved(source));
|
|
h.run_steps(1);
|
|
h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
h.event(Event::PointerMoved(source - vec2(90.0, 0.0)));
|
|
h.run_steps(2);
|
|
assert!(h.state().ui.docking.is_some());
|
|
h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
assert!(h.state().ui.docking.is_none());
|
|
h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(2);
|
|
assert!(h.state().ui.docking.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn serialized_layout_actions_cover_geometry_reorder_accordion_and_atomic_rejection() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
app.run("window.panel.layout", json!({"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}})).unwrap();
|
|
assert!(
|
|
matches!(app.ui.docking.as_ref().unwrap().root, Some(Node::Split { size: craft_ui::layout::SplitSize::Ratio(value), .. }) if (value - 0.35).abs() < 0.001)
|
|
);
|
|
app.run("window.panel.float", json!({"panel":"properties"})).unwrap();
|
|
app.run("window.panel.layout", json!({"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}})).unwrap();
|
|
assert_eq!(
|
|
app.ui.docking.as_ref().unwrap().floating.iter().find(|group| group.panels.iter().any(|p| p == "properties")).unwrap().rect,
|
|
[44.0, 66.0, 360.0, 450.0]
|
|
);
|
|
app.run("window.panel.layout", json!({"action":{"Move":{"panel":"properties","anchor":"swatches","placement":{"Tab":{"before":"swatches"}}}}}))
|
|
.unwrap();
|
|
assert_eq!(group_members_for_test(app.ui.docking.as_ref().unwrap(), "swatches"), ["properties", "swatches"]);
|
|
let before = app.ui.docking.clone();
|
|
let hidden = app.ui.docking_hidden.clone();
|
|
for action in [
|
|
json!({"MoveFloating":{"panel":"properties","rect":[0,0,-5,30]}}),
|
|
json!({"ResizeSplit":{"path":[true,true,true],"size":{"Ratio":0.2}}}),
|
|
json!({"Open":{"panel":"unknown","anchor":null}}),
|
|
json!({"Move":{"panel":"properties","anchor":"swatches","placement":{"Tab":{"before":"unknown"}}}}),
|
|
] {
|
|
assert!(app.run("window.panel.layout", json!({"action":action})).is_err());
|
|
assert_eq!(app.ui.docking, before);
|
|
assert_eq!(app.ui.docking_hidden, hidden);
|
|
}
|
|
app.ui.docking = Some(Layout {
|
|
root: Some(Node::Stack { entries: vec![craft_ui::docking::StackEntry { panel: "layers".into(), open: true, height: Some(100.0) }] }),
|
|
floating: Vec::new(),
|
|
});
|
|
app.run("window.panel.layout", json!({"action":{"SetStackOpen":{"panel":"layers","open":false}}})).unwrap();
|
|
app.run("window.panel.layout", json!({"action":{"ResizeStack":{"panel":"layers","height":150}}})).unwrap();
|
|
let Some(Node::Stack { entries }) = &app.ui.docking.as_ref().unwrap().root else { panic!("expected stack") };
|
|
assert!(!entries[0].open);
|
|
assert_eq!(entries[0].height, Some(150.0));
|
|
}
|
|
|
|
fn group_members_for_test(layout: &Layout<String>, panel: &str) -> Vec<String> {
|
|
let mut pending: Vec<_> = layout.root.iter().collect();
|
|
while let Some(node) = pending.pop() {
|
|
match node {
|
|
Node::Tabs { panels, .. } if panels.iter().any(|p| p == panel) => return panels.clone(),
|
|
Node::Split { first, second, .. } => pending.extend([first.as_ref(), second.as_ref()]),
|
|
_ => {}
|
|
}
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
#[test]
|
|
fn populated_canvas_keeps_a_real_stroke_edit_after_floating_and_redocking() {
|
|
let mut app = app();
|
|
app.run("file.new", json!({"width":480,"height":360})).unwrap();
|
|
let id = app.run("shape.rectangle", json!({"x":80,"y":70,"width":250,"height":160})).unwrap()["id"].clone();
|
|
app.run("select.set", json!({"ids":[id]})).unwrap();
|
|
app.run("paint.setFill", json!({"color":"#5297bb"})).unwrap();
|
|
app.run("stroke.set", json!({"weight":8})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"stroke","x":40,"y":140,"width":310,"height":490})).unwrap();
|
|
let directory = std::env::var_os("CRAFT_UI_DOCKING_FIXTURES").map(std::path::PathBuf::from);
|
|
let builder = Harness::builder().with_size(vec2(1280.0, 800.0));
|
|
let builder = if directory.is_some() { builder.wgpu() } else { builder };
|
|
let mut h = builder.build_ui_state(
|
|
|ui, app: &mut VectorcraftApp| {
|
|
app.logic(ui.ctx());
|
|
app.ui(ui);
|
|
},
|
|
app,
|
|
);
|
|
h.input_mut().max_texture_side = Some(8192);
|
|
h.run_steps(5);
|
|
let mut pending: Vec<_> = h.output().shapes.iter().map(|shape| &shape.shape).collect();
|
|
let mut checkbox = None;
|
|
while let Some(shape) = pending.pop() {
|
|
match shape {
|
|
egui::Shape::Text(text) if text.galley.text() == "Dashed Line" => {
|
|
checkbox = Some(text.visual_bounding_rect().center());
|
|
break;
|
|
}
|
|
egui::Shape::Vec(shapes) => pending.extend(shapes),
|
|
_ => {}
|
|
}
|
|
}
|
|
let at = checkbox.expect("painted Dashed Line checkbox");
|
|
h.hover_at(at);
|
|
h.run_steps(1);
|
|
for pressed in [true, false] {
|
|
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
}
|
|
h.run_steps(2);
|
|
let dash = crate::panels::current_stroke(h.state()).unwrap().dash.unwrap();
|
|
assert_eq!(dash.pattern, [12.0, 12.0]);
|
|
if let Some(directory) = &directory {
|
|
std::fs::create_dir_all(directory).unwrap();
|
|
h.render().unwrap().save(directory.join("vectorcraft-populated-stroke-floating.png")).unwrap();
|
|
}
|
|
let from = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"stroke".to_string()))).unwrap().rect.center();
|
|
let to = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"properties".to_string()))).unwrap().rect.center();
|
|
drag(&mut h, from, to);
|
|
assert!(h.state().ui.docking.as_ref().unwrap().floating.is_empty());
|
|
assert_eq!(crate::panels::current_stroke(h.state()).unwrap().dash.unwrap(), dash);
|
|
if let Some(directory) = &directory {
|
|
h.render().unwrap().save(directory.join("vectorcraft-populated-stroke-redocked.png")).unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn changing_workspace_during_a_drag_cancels_the_stale_gesture() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
let destination = app.ui.docking.clone();
|
|
app.run("window.workspace.new", json!({"name":"Destination"})).unwrap();
|
|
app.run("window.workspace", json!({"name":"Essentials"})).unwrap();
|
|
let mut h = harness(app, vec2(1280.0, 800.0));
|
|
let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
|
h.event(Event::PointerMoved(source));
|
|
h.run_steps(1);
|
|
h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
h.event(Event::PointerMoved(source - vec2(90.0, 0.0)));
|
|
h.run_steps(2);
|
|
h.state_mut().run("window.workspace", json!({"name":"Destination"})).unwrap();
|
|
h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
assert_eq!(h.state().ui.docking, destination);
|
|
h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().ui.docking, destination);
|
|
}
|
|
|
|
#[test]
|
|
fn floating_icon_groups_redock_to_the_rail_and_keep_saved_origins() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"color"})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"swatches", "onto":"color"})).unwrap();
|
|
assert_eq!(app.ui.docking_icons.len(), 2);
|
|
let workspace = crate::workspaces::capture(&app.ui, "Icon origins");
|
|
let saved = serde_json::to_vec(&workspace).unwrap();
|
|
let restored: crate::workspaces::Workspace = serde_json::from_slice(&saved).unwrap();
|
|
crate::workspaces::apply(&mut app.ui, &restored);
|
|
app.run("window.panel.dock", json!({"panel":"color", "group":true})).unwrap();
|
|
let layout = app.ui.docking.as_ref().unwrap();
|
|
assert!(!layout.contains(&"color".into()));
|
|
assert!(!layout.contains(&"swatches".into()));
|
|
assert!(app.ui.floating_panels.is_empty());
|
|
assert_eq!(app.ui.open_panel.as_deref(), Some("color"), "the requested icon pops out after redocking");
|
|
app.run("window.panel", json!({"panel":"color"})).unwrap();
|
|
assert!(app.ui.open_panel.is_none(), "the same icon puts it away");
|
|
app.run("window.panel", json!({"panel":"color"})).unwrap();
|
|
assert_eq!(app.ui.open_panel.as_deref(), Some("color"));
|
|
}
|
|
|
|
#[test]
|
|
fn all_dock_group_origins_survive_float_and_return() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"layers", "group":true})).unwrap();
|
|
assert_eq!(app.ui.floating_panels[0].panels.len(), 3);
|
|
assert!(app.ui.docking_hidden.contains_key("properties"));
|
|
assert!(app.ui.docking_hidden.contains_key("layers"));
|
|
assert!(app.ui.docking_hidden.contains_key("libraries"));
|
|
app.run("window.panel.dock", json!({"panel":"layers", "group":true})).unwrap();
|
|
let layout = app.ui.docking.as_ref().unwrap();
|
|
assert!(layout.floating.is_empty());
|
|
assert_eq!(layout.panels().into_iter().map(String::as_str).collect::<Vec<_>>(), ["properties", "layers", "libraries"]);
|
|
assert_eq!(app.ui.dock_tab, crate::state::DockTab::Layers);
|
|
assert!(valid(layout));
|
|
}
|
|
|
|
#[test]
|
|
fn panel_specific_menu_executes_after_first_float_regroup_and_reload() {
|
|
let mut app = app();
|
|
app.run("file.new", json!({})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"swatches","x":40,"y":100})).unwrap();
|
|
let mut h = harness(app, vec2(1280.0, 900.0));
|
|
for stage in 0..3 {
|
|
if stage == 1 {
|
|
h.state_mut().run("window.panel.float", json!({"panel":"color","x":60,"y":120})).unwrap();
|
|
h.state_mut().run("window.panel.float", json!({"panel":"swatches","onto":"color"})).unwrap();
|
|
} else if stage == 2 {
|
|
let saved = serde_json::to_vec(&h.state().ui).unwrap();
|
|
h.state_mut().ui = serde_json::from_slice(&saved).unwrap();
|
|
}
|
|
h.run_steps(4);
|
|
let at = h
|
|
.query_all_by_label("Panel menu")
|
|
.find(|node| node.rect().center().x < 800.0)
|
|
.expect("floating panel menu has an accessible button")
|
|
.rect()
|
|
.center();
|
|
h.hover_at(at);
|
|
h.run_steps(1);
|
|
for pressed in [true, false] {
|
|
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
}
|
|
h.run_steps(2);
|
|
let action = h.query_all_by_label(" New Swatch…").next().expect("app-specific menu action remains reachable").rect().center();
|
|
for pressed in [true, false] {
|
|
h.event(Event::PointerButton { pos: action, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
}
|
|
assert_eq!(h.state().ui.dialog.as_ref().map(|dialog| dialog.kind.as_str()), Some("newSwatch"), "stage {stage}: actual panel action executes");
|
|
h.state_mut().ui.dialog = None;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn iconic_origin_tracks_explicit_custom_dock_and_legacy_saved_floats() {
|
|
let mut custom = app();
|
|
custom.run("window.panel.move", json!({"panel":"color","anchor":"layers"})).unwrap();
|
|
let before = custom.ui.docking.clone().unwrap();
|
|
custom.run("window.panel.float", json!({"panel":"color"})).unwrap();
|
|
custom.run("window.panel.dock", json!({"panel":"color"})).unwrap();
|
|
assert_eq!(custom.ui.docking.as_ref(), Some(&before), "explicit custom tabs are the saved destination");
|
|
assert!(custom.ui.open_panel.is_none(), "a docked custom tab is not an icon popout");
|
|
|
|
let mut legacy = app();
|
|
crate::floating::float(&mut legacy.ui, &["color"], "color", egui::pos2(120.0, 160.0));
|
|
let saved = serde_json::to_vec(&legacy.ui).unwrap();
|
|
legacy.ui = serde_json::from_slice(&saved).unwrap();
|
|
assert!(legacy.ui.docking.is_none());
|
|
legacy.run("window.panel.dock", json!({"panel":"color"})).unwrap();
|
|
assert!(!legacy.ui.docking.as_ref().unwrap().contains(&"color".into()), "legacy icon floats return to rail");
|
|
assert_eq!(legacy.ui.open_panel.as_deref(), Some("color"));
|
|
}
|
|
|
|
#[test]
|
|
fn native_sets_columns_and_collapse_preserve_shared_splits_and_saved_origins() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
let original = app.ui.docking.clone().unwrap();
|
|
app.run("window.panel.float", json!({"panel":"stroke","x":100,"y":100})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"color","below":"stroke","collapsed":true})).unwrap();
|
|
let root = app.ui.docking.as_ref().unwrap().root.clone();
|
|
assert_eq!(root, original.root);
|
|
let column = app.ui.floating_panels.first().unwrap().column;
|
|
assert!(column.is_some());
|
|
assert_eq!(app.ui.floating_panels.len(), 2);
|
|
assert!(app.ui.floating_panels.iter().all(|g| g.column == column));
|
|
app.run("window.panel.dock", json!({"panel":"stroke","group":true,"column":true})).unwrap();
|
|
assert!(app.ui.floating_panels.iter().all(|g| g.docked == Some(1)));
|
|
app.run("window.workspace.new", json!({"name":"Mixed shared native"})).unwrap();
|
|
let saved = app.ui.floating_panels.clone();
|
|
app.run("window.workspace", json!({"name":"Essentials"})).unwrap();
|
|
app.run("window.workspace", json!({"name":"Mixed shared native"})).unwrap();
|
|
assert_eq!(app.ui.floating_panels, saved);
|
|
assert_eq!(app.ui.docking.as_ref().unwrap().root, root);
|
|
app.run("window.panel.float", json!({"panel":"stroke","group":true})).unwrap();
|
|
assert!(app.ui.floating_panels.iter().all(|g| g.docked.is_none()));
|
|
let before = serde_json::to_value(&app.ui).unwrap();
|
|
assert!(app.run("window.panel.float", json!({"panel":"stroke","below":"unknown"})).is_err());
|
|
// Error reporting may change status, but ownership, metadata and root remain atomic.
|
|
let mut after = serde_json::to_value(&app.ui).unwrap();
|
|
let mut before = before;
|
|
before.as_object_mut().unwrap().remove("status");
|
|
after.as_object_mut().unwrap().remove("status");
|
|
assert_eq!(before, after);
|
|
app.run("window.panel.dock", json!({"panel":"stroke","group":true})).unwrap();
|
|
app.run("window.panel.dock", json!({"panel":"color","group":true})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap().root, original.root);
|
|
assert!(app.ui.floating_panels.is_empty());
|
|
assert!(valid(app.ui.docking.as_ref().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn native_set_close_pointer_restores_shared_split_ownership() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
let root = app.ui.docking.as_ref().unwrap().root.clone();
|
|
app.run("window.panel.float", json!({"panel":"stroke","x":70,"y":90})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"color","below":"stroke"})).unwrap();
|
|
let mut h = harness(app, vec2(1280.0, 900.0));
|
|
if let Some(directory) = std::env::var_os("CRAFT_UI_DOCKING_SNAPSHOT_DIR") {
|
|
let directory = std::path::PathBuf::from(directory);
|
|
std::fs::create_dir_all(&directory).unwrap();
|
|
h.render().unwrap().save(directory.join("vectorcraft-native-set-shared-split.png")).unwrap();
|
|
}
|
|
let area = h.ctx.memory(|m| m.area_rect(crate::floating::area_id("stroke"))).unwrap();
|
|
let at = egui::pos2(area.right() - 10.0, area.top() + 1.0 + crate::floating::TITLE / 2.0);
|
|
for pressed in [true, false] {
|
|
h.event(Event::PointerMoved(at));
|
|
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
}
|
|
h.run_steps(3);
|
|
assert!(h.state().ui.floating_panels.is_empty());
|
|
assert_eq!(h.state().ui.docking.as_ref().unwrap().root, root);
|
|
assert!(valid(h.state().ui.docking.as_ref().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn a_single_native_tab_redock_retains_the_remaining_float_and_return_origin() {
|
|
let mut app = app();
|
|
app.run("window.panel.move", json!({"panel":"swatches","anchor":"layers","zone":"top"})).unwrap();
|
|
let root = app.ui.docking.as_ref().unwrap().root.clone();
|
|
app.run("window.panel.float", json!({"panel":"swatches","x":80,"y":90})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"color","onto":"swatches","collapsed":true})).unwrap();
|
|
assert!(
|
|
matches!(app.ui.docking.as_ref().unwrap().root.as_ref(), Some(Node::Tabs { panels, active }) if panels.get(*active).is_some_and(|panel| panel == "properties")),
|
|
"floating Color intentionally activates the first root fallback, Properties"
|
|
);
|
|
let mut expected = Layout { root, floating: vec![] };
|
|
expected.apply(Action::Activate { panel: "properties".to_string() }).unwrap();
|
|
app.run("window.panel.dock", json!({"panel":"swatches"})).unwrap();
|
|
assert_eq!(app.ui.docking.as_ref().unwrap().root, expected.root);
|
|
assert_eq!(app.ui.floating_panels.len(), 1);
|
|
assert_eq!(app.ui.floating_panels[0].panels, ["color"]);
|
|
assert!(valid(app.ui.docking.as_ref().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_shared_activation_expands_a_native_collapsed_owner() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"layers","collapsed":true})).unwrap();
|
|
assert!(app.ui.floating_panels[0].collapsed);
|
|
app.run("window.panel.layout", json!({"action":Action::Activate { panel:"layers".to_string() }})).unwrap();
|
|
assert!(!app.ui.floating_panels[0].collapsed);
|
|
assert_eq!(app.ui.floating_panels[0].panels, ["layers"]);
|
|
assert!(valid(app.ui.docking.as_ref().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn escape_restores_native_set_position_and_metadata_after_several_frames() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"stroke","x":80,"y":100})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"color","below":"stroke"})).unwrap();
|
|
let mut h = harness(app, vec2(1280.0, 900.0));
|
|
let original = h.state().ui.floating_panels.clone();
|
|
let layout = h.state().ui.docking.clone();
|
|
let area = h.ctx.memory(|m| m.area_rect(crate::floating::area_id("stroke"))).unwrap();
|
|
let from = area.left_top() + vec2(30.0, 1.0 + crate::floating::TITLE / 2.0);
|
|
h.event(Event::PointerMoved(from));
|
|
h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.run_steps(1);
|
|
for step in 1..=8 {
|
|
h.event(Event::PointerMoved(from + vec2(step as f32 * 12.0, step as f32 * 9.0)));
|
|
h.run_steps(1);
|
|
}
|
|
assert_ne!(h.state().ui.floating_panels, original, "actual title gesture moved the set");
|
|
h.event(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().ui.floating_panels, original);
|
|
assert_eq!(h.state().ui.docking, layout);
|
|
}
|
|
|
|
#[test]
|
|
fn first_legacy_icon_tear_can_join_a_native_set() {
|
|
let mut app = app();
|
|
crate::floating::float(&mut app.ui, &["stroke"], "stroke", egui::pos2(100.0, 100.0));
|
|
crate::floating::float(&mut app.ui, &["color"], "color", egui::pos2(100.0, 100.0));
|
|
crate::floating::attach(&mut app.ui, &[1], "stroke", true);
|
|
assert!(app.ui.docking.is_none());
|
|
let mut h = harness(app, vec2(1280.0, 900.0));
|
|
let source = h.query_all_by_label("Swatches").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
|
let color = h.ctx.data(|data| data.get_temp::<egui::Rect>(crate::floating::group_rect_id("color"))).unwrap();
|
|
drag(&mut h, source, egui::pos2(color.center().x, color.bottom() + 2.0));
|
|
assert!(h.state().ui.docking.is_some(), "successful first tear retains shared ownership");
|
|
let groups = &h.state().ui.floating_panels;
|
|
let color = groups.iter().find(|g| g.panels.iter().any(|p| p == "color")).unwrap();
|
|
let swatches = groups.iter().find(|g| g.panels.iter().any(|p| p == "swatches")).unwrap();
|
|
assert!(color.column.is_some());
|
|
assert_eq!(swatches.column, color.column);
|
|
assert!(valid(h.state().ui.docking.as_ref().unwrap()));
|
|
}
|
|
|
|
#[test]
|
|
fn native_set_drop_targets_respect_the_visible_layer_above_an_ordinary_float() {
|
|
let mut app = app();
|
|
app.run("window.panel.float", json!({"panel":"swatches","x":100,"y":100})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"stroke","x":100,"y":100})).unwrap();
|
|
app.run("window.panel.float", json!({"panel":"color","below":"stroke"})).unwrap();
|
|
let h = harness(app, vec2(1280.0, 900.0));
|
|
let rect = h.ctx.data(|data| data.get_temp::<egui::Rect>(crate::floating::group_rect_id("stroke"))).unwrap();
|
|
let at = rect.center();
|
|
assert_eq!(h.ctx.layer_id_at(at).unwrap().id, crate::floating::area_id("stroke"), "native set is visibly above the ordinary shared float");
|
|
let target = crate::floating::drop_target(h.state(), &h.ctx, crate::floating::Moving::Panel("layers"), at).unwrap().0;
|
|
assert!(
|
|
matches!(target, crate::floating::Drop::Above("stroke") | crate::floating::Drop::Below("stroke")),
|
|
"visible native group owns the drop: {target:?}"
|
|
);
|
|
}
|