Files
vectorcraft/crates/ui-egui/src/panels/tests_effectedit.rs
bflatastic 43d7ac420d Merge wp/D2
# Conflicts:
#	crates/pdf/src/export.rs
#	crates/render/src/fx.rs
#	crates/render/src/lib.rs
#	crates/svg/src/export.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/dialogs/mod.rs
#	crates/ui-egui/src/panels/appearance.rs
#	docs/control-protocol.md
#	docs/mcp.md
2026-10-04 23:46:38 +07:00

197 lines
11 KiB
Rust

//! Headless frames of the Appearance panel's editing depth: effect names open their dialog
//! prefilled, rows drag (Alt copies), several fills/strokes select together, stroke notes, the
//! chip's Shift-click, the thumbnail drag and the Properties fx button.
use serde_json::{Value, json};
use super::tests_appearance::{app_with_rect, click, frame_events, frame_raw, run, text_rect};
use super::*;
fn pointer(pos: egui::Pos2, pressed: bool, modifiers: egui::Modifiers) -> egui::Event {
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers }
}
/// One frame of the Appearance panel with `events` and `modifiers` held.
fn panel_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>, modifiers: egui::Modifiers) -> Vec<(String, egui::Rect)> {
let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect();
frame_raw(ctx, app, egui::RawInput { events, ..Default::default() }, appearance::show)
}
/// Drag from `from` to `to` in the Appearance panel, `modifiers` held on release.
fn drag(ctx: &egui::Context, app: &mut VectorcraftApp, from: egui::Pos2, to: egui::Pos2, modifiers: egui::Modifiers) {
let none = egui::Modifiers::NONE;
panel_frame(ctx, app, vec![egui::Event::PointerMoved(from)], none);
panel_frame(ctx, app, vec![pointer(from, true, none)], none);
panel_frame(ctx, app, vec![egui::Event::PointerMoved(from + egui::vec2(0.0, 8.0))], none);
panel_frame(ctx, app, vec![egui::Event::PointerMoved(to)], modifiers);
panel_frame(ctx, app, vec![pointer(to, false, modifiers)], modifiers);
panel_frame(ctx, app, vec![], none);
}
/// Click `pos` with `modifiers` held.
fn click_with(ctx: &egui::Context, app: &mut VectorcraftApp, pos: egui::Pos2, modifiers: egui::Modifiers) {
for e in [egui::Event::PointerMoved(pos), pointer(pos, true, modifiers), pointer(pos, false, modifiers)] {
panel_frame(ctx, app, vec![e], modifiers);
}
}
/// A point in the row of the label `s` (right of its chip, weight and notes).
fn row_of(texts: &[(String, egui::Rect)], s: &str) -> egui::Pos2 {
let r = text_rect(texts, s);
egui::pos2(r.left() + 200.0, r.center().y)
}
/// The rect of the panel-menu entry `s` (entries are drawn after a check-mark column).
fn menu_item(texts: &[(String, egui::Rect)], s: &str) -> egui::Rect {
texts.iter().find(|(t, _)| t.trim_start() == s).unwrap_or_else(|| panic!("`{s}` not in the menu: {texts:?}")).1
}
fn effects(app: &VectorcraftApp, item: Option<usize>) -> Vec<vectorcraft_doc::Effect> {
first_selected(app).unwrap().appearance.effects_at(item).cloned().unwrap_or_default()
}
#[test]
fn an_effect_name_opens_its_dialog_prefilled_and_ok_edits_it_in_place() {
let mut app = app_with_rect();
run(&mut app, "effect.apply", json!({"effect": "distort.roughen", "params": {"size": 9}}));
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
click(&ctx, &mut app, text_rect(&texts, "Roughen").center(), appearance::show);
let d = app.ui.dialog.clone().expect("the effect's dialog");
assert_eq!((d.kind.as_str(), d.f64("size", 0.0), d.fields["__index"].clone()), ("effect", 9.0, json!(0)));
// The dialog previews; OK keeps the new value on the same effect as one undo step.
let mut out = ctx.run_ui(Default::default(), |ui| crate::dialogs::show(&mut app, ui.ctx()));
out.textures_delta.clear();
app.ui.dialog.as_mut().unwrap().fields.insert("size".into(), json!(20));
crate::dialogs::confirm(&mut app).unwrap();
let fx = effects(&app, None);
assert_eq!((fx.len(), fx[0].params["size"].clone()), (1, json!(20)));
assert!(app.ui.dialog.is_none() && app.last_effect.is_none(), "an edit is not a new Last Effect");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(effects(&app, None)[0].params["size"], json!(9));
}
#[test]
fn choosing_an_applied_effect_asks_to_edit_it_or_add_another() {
let mut app = app_with_rect();
run(&mut app, "effect.apply", json!({"effect": "distort.roughen", "item": 0, "params": {"size": 9}}));
run(&mut app, "appearance.setActiveItem", json!({"index": 0}));
// The active fill already has it: the question comes first; OK edits the applied one.
assert_eq!(app.run("effect.dialog", json!({"effect": "distort.roughen"})).unwrap(), json!({"pending": "effectExists"}));
crate::dialogs::confirm(&mut app).unwrap();
let d = app.ui.dialog.clone().unwrap();
assert_eq!((d.kind.as_str(), d.f64("size", 0.0), d.fields["__index"].clone(), d.fields["__item"].clone()), ("effect", 9.0, json!(0), json!(0)));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(effects(&app, Some(0)).len(), 1);
// "Add New": a fresh dialog with the defaults, which adds a second one.
app.run("effect.dialog", json!({"effect": "distort.roughen"})).unwrap();
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
crate::dialogs::confirm(&mut app).unwrap();
let d = app.ui.dialog.clone().unwrap();
assert!(d.fields.get("__index").is_none() && d.f64("size", 0.0) == 5.0, "{:?}", d.fields);
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(effects(&app, Some(0)).len(), 2);
// Another list without it opens the dialog directly.
assert_eq!(app.run("effect.dialog", json!({"effect": "distort.roughen", "item": null})).unwrap(), Value::Null);
assert_eq!(app.ui.dialog.as_ref().unwrap().kind, "effect");
}
#[test]
fn effect_rows_drag_to_reorder_and_into_an_item() {
let mut app = app_with_rect();
run(&mut app, "effect.apply", json!({"effect": "distort.twist"}));
run(&mut app, "effect.apply", json!({"effect": "distort.roughen"}));
let ctx = egui::Context::default();
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
// Twist below Roughen.
let below = text_rect(&texts, "Roughen").center() + egui::vec2(60.0, 9.0);
drag(&ctx, &mut app, row_of(&texts, "Twist"), below, egui::Modifiers::NONE);
let ids = |fx: Vec<vectorcraft_doc::Effect>| fx.into_iter().map(|e| e.id).collect::<Vec<_>>();
assert_eq!(ids(effects(&app, None)), ["distort.roughen", "distort.twist"]);
// Alt-dragging Twist onto the stroke row copies it into the stroke's effects.
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
drag(&ctx, &mut app, row_of(&texts, "Twist"), row_of(&texts, "Stroke:"), egui::Modifiers::ALT);
assert_eq!(ids(effects(&app, None)), ["distort.roughen", "distort.twist"]);
assert_eq!(ids(effects(&app, Some(1))), ["distort.twist"]);
// Dropped effects become the selected row (their item active).
assert_eq!(app.session.appearance_item(), Some(1));
app.run("edit.undo", json!({})).unwrap();
assert!(effects(&app, Some(1)).is_empty());
}
#[test]
fn alt_dragging_an_item_duplicates_it_and_several_items_delete_together() {
let mut app = app_with_rect(); // [Fill, Stroke]
let ctx = egui::Context::default();
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
drag(&ctx, &mut app, row_of(&texts, "Fill:"), row_of(&texts, "Stroke:"), egui::Modifiers::ALT);
let kinds = |app: &VectorcraftApp| first_selected(app).unwrap().appearance.items.iter().map(|i| i.kind_name()).collect::<Vec<_>>();
assert_eq!(kinds(&app), ["fill", "stroke", "fill"]);
// Click the stroke, Cmd-click the top fill: both go with Remove Item.
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
click_with(&ctx, &mut app, row_of(&texts, "Stroke:"), egui::Modifiers::NONE);
let top_fill = texts.iter().filter(|(t, _)| t == "Fill:").map(|(_, r)| *r).min_by(|a, b| a.top().total_cmp(&b.top())).unwrap();
click_with(&ctx, &mut app, egui::pos2(top_fill.left() + 200.0, top_fill.center().y), egui::Modifiers::COMMAND);
let menu = frame_events(&ctx, &mut app, vec![], appearance::menu);
click(&ctx, &mut app, menu_item(&menu, "Remove Item").center(), appearance::menu);
assert_eq!(kinds(&app), ["fill"]);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(kinds(&app).len(), 3, "one undo step");
}
#[test]
fn stroke_rows_note_dashes_brushes_and_show_all_hidden_attributes() {
let mut app = app_with_rect();
let ctx = egui::Context::default();
assert!(!frame_events(&ctx, &mut app, vec![], appearance::show).iter().any(|(t, _)| t == "Dashed"));
run(&mut app, "stroke.set", json!({"dash": [4, 2], "profile": "lens"}));
let brush = run(&mut app, "brush.list", json!({}))["brushes"][0]["name"].as_str().unwrap().to_string();
run(&mut app, "brush.apply", json!({ "name": brush }));
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
assert!(texts.iter().any(|(t, _)| *t == format!("{brush}, Dashed")), "{texts:?}");
// Show All Hidden Attributes is enabled once something is hidden, and shows it.
let enabled = |app: &mut VectorcraftApp| {
let menu = frame_events(&ctx, app, vec![], appearance::menu);
let r = menu_item(&menu, "Show All Hidden Attributes");
click(&ctx, app, r.center(), appearance::menu);
};
run(&mut app, "appearance.setItem", json!({"index": 0, "visible": false}));
enabled(&mut app);
assert!(first_selected(&app).unwrap().appearance.items[0].visible());
}
#[test]
fn shift_clicking_a_chip_opens_the_mixer_on_that_row() {
let mut app = app_with_rect();
let ctx = egui::Context::default();
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
// The chip sits 52 pt right of the label's start (label at EYE_W + 24, chip at EYE_W + 76).
let label = text_rect(&texts, "Fill:");
click_with(&ctx, &mut app, egui::pos2(label.left() + 52.0 + 9.0, label.center().y), egui::Modifiers::SHIFT);
assert_eq!(app.ui.open_panel.as_deref(), Some("color"));
assert_eq!(app.session.appearance_item(), Some(0));
}
#[test]
fn the_thumbnail_drags_the_appearance_and_the_properties_fx_button_opens_the_effects() {
let mut app = app_with_rect();
let ctx = egui::Context::default();
let texts = panel_frame(&ctx, &mut app, vec![], egui::Modifiers::NONE);
// The thumbnail is left of the object row's label.
let label = text_rect(&texts, "Rectangle");
let thumb = egui::pos2(label.left() - 28.0, label.center().y);
let none = egui::Modifiers::NONE;
for e in [egui::Event::PointerMoved(thumb), pointer(thumb, true, none), egui::Event::PointerMoved(thumb + egui::vec2(30.0, 30.0))] {
panel_frame(&ctx, &mut app, vec![e], none);
}
let id = first_selected(&app).unwrap().id;
assert_eq!(egui::DragAndDrop::payload::<crate::widgets::PanelDrag>(&ctx).as_deref(), Some(&crate::widgets::PanelDrag::Appearance(id)));
// Properties: the fx button lists the effect groups.
let props = egui::Context::default();
let texts = frame_raw(&props, &mut app, Default::default(), properties::show);
click(&props, &mut app, text_rect(&texts, "fx").center(), properties::show);
let texts = frame_raw(&props, &mut app, Default::default(), properties::show);
assert!(texts.iter().any(|(t, _)| t == "Distort & Transform"), "{texts:?}");
}