diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e2b8c0cdc..56fcd0443 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -79,6 +79,8 @@ mod tests_home; #[cfg(test)] mod tests_labels; #[cfg(test)] +mod tests_libmenus; +#[cfg(test)] mod tests_nativemenu; #[cfg(test)] mod tests_nativeoptions; diff --git a/crates/ui-egui/src/tests_libmenus.rs b/crates/ui-egui/src/tests_libmenus.rs new file mode 100644 index 000000000..b1495a7d6 --- /dev/null +++ b/crates/ui-egui/src/tests_libmenus.rs @@ -0,0 +1,156 @@ +//! Swatch Libraries (#536): Window → Swatch Libraries and the Swatches panel's libraries menu open +//! the library panel, clicked through the whole window as a user would. + +use egui::{Event, PointerButton, Pos2, Rect, pos2, vec2}; +use serde_json::json; +use vectorcraft_engine::Session; + +use crate::VectorcraftApp; + +/// A short window, so the Window menu scrolls to reach its library submenus. +const SW: f32 = 1100.0; +const SH: f32 = 640.0; + +/// One headless frame of the whole window with `events` → the text painted, with its rect. +fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec) -> Vec<(String, Rect)> { + let raw = egui::RawInput { + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(SW, SH))), + events, + time: Some(ctx.input(|i| i.time) + 0.1), + ..Default::default() + }; + let mut out = ctx.run_ui(raw, |ui| { + app.logic(ui.ctx()); + app.ui(ui); + }); + out.textures_delta.clear(); + fn walk(s: &egui::Shape, v: &mut Vec<(String, Rect)>) { + match s { + egui::Shape::Text(t) => v.push((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))), + egui::Shape::Vec(s) => s.iter().for_each(|s| walk(s, v)), + _ => {} + } + } + let mut v = vec![]; + out.shapes.iter().for_each(|c| walk(&c.shape, &mut v)); + v +} + +fn at(texts: &[(String, Rect)], label: &str) -> Pos2 { + texts + .iter() + .rev() + .find(|(t, _)| t.trim_start_matches(['✓', ' ']) == label) + .map(|(_, r)| r.center()) + .unwrap_or_else(|| panic!("no `{label}` in {texts:?}")) +} + +fn hover(app: &mut VectorcraftApp, ctx: &egui::Context, p: Pos2) -> Vec<(String, Rect)> { + frame(app, ctx, vec![Event::PointerMoved(p)]); + frame(app, ctx, vec![]); + frame(app, ctx, vec![]) +} + +fn click(app: &mut VectorcraftApp, ctx: &egui::Context, p: Pos2) -> Vec<(String, Rect)> { + let b = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, ctx, vec![Event::PointerMoved(p)]); + frame(app, ctx, vec![b(true)]); + frame(app, ctx, vec![b(false)]); + frame(app, ctx, vec![]); + frame(app, ctx, vec![]) +} + +fn app() -> (VectorcraftApp, egui::Context) { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + for _ in 0..3 { + frame(&mut app, &ctx, vec![]); + } + (app, ctx) +} + +#[test] +fn window_swatch_libraries_opens_the_library_panel() { + let (mut app, ctx) = app(); + let texts = frame(&mut app, &ctx, vec![]); + let texts = click(&mut app, &ctx, at(&texts, "Window")); + // Scroll the menu down to its library submenus. + let menu = at(&texts, "Actions"); + let wheel = Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: vec2(0.0, -2000.0), + modifiers: Default::default(), + phase: egui::TouchPhase::Move, + }; + frame(&mut app, &ctx, vec![Event::PointerMoved(menu), wheel]); + for _ in 0..10 { + frame(&mut app, &ctx, vec![]); + } + let texts = frame(&mut app, &ctx, vec![]); + let texts = hover(&mut app, &ctx, at(&texts, "Swatch Libraries")); + let p = at(&texts, "Brights"); + let texts = hover(&mut app, &ctx, pos2(p.x, p.y)); + let texts = click(&mut app, &ctx, at(&texts, "Brights")); + assert_eq!(app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some("brights")); + assert!(texts.iter().any(|(t, _)| t == "Add to Swatches" || t == "Brights"), "{texts:?}"); +} + +#[test] +fn every_swatch_library_shows() { + let (mut app, ctx) = app(); + for l in vectorcraft_color::libraries::SWATCH_LIBRARIES.iter().chain(vectorcraft_color::libraries::GRADIENT_LIBRARIES) { + app.run("window.swatchLibrary", json!({"library": l.id})).unwrap(); + frame(&mut app, &ctx, vec![]); + let texts = frame(&mut app, &ctx, vec![]); + assert!(!app.ui.status.contains("Internal error"), "{}: {}", l.id, app.ui.status); + assert!(texts.iter().any(|(t, _)| t == l.name), "{} shows: {texts:?}", l.id); + assert!(ctx.memory(|m| m.area_rect(egui::Id::new(("library-panel", "swatches")))).is_some()); + } +} + +/// Where the button whose tooltip is `tip` is in `area`: hovered along its bottom rows. +fn button_with_tip(app: &mut VectorcraftApp, ctx: &egui::Context, area: Rect, tip: &str) -> Pos2 { + for dy in [22.0, 18.0, 26.0, 30.0, 14.0] { + for i in 0..12 { + let p = pos2(area.left() + 12.0 + 6.0 * i as f32, area.bottom() - dy); + frame(app, ctx, vec![Event::PointerMoved(p)]); + for _ in 0..8 { + if frame(app, ctx, vec![]).iter().any(|(t, _)| t == tip) { + return p; + } + } + } + } + panic!("no button `{tip}` in {area:?}"); +} + +#[test] +fn the_swatches_panel_library_button_opens_a_library() { + let (mut app, ctx) = app(); + app.run("window.panel", json!({"panel": "swatches"})).unwrap(); + for _ in 0..3 { + frame(&mut app, &ctx, vec![]); + } + let area = ctx.memory(|m| m.area_rect(egui::Id::new("icon-panel"))).expect("the Swatches flyout"); + let p = button_with_tip(&mut app, &ctx, area, "Swatch Libraries Menu"); + let texts = click(&mut app, &ctx, p); + let texts = click(&mut app, &ctx, at(&texts, "Pastels")); + assert_eq!(app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some("pastels"), "{}", app.ui.status); + assert!(ctx.memory(|m| m.area_rect(egui::Id::new(("library-panel", "swatches")))).is_some()); + assert!(texts.iter().any(|(t, _)| t == "Pastels"), "{texts:?}"); +} + +#[test] +fn a_library_shows_without_a_document() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + for _ in 0..3 { + frame(&mut app, &ctx, vec![]); + } + app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap(); + frame(&mut app, &ctx, vec![]); + let texts = frame(&mut app, &ctx, vec![]); + assert!(!app.ui.status.contains("Internal error"), "{}", app.ui.status); + assert!(texts.iter().any(|(t, _)| t == "Pastels"), "{texts:?}"); +} diff --git a/crates/ui-egui/src/tests_nativemenu.rs b/crates/ui-egui/src/tests_nativemenu.rs index 9ffe28142..0993e7b44 100644 --- a/crates/ui-egui/src/tests_nativemenu.rs +++ b/crates/ui-egui/src/tests_nativemenu.rs @@ -420,3 +420,24 @@ fn the_app_bar_hides_its_menus_with_the_mac_menu_bar() { assert_eq!(n(&native) + 1, n(&in_window), "{title}"); } } + +/// Window › Swatch Libraries › a library, chosen in the Mac menu bar, opens the library panel (#536). +#[test] +fn a_swatch_library_chosen_in_the_mac_menu_opens_the_library_panel() { + let mut m = Mac::new(true); + let brights = m + .fake + .last() + .items() + .into_iter() + .find(|it| it.command == Some("window.swatchLibrary") && it.params["library"] == "brights") + .cloned() + .unwrap(); + m.fake.report(native_menu::Event::Click(brights)); + m.frame(vec![]); + let out = m.frame(vec![]); + assert_eq!(m.app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some("brights"), "{}", m.app.ui.status); + let texts: Vec = + out.shapes.iter().filter_map(|s| if let egui::Shape::Text(t) = &s.shape { Some(t.galley.text().to_string()) } else { None }).collect(); + assert!(texts.iter().any(|t| t == "Brights"), "{texts:?}"); +}