mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 04:07:12 -06:00
The Artboard Options help pages describe two display options (https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/modify-display-settings-of-artboards.html, .../set-video-display-options-for-artboards.html): a mark at the artboard's center, and lines from the middle of each side through the center. The document already had show_center_mark and show_cross_hairs on Artboard, but nothing set or drew them. artboard.setProps (each `artboards` entry too) and artboard.new take showCenterMark and showCrossHairs as true/false, in the same single undo step as the other fields; a locked artboard can still change them. Artboard Options has both checkboxes, filled in from the artboard whether it opens from the panel or a double-click with the Artboard tool, and its New Artboard form passes them to artboard.new. The canvas draws them in the new theme token artboard_mark, 1 point wide on screen at any zoom: a cross 12 points across at the center whose arms follow the artboard's sides, and the two cross hairs. They are screen-only like the edges; exports and print never read the fields. inspect_document reports them when on. Saved files are unchanged: the two keys were already always written, so a document that never shows them saves byte for byte as before, and a file without the keys opens with both off. Video safe areas, video rulers and the pixel aspect ratio are not part of this change. Verification: artboard_display_marks_are_set_and_cleared_in_one_undo_step, artboard_display_marks_round_trip_through_the_native_format, artboard_display_marks_stay_out_of_exports (SVG and PDF identical with the marks on and off), an_artboard_shows_its_center_mark_and_cross_hairs (positions, screen-constant size under zoom, rotated view) and artboard_options_show_center_mark_and_cross_hairs. cargo xtask ci: all 10 steps passed.
2330 lines
118 KiB
Rust
2330 lines
118 KiB
Rust
//! The dialog registry and the shared frame, drawn headlessly.
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_doc::Unit;
|
||
use vectorcraft_engine::Session;
|
||
|
||
use super::*;
|
||
use crate::theme;
|
||
|
||
fn app() -> VectorcraftApp {
|
||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||
app
|
||
}
|
||
|
||
/// One headless frame of the dialog layer.
|
||
fn frame(app: &mut VectorcraftApp, input: egui::RawInput) {
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let mut out = ctx.run_ui(input, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
}
|
||
|
||
fn enter() -> egui::RawInput {
|
||
let key = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||
egui::RawInput { events: vec![key], ..Default::default() }
|
||
}
|
||
|
||
fn open(app: &mut VectorcraftApp, kind: &str, fields: serde_json::Value) {
|
||
app.ui.dialog = Some(Dialog::new(kind, fields));
|
||
}
|
||
|
||
fn objects(app: &VectorcraftApp) -> usize {
|
||
app.session.doc().unwrap().doc.layers.iter().map(|l| l.children().map_or(0, |c| c.len())).sum()
|
||
}
|
||
|
||
#[test]
|
||
fn kinds_map_to_their_dialogs() {
|
||
for (kind, want) in [
|
||
("newDocument", DialogKind::NewDocument),
|
||
("roundedRectangle", DialogKind::Shape),
|
||
("lineSegment", DialogKind::Shape),
|
||
("arc", DialogKind::Shape),
|
||
("rectangularGrid", DialogKind::Shape),
|
||
("polarGrid", DialogKind::Shape),
|
||
("shear", DialogKind::Transform),
|
||
("splitIntoGrid", DialogKind::PathOp),
|
||
("artboardOptions", DialogKind::ArtboardOptions),
|
||
("command", DialogKind::Command),
|
||
("effect", DialogKind::Effect),
|
||
(crate::unsaved::KIND, DialogKind::SaveChanges),
|
||
("manageWorkspaces", DialogKind::Workspaces),
|
||
("findFont", DialogKind::FindFont),
|
||
] {
|
||
assert_eq!(DialogKind::of(kind), Some(want), "{kind}");
|
||
}
|
||
assert_eq!(DialogKind::of("nope"), None);
|
||
assert!(spec("effect").preview && spec("recolor").preview && !spec("rectangle").preview);
|
||
assert!(spec("preferences").window.is_some() && spec("rectangle").window.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn every_dialog_draws() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let openers: [(&str, serde_json::Value); 8] = [
|
||
("file.newDialog", json!({})),
|
||
("file.documentSetup", json!({})),
|
||
("file.exportForScreens", json!({})),
|
||
("ui.recolorDialog", json!({})),
|
||
("ui.paramDialog", json!({"command": "object.path.simplify", "label": "Simplify", "params": {"tolerance": 2, "csv": "a\nb"}})),
|
||
("effect.dialog", json!({"effect": "distort.roughen"})),
|
||
("edit.preferences", json!({})),
|
||
("type.findFont", json!({})),
|
||
];
|
||
for (id, p) in openers {
|
||
app.run(id, p).unwrap();
|
||
assert!(app.ui.dialog.is_some(), "{id} opened nothing");
|
||
frame(&mut app, Default::default());
|
||
app.ui.dialog = None;
|
||
let _ = app.session.cancel_interaction();
|
||
}
|
||
for kind in [
|
||
"rectangle",
|
||
"roundedRectangle",
|
||
"ellipse",
|
||
"polygon",
|
||
"star",
|
||
"lineSegment",
|
||
"arc",
|
||
"rectangularGrid",
|
||
"polarGrid",
|
||
"rotate",
|
||
"reflect",
|
||
"scale",
|
||
"shear",
|
||
"artboardOptions",
|
||
] {
|
||
open_tool_dialog(&mut app, kind, json!({"x": 5, "y": 5, "index": 0}));
|
||
assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == kind), "{kind}");
|
||
frame(&mut app, Default::default());
|
||
}
|
||
for (kind, fields) in [
|
||
("move", json!({"dx": "0 pt", "dy": "0 pt"})),
|
||
("average", json!({"axis": "both"})),
|
||
(crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"})),
|
||
("newWorkspace", json!({"name": "Mine"})),
|
||
("shortcuts", json!({})),
|
||
("unregistered", json!({"value": "1"})),
|
||
] {
|
||
open(&mut app, kind, fields);
|
||
frame(&mut app, Default::default());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn enter_confirms_through_the_dialog_spec() {
|
||
let mut app = app();
|
||
open_tool_dialog(&mut app, "rectangle", json!({"x": 10, "y": 20}));
|
||
frame(&mut app, enter());
|
||
assert!(app.ui.dialog.is_none());
|
||
assert_eq!(objects(&app), 1);
|
||
// An unregistered kind still closes on OK without running anything.
|
||
open(&mut app, "unregistered", json!({}));
|
||
assert_eq!(confirm(&mut app), Ok(serde_json::Value::Null));
|
||
assert!(app.ui.dialog.is_none());
|
||
assert_eq!(confirm(&mut app), Err("no dialog open".into()));
|
||
}
|
||
|
||
#[test]
|
||
fn confirm_runs_each_dialogs_command() {
|
||
let mut app = app();
|
||
open(&mut app, "star", json!({"x": 50, "y": 50, "radius1": "40 pt", "radius2": "20 pt", "points": 6}));
|
||
confirm(&mut app).unwrap();
|
||
open(&mut app, "lineSegment", json!({"x": 0, "y": 0, "length": "30 pt", "angle": 90}));
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(objects(&app), 2);
|
||
app.run("select.all", json!({})).unwrap();
|
||
// Scale with Uniform uses the horizontal value for both axes; Copy keeps the original.
|
||
open(&mut app, "scale", json!({"sx": 200, "sy": 50, "uniform": true, "copy": true}));
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(objects(&app), 4);
|
||
// The parameter dialog closes before its command runs, then runs it with the edited values.
|
||
app.run("ui.paramDialog", json!({"command": "view.saved.new", "label": "New View", "params": {"name": "Close-up"}})).unwrap();
|
||
confirm(&mut app).unwrap();
|
||
assert!(app.ui.dialog.is_none());
|
||
assert_eq!(app.session.doc().unwrap().doc.views.last().map(|v| v.name.as_str()), Some("Close-up"));
|
||
// New Document creates the document and closes.
|
||
let docs = app.session.documents().len();
|
||
open(
|
||
&mut app,
|
||
"newDocument",
|
||
json!({"width": "300 pt", "height": "200 pt", "units": "Points", "name": "Card", "artboards": 1, "colorMode": "RGB"}),
|
||
);
|
||
confirm(&mut app).unwrap();
|
||
assert!(app.ui.dialog.is_none());
|
||
assert_eq!(app.session.documents().len(), docs + 1);
|
||
}
|
||
|
||
#[test]
|
||
fn effect_preview_is_kept_as_one_undo_step() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
app.run("effect.dialog", json!({"effect": "distort.roughen"})).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true));
|
||
frame(&mut app, Default::default());
|
||
assert!(app.session.in_interaction(), "the preview runs as an interaction");
|
||
confirm(&mut app).unwrap();
|
||
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
|
||
assert_eq!(app.last_effect.as_ref().map(|e| e.0.as_str()), Some("distort.roughen"));
|
||
assert_ne!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
|
||
/// Drop Shadow's Color typed as text the effect can't draw: OK returns the error and keeps the
|
||
/// dialog open, with Preview on or off, and the document stays as it was.
|
||
#[test]
|
||
fn effect_dialog_refuses_a_color_it_cannot_draw() {
|
||
for preview in [true, false] {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
app.run("effect.dialog", json!({"effect": "stylize.dropShadow"})).unwrap();
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
d.fields.insert("preview".into(), json!(preview));
|
||
d.fields.insert("color".into(), json!("not-a-color"));
|
||
frame(&mut app, Default::default());
|
||
let e = confirm(&mut app).unwrap_err();
|
||
assert!(e.contains("`color` must be a #rrggbb color"), "preview {preview}: {e}");
|
||
assert!(app.ui.dialog.is_some() && app.last_effect.is_none(), "preview {preview}");
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "preview {preview}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn raster_filter_dialogs_preview_and_apply() {
|
||
for (effect, key, value) in [
|
||
("artistic.cutout", "levels", "6"),
|
||
("artistic.neonGlow", "color", "#ff00ff"),
|
||
("artistic.posterEdges", "edgeThickness", "5"),
|
||
("artistic.smudgeStick", "strokeLength", "6"),
|
||
("artistic.sponge", "definition", "20"),
|
||
("artistic.watercolor", "texture", "2"),
|
||
("blur.radial", "method", "zoom"),
|
||
("blur.smart", "quality", "high"),
|
||
("blur.smart", "blurMode", "overlayEdge"),
|
||
("sharpen.unsharpMask", "amount", "120"),
|
||
("pixelate.colorHalftone", "channel1", "30"),
|
||
("pixelate.crystallize", "cellSize", "20"),
|
||
("pixelate.mezzotint", "type", "longLines"),
|
||
("pixelate.pointillize", "cellSize", "12"),
|
||
("sketch.basRelief", "lightDirection", "left"),
|
||
("sketch.charcoal", "thickness", "5"),
|
||
("sketch.conteCrayon", "texture", "brick"),
|
||
("sketch.conteCrayon", "foregroundLevel", "16"),
|
||
("sketch.halftonePattern", "patternType", "line"),
|
||
("sketch.photocopy", "darkness", "40"),
|
||
("sketch.stamp", "lightDarkBalance", "10"),
|
||
("sketch.plaster", "lightDirection", "bottomLeft"),
|
||
("texture.craquelure", "crackDepth", "9"),
|
||
("texture.grain", "grainType", "stippled"),
|
||
("texture.mosaicTiles", "tileSize", "20"),
|
||
("texture.patchwork", "relief", "20"),
|
||
("texture.stainedGlass", "borderThickness", "2"),
|
||
("texture.texturizer", "lightDirection", "bottomLeft"),
|
||
] {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": effect})).unwrap();
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
let v: serde_json::Value = value.parse::<f64>().map_or_else(|_| json!(value), |n| json!(n));
|
||
d.fields.insert(key.into(), v.clone());
|
||
frame(&mut app, Default::default());
|
||
assert!(app.session.in_interaction(), "{effect}: previews");
|
||
confirm(&mut app).unwrap();
|
||
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||
assert_eq!(n.appearance.effects.len(), 1, "{effect}");
|
||
assert_eq!(n.appearance.effects[0].params[key], v, "{effect}");
|
||
}
|
||
// Fields follow the effect's documented order (its dialog's), not the alphabet.
|
||
let doc = vectorcraft_effects::effect_info("blur.smart").unwrap().params;
|
||
let ranks: Vec<usize> = ["radius", "threshold", "quality", "blurMode"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[3] < usize::MAX, "{ranks:?}");
|
||
assert_eq!(super::effect::doc_rank(doc, "nope"), usize::MAX);
|
||
let doc = vectorcraft_effects::effect_info("pixelate.colorHalftone").unwrap().params;
|
||
let ranks: Vec<usize> = ["maxRadius", "channel1", "channel2", "channel3", "channel4"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[4] < usize::MAX, "{ranks:?}");
|
||
// Labels as the dialogs show them.
|
||
assert_eq!(super::form::humanize("maxRadius"), "Max. Radius:");
|
||
assert_eq!(super::form::humanize("blurMode"), "Mode:");
|
||
assert_eq!(super::form::humanize("channel3"), "Channel 3:");
|
||
assert_eq!(super::form::humanize("cellSize"), "Cell Size:");
|
||
assert_eq!(super::form::humanize("lightDirection"), "Light Direction:");
|
||
let doc = vectorcraft_effects::effect_info("texture.texturizer").unwrap().params;
|
||
let ranks: Vec<usize> = ["texture", "scaling", "relief", "lightDirection", "invert"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[4] < usize::MAX, "{ranks:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn effect_dialog_applies_to_its_appearance_item() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": "distort.roughen", "item": 1})).unwrap();
|
||
frame(&mut app, Default::default());
|
||
confirm(&mut app).unwrap();
|
||
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||
assert_eq!(n.appearance.items[1].effects().len(), 1, "the effect goes to the stroke");
|
||
assert!(n.appearance.effects.is_empty() && n.appearance.items[0].effects().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn scribble_dialog_shows_the_angle_as_stored_and_each_variation() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": "stylize.scribble"})).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("angle".into(), json!(270));
|
||
frame(&mut app, Default::default());
|
||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["angle"].as_f64(), Some(-90.0), "the field reads what is stored");
|
||
confirm(&mut app).unwrap();
|
||
let n = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||
assert_eq!(n.appearance.effects[0].params["angle"].as_f64(), Some(-90.0));
|
||
// Each variation follows the option it varies, as in the reference app's Scribble Options.
|
||
let doc = vectorcraft_effects::effect_info("stylize.scribble").unwrap().params;
|
||
let order = ["angle", "overlap", "overlapVariation", "strokeWidth", "curviness", "curvinessVariation", "spacing", "spacingVariation", "seed"];
|
||
let ranks: Vec<usize> = order.iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[8] < usize::MAX, "{ranks:?}");
|
||
for key in ["overlapVariation", "curvinessVariation", "spacingVariation"] {
|
||
assert_eq!(super::form::humanize(key), "Variation:");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn revolve_dialog_preview_cancel_confirm_and_undo() {
|
||
let mut app = app();
|
||
app.run("path.create", json!({"d":"M150 40 L180 60 L180 140 L150 160"})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
app.run("effect.dialog", json!({"effect":"threeD.revolve","item":null})).unwrap();
|
||
frame(&mut app, Default::default());
|
||
assert!(app.session.in_interaction());
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("effect.dialog", json!({"effect":"threeD.revolve","item":null})).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("angle".into(), json!(180));
|
||
frame(&mut app, Default::default());
|
||
confirm(&mut app).unwrap();
|
||
assert_ne!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
|
||
#[test]
|
||
fn extrude_inflate_and_rotate_dialogs_preview_confirm_and_undo() {
|
||
for effect in vectorcraft_effects::SOLIDS {
|
||
let mut app = app();
|
||
app.run("path.create", json!({"d":"M100 100 L200 100 L200 200 L100 200 Z"})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
app.run("effect.dialog", json!({"effect": effect, "item": null})).unwrap();
|
||
frame(&mut app, Default::default());
|
||
assert!(app.session.in_interaction(), "{effect}: previewing");
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "{effect}: Cancel restores");
|
||
app.run("effect.dialog", json!({"effect": effect, "item": null})).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("rotationY".into(), json!(45));
|
||
frame(&mut app, Default::default());
|
||
confirm(&mut app).unwrap();
|
||
let doc = &app.session.doc().unwrap().doc;
|
||
let n = doc.node(doc.layers[0].children().unwrap()[0].id).unwrap();
|
||
assert_eq!(n.appearance.effects.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [effect]);
|
||
assert_eq!(n.appearance.effects[0].params["rotationY"], json!(45));
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "{effect}: one undo step");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn dialogs_are_as_tall_as_their_content() {
|
||
// The save prompt, shown after a taller dialog, is as tall as its text and buttons: no empty
|
||
// band around the buttons, nothing inherited from the other dialog.
|
||
let mut app = app();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
|
||
let height = |app: &mut VectorcraftApp, kind: &str, fields: serde_json::Value| {
|
||
open(app, kind, fields);
|
||
for _ in 0..30 {
|
||
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
|
||
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
|
||
}
|
||
ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind)))).unwrap().height()
|
||
};
|
||
let tall = height(&mut app, "command", json!({"__command": "object.move", "a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7, "h": 8}));
|
||
let prompt = height(&mut app, crate::unsaved::KIND, json!({"index": 0, "name": "Untitled-1", "then": "close"}));
|
||
assert!(prompt < 180.0 && prompt < tall, "save prompt {prompt} pt tall (the dialog before it: {tall} pt)");
|
||
}
|
||
|
||
#[test]
|
||
fn dialogs_do_not_stretch_to_the_screen() {
|
||
// A dialog is as tall as its content: the same on a short and a tall screen.
|
||
let openers: &[(&str, serde_json::Value)] = &[
|
||
("ui.newSwatch", json!({})),
|
||
("ui.newColorGroup", json!({})),
|
||
("ui.swatchOptions", json!({"name": "White"})),
|
||
("ui.colorPicker", json!({})),
|
||
("ui.graphicStyleOptions", json!({})),
|
||
("ui.newArtboard", json!({})),
|
||
("ui.brushOptions", json!({"name": "3 pt. Round"})),
|
||
("ui.brushOptions", json!({"name": "Bristle Round"})),
|
||
("ui.colorGuideOptions", json!({})),
|
||
("ui.colorBalanceDialog", json!({})),
|
||
("ui.saturateDialog", json!({})),
|
||
("ui.saveSwatchLibrary", json!({})),
|
||
("ui.flattenTransparencyDialog", json!({})),
|
||
("ui.expandDialog", json!({})),
|
||
("ui.spotColors", json!({})),
|
||
("ui.paramDialog", json!({"command": "object.path.simplify", "label": "Simplify", "params": {"tolerance": 2}})),
|
||
("effect.dialog", json!({"effect": "distort.roughen"})),
|
||
("ui.recolorDialog", json!({})),
|
||
];
|
||
let height = |id: &str, p: &serde_json::Value, screen_h: f32| -> Option<(String, f32)> {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
app.run(id, p.clone()).ok()?;
|
||
let kind = app.ui.dialog.as_ref()?.kind.clone();
|
||
spec(&kind).window.is_none().then_some(())?;
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, screen_h));
|
||
for _ in 0..4 {
|
||
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
|
||
ctx.run_ui(input, |ui| show(&mut app, ui.ctx())).textures_delta.clear();
|
||
}
|
||
let h = ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind.as_str()))))?.height();
|
||
Some((kind, h))
|
||
};
|
||
let mut checked = 0;
|
||
for (id, p) in openers {
|
||
let (Some((kind, short)), Some((_, tall))) = (height(id, p, 1000.0), height(id, p, 2000.0)) else { continue };
|
||
assert!((short - tall).abs() < 1.0, "{kind} is {short} pt tall on a 1000 pt screen but {tall} pt on a 2000 pt one");
|
||
checked += 1;
|
||
}
|
||
assert!(checked >= 10, "only {checked} dialogs drew in the shared frame");
|
||
}
|
||
|
||
/// The dialog window of `app`'s open dialog after a few frames in a `w` × 900 window.
|
||
fn dialog_rect(app: &mut VectorcraftApp, w: f32) -> egui::Rect {
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w, 900.0));
|
||
let kind = app.ui.dialog.as_ref().unwrap().kind.clone();
|
||
for _ in 0..3 {
|
||
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
|
||
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
|
||
}
|
||
ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", kind.as_str())))).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn a_form_dialog_is_as_wide_as_its_fields_not_the_window() {
|
||
let mut app = app();
|
||
// Clean Up: three check boxes in the generic parameter dialog.
|
||
let params = json!({"emptyTextPaths": true, "strayPoints": true, "unpaintedObjects": true});
|
||
app.run("ui.paramDialog", json!({"command": "object.path.cleanUp", "label": "Clean Up", "params": params})).unwrap();
|
||
let wide = dialog_rect(&mut app, 1600.0);
|
||
assert!(wide.width() < 500.0, "Clean Up is {:.0} wide in a 1600-point window", wide.width());
|
||
// The same dialog in a narrow window still fits it.
|
||
let narrow = dialog_rect(&mut app, 360.0);
|
||
assert!(narrow.width() <= 360.0, "{:.0} wide in a 360-point window", narrow.width());
|
||
}
|
||
|
||
/// Object › Path › Split Into Grid opens on a grid the size of the selection, shows its rows and
|
||
/// columns side by side (number, size, gutter, total), previews on the canvas and, on OK, splits
|
||
/// the path and adds the guides as one undo step.
|
||
#[test]
|
||
fn split_into_grid_opens_at_the_selections_size_and_previews() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 112, "height": 62})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let undo = app.session.doc().unwrap().history.undo.len();
|
||
crate::menus::invoke(&mut app, "object.path.splitIntoGrid", json!({}));
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
assert_eq!((d.f64("totalWidth", 0.0), d.f64("totalHeight", 0.0), d.bool("addGuides")), (112.0, 62.0, false));
|
||
d.fields.extend([
|
||
("columns".to_string(), json!(3)),
|
||
("columnGutter".to_string(), json!(8)),
|
||
("addGuides".into(), json!(true)),
|
||
("preview".into(), json!(true)),
|
||
]);
|
||
frame(&mut app, Default::default());
|
||
// The preview: 2 × 3 cells and the guides along their edges, not yet an undo step.
|
||
assert_eq!((objects(&app), app.session.doc().unwrap().doc.guides.len()), (6, 10));
|
||
assert!(app.session.in_interaction());
|
||
confirm(&mut app).unwrap();
|
||
let st = app.session.doc().unwrap();
|
||
assert_eq!((objects(&app), st.doc.guides.len(), st.history.undo.len()), (6, 10, undo + 1));
|
||
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
|
||
// Cancel rolls a preview back.
|
||
app.run("select.all", json!({})).unwrap();
|
||
crate::menus::invoke(&mut app, "object.path.splitIntoGrid", json!({}));
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true));
|
||
frame(&mut app, Default::default());
|
||
assert_eq!(objects(&app), 4, "one grid over the six cells");
|
||
cancel(&mut app);
|
||
assert_eq!(objects(&app), 6);
|
||
}
|
||
|
||
#[test]
|
||
fn menu_parameter_dialogs_are_compact() {
|
||
// #292: Object › Path › Simplify (and its neighbours) spanned the whole window.
|
||
for id in ["object.path.simplify", "object.path.offsetPath", "object.move", "object.rotate", "object.path.splitIntoGrid", "path.average"] {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
crate::menus::invoke(&mut app, id, json!({}));
|
||
// Simplify opens its slider; More Options opens the dialog.
|
||
if id == "object.path.simplify" {
|
||
app.run("ui.simplifyMoreOptions", json!({})).unwrap();
|
||
}
|
||
assert!(app.ui.dialog.is_some(), "{id} opened no dialog");
|
||
let wide = dialog_rect(&mut app, 1600.0);
|
||
eprintln!("{id}: {:.0} × {:.0} in a 1600-point window", wide.width(), wide.height());
|
||
assert!(wide.width() < 420.0, "{id} is {:.0} wide in a 1600-point window", wide.width());
|
||
}
|
||
}
|
||
|
||
/// A list mixing built-in labels with names translates only the built-in entries: names that
|
||
/// happen to be catalog keys ("Black", "Regular") are shown as they are.
|
||
#[test]
|
||
fn mixed_lists_translate_only_their_built_in_entries() {
|
||
use crate::i18n::{Lang, tr};
|
||
let zh = Lang::from_code("zh-hant").unwrap();
|
||
for key in ["None", "Black", "Regular", "Default"] {
|
||
assert_ne!(tr(zh, key), key, "{key} must be a catalog key for this test");
|
||
}
|
||
let names = ["None", "Black", "Regular", "Default"];
|
||
assert_eq!(shown_names(zh, &names, |k| k == 0 || k == 3), [tr(zh, "None"), "Black", "Regular", tr(zh, "Default")]);
|
||
assert_eq!(shown_names(zh, &names, |_| false), names);
|
||
assert_eq!(shown_names(Lang::EN, &names, |_| true), names);
|
||
}
|
||
|
||
/// Confirmations and plug-in dialogs are headed by the text they are given (callers translate
|
||
/// their own templates; a plug-in's name is its own).
|
||
#[test]
|
||
fn given_headings_are_shown_as_they_are() {
|
||
let d = Dialog::new(confirm::KIND, json!({"message": "Delete “Black”?", "detail": "Regular"}));
|
||
assert_eq!((spec(&d.kind).heading)(&d), "Delete “Black”?");
|
||
let d = Dialog::new(plugin::KIND, json!({"__label": "Black", "__plugin": "x"}));
|
||
assert_eq!((spec(&d.kind).heading)(&d), "Black");
|
||
}
|
||
|
||
/// A plug-in dialog with one whole-number field `seed` in `min..=max`, driven by egui events.
|
||
struct PluginIntField {
|
||
ctx: egui::Context,
|
||
d: Dialog,
|
||
m: vectorcraft_plugins::Manifest,
|
||
pos: egui::Pos2,
|
||
}
|
||
|
||
impl PluginIntField {
|
||
/// The dialog for a `seed` in `min..=max` showing `start`, drawn once to find the number,
|
||
/// where drags start (`pos`).
|
||
fn new(min: i64, max: i64, start: i64) -> Self {
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let m = vectorcraft_plugins::Manifest {
|
||
id: "test.seed".into(),
|
||
name: "Seed".into(),
|
||
version: String::new(),
|
||
kind: vectorcraft_plugins::Kind::Filter,
|
||
description: String::new(),
|
||
author: String::new(),
|
||
params: vec![("seed".into(), vectorcraft_plugins::ParamSpec::Int { min, max, default: 0 }.into())],
|
||
extensions: Vec::new(),
|
||
budget: Default::default(),
|
||
};
|
||
let mut f = PluginIntField { ctx, d: Dialog::new(plugin::KIND, json!({ "seed": start })), m, pos: egui::Pos2::ZERO };
|
||
fn number_rect(shape: &egui::Shape, text: &str) -> Option<egui::Rect> {
|
||
match shape {
|
||
egui::Shape::Text(t) if t.galley.text() == text => Some(t.visual_bounding_rect()),
|
||
egui::Shape::Vec(shapes) => shapes.iter().find_map(|s| number_rect(s, text)),
|
||
_ => None,
|
||
}
|
||
}
|
||
let out = f.run(vec![]);
|
||
f.pos = out.shapes.iter().find_map(|s| number_rect(&s.shape, &start.to_string())).unwrap().center();
|
||
f
|
||
}
|
||
|
||
fn run(&mut self, events: Vec<egui::Event>) -> egui::FullOutput {
|
||
let (d, m) = (&mut self.d, &self.m);
|
||
let mut out = self.ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||
form::schema_rows(ui, d, m);
|
||
});
|
||
out.textures_delta.clear();
|
||
out
|
||
}
|
||
|
||
/// Clicks the field, sends `events`, presses Enter and returns the field's value.
|
||
fn commit(&mut self, events: Vec<egui::Event>) -> serde_json::Value {
|
||
use egui::{Event, Key, Modifiers, PointerButton};
|
||
let (pos, button) = (self.pos, PointerButton::Primary);
|
||
self.run(vec![Event::PointerMoved(pos), Event::PointerButton { pos, button, pressed: true, modifiers: Modifiers::default() }]);
|
||
self.run(vec![Event::PointerButton { pos, button, pressed: false, modifiers: Modifiers::default() }]);
|
||
self.run(events);
|
||
self.run(vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
|
||
self.d.fields["seed"].clone()
|
||
}
|
||
|
||
/// Replaces the field's text with `text` and commits it.
|
||
fn type_text(&mut self, text: &str) -> serde_json::Value {
|
||
let all = egui::Modifiers { ctrl: true, command: true, ..Default::default() };
|
||
self.commit(vec![
|
||
egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: all },
|
||
egui::Event::Text(text.into()),
|
||
])
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn plugin_integer_field_preserves_precision_and_clamps_exactly() {
|
||
let (min, max) = (-9007199254740995_i64, 9007199254740995_i64);
|
||
let mut f = PluginIntField::new(min, max, 0);
|
||
for (text, expected) in [
|
||
("9007199254740993", 9007199254740993_i64),
|
||
("9007199254740996", max),
|
||
("-9007199254740996", min),
|
||
("invalid", min),
|
||
("1.5", min),
|
||
("9223372036854775808", min),
|
||
] {
|
||
assert_eq!(f.type_text(text), json!(expected), "{text}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn plugin_integer_field_with_small_bounds_keeps_numeric_editing() {
|
||
let mut f = PluginIntField::new(1, 16, 3);
|
||
let up = egui::Event::Key { key: egui::Key::ArrowUp, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||
assert_eq!(f.commit(vec![up]), json!(4));
|
||
for (text, expected) in [("7", 7), ("2 + 3", 5), ("17", 16)] {
|
||
assert_eq!(f.type_text(text), json!(expected), "{text}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_dialog_opens_with_its_first_field_focused_so_typing_and_enter_apply() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
};
|
||
let enter = || egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||
let bounds = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap();
|
||
// Object › Transform › Rotate…: type 90 over the angle, press Enter.
|
||
crate::menus::invoke(&mut app, "object.rotate", json!({}));
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("90".into())]);
|
||
frame(&mut app, vec![enter()]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let b = bounds(&app);
|
||
assert!((b.width() - 20.0).abs() < 1e-6 && (b.height() - 40.0).abs() < 1e-6, "{b:?}");
|
||
// Move…: its first field is a distance (with a unit): typing replaces it as well.
|
||
let before = bounds(&app);
|
||
crate::menus::invoke(&mut app, "object.move", json!({}));
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("15".into())]);
|
||
frame(&mut app, vec![enter()]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let after = bounds(&app);
|
||
assert!((after.x0 - before.x0 - 15.0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} → {after:?}");
|
||
// Transform Each, then the Transform effect: Scale › Horizontal, a plain number, comes first.
|
||
let before = bounds(&app);
|
||
crate::menus::invoke(&mut app, "object.transformEach", json!({}));
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("50".into())]);
|
||
frame(&mut app, vec![enter()]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let after = bounds(&app);
|
||
assert!((after.width() - before.width() / 2.0).abs() < 1e-6 && (after.height() - before.height()).abs() < 1e-6, "{before:?} → {after:?}");
|
||
app.run("effect.dialog", json!({"effect": "distort.transform"})).unwrap();
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("50".into())]);
|
||
frame(&mut app, vec![enter()]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let node = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||
assert_eq!(node.appearance.effects.last().map(|e| e.params["scaleH"].clone()), Some(json!(50.0)));
|
||
}
|
||
|
||
/// In a dialog, Shift+Enter in a field applies its value and confirms the dialog, as Enter does:
|
||
/// the Move dialog moves the art, and the New Document dialog creates a document as wide as typed.
|
||
/// Panel fields keep the focus on Shift+Enter (`tests_numfields`).
|
||
#[test]
|
||
fn shift_enter_in_a_dialog_field_applies_it_and_confirms_the_dialog() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 900.0)));
|
||
let mut time = 0.0;
|
||
// Whole app frames: the panels, then the dialog.
|
||
let mut frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
time += 0.1;
|
||
let mut out = ctx.run_ui(egui::RawInput { events, time: Some(time), screen_rect, ..Default::default() }, |ui| {
|
||
app.logic(ui.ctx());
|
||
app.ui(ui);
|
||
});
|
||
out.textures_delta.clear();
|
||
out
|
||
};
|
||
let shift_enter =
|
||
|| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT };
|
||
let bounds = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap();
|
||
// Move…: type 15 over its first field and press Shift+Enter.
|
||
let before = bounds(&app);
|
||
crate::menus::invoke(&mut app, "object.move", json!({}));
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("15".into())]);
|
||
frame(&mut app, vec![shift_enter()]);
|
||
assert!(app.ui.dialog.is_none(), "Shift+Enter confirms the Move dialog");
|
||
let after = bounds(&app);
|
||
assert!((after.x0 - before.x0 - 15.0).abs() < 1e-6 && (after.y0 - before.y0).abs() < 1e-6, "{before:?} → {after:?}");
|
||
// New Document (the recent 200 × 200 pt size): click into Width, type 300 and press Shift+Enter.
|
||
app.run("file.newDialog", json!({})).unwrap();
|
||
frame(&mut app, vec![]);
|
||
let out = frame(&mut app, vec![]);
|
||
fn walk(s: &egui::Shape, out: &mut Vec<(String, egui::Pos2)>) {
|
||
match s {
|
||
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)),
|
||
egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect().center())),
|
||
_ => {}
|
||
}
|
||
}
|
||
let mut texts = vec![];
|
||
out.shapes.iter().for_each(|c| walk(&c.shape, &mut texts));
|
||
// The Width field comes right after its caption.
|
||
let width = texts.iter().position(|(t, _)| t == "Width").and_then(|i| texts.get(i + 1));
|
||
assert_eq!(width.map(|(t, _)| t.as_str()), Some("200 pt"), "{texts:?}");
|
||
let at = width.map(|(_, p)| *p).unwrap();
|
||
let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||
frame(&mut app, vec![egui::Event::PointerMoved(at), press(true)]);
|
||
frame(&mut app, vec![press(false)]);
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("300".into())]);
|
||
frame(&mut app, vec![shift_enter()]);
|
||
assert!(app.ui.dialog.is_none(), "Shift+Enter creates the document");
|
||
assert_eq!(app.session.active().unwrap().doc.artboards[0].rect.width(), 300.0);
|
||
}
|
||
|
||
/// #1367: a dialog opened by a click (its menu item) is drawn in the frame of that click, where egui
|
||
/// takes the focus from a field the click missed: its first field still has the focus, each time it
|
||
/// opens.
|
||
#[test]
|
||
fn a_dialog_opened_by_a_click_focuses_its_first_field_each_time_it_opens() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
};
|
||
// A click away from the dialog, as on a menu item.
|
||
let click = || {
|
||
let (pos, button, modifiers) = (egui::pos2(5.0, 5.0), egui::PointerButton::Primary, egui::Modifiers::NONE);
|
||
let press = |pressed| egui::Event::PointerButton { pos, button, pressed, modifiers };
|
||
vec![egui::Event::PointerMoved(pos), press(true), press(false)]
|
||
};
|
||
type Open = fn(&mut VectorcraftApp);
|
||
let opens: [(&str, Open); 2] = [
|
||
("Convert to Shape", |app| drop(app.run("effect.dialog", json!({"effect": "convertToShape.rectangle"})))),
|
||
("Rasterize", |app| crate::menus::invoke(app, "object.rasterize", json!({}))),
|
||
];
|
||
for (name, open) in opens {
|
||
for turn in 1..=3 {
|
||
open(&mut app);
|
||
assert!(app.ui.dialog.is_some(), "{name}");
|
||
frame(&mut app, click());
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![]);
|
||
assert!(ctx.memory(|m| m.focused()).is_some(), "{name}, turn {turn}");
|
||
cancel(&mut app);
|
||
// A frame without the dialog, as between two uses of it.
|
||
frame(&mut app, vec![]);
|
||
assert!(ctx.memory(|m| m.focused()).is_none(), "{name}, turn {turn}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A greyed-out field can't hold the keyboard focus, so it doesn't take a just-opened dialog's
|
||
/// either: the focus waits for the first enabled field. Blend Options on Smooth Color shows its
|
||
/// steps greyed out; choosing Specified Steps enables the field and it takes the focus (type a
|
||
/// count, press Enter).
|
||
#[test]
|
||
fn a_dialogs_focus_skips_a_greyed_out_first_field() {
|
||
let mut app = app();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
};
|
||
let enter = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||
app.run("ui.blendOptions", json!({})).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("spacing".into(), json!("smooth"));
|
||
for _ in 0..3 {
|
||
frame(&mut app, vec![]);
|
||
}
|
||
assert!(ctx.memory(|m| m.focused()).is_none(), "nothing to type in yet");
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("spacing".into(), json!("steps"));
|
||
frame(&mut app, vec![]);
|
||
assert!(ctx.memory(|m| m.focused()).is_some(), "the steps field takes the focus once it is enabled");
|
||
frame(&mut app, vec![egui::Event::Text("7".into())]);
|
||
frame(&mut app, vec![enter]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let info = app.run("object.blend.info", json!({})).unwrap();
|
||
assert_eq!((info["spacing"].clone(), info["steps"].clone()), (json!("steps"), json!(7)), "{info}");
|
||
}
|
||
|
||
/// #1692: a value typed into the field before the # field of the Color Picker or Swatch Options
|
||
/// stays when Tab passes through the # field. The # field takes the focus in the frame the value is
|
||
/// committed, still showing the color from before it, and commits nothing when Tab moves on. A hex
|
||
/// typed into the # field is still committed by Tab.
|
||
#[test]
|
||
fn tab_through_the_hex_field_keeps_the_value_typed_before_it() {
|
||
let mut app = app();
|
||
app.run("swatch.new", json!({"name": "Sky", "color": "#3366cc"})).unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
// One frame of the dialog layer: every text drawn, with its center.
|
||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
fn collect(s: &egui::Shape, out: &mut Vec<(String, egui::Pos2)>) {
|
||
match s {
|
||
egui::Shape::Text(t) => out.push((t.galley.text().to_string(), t.visual_bounding_rect().center())),
|
||
egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
|
||
_ => {}
|
||
}
|
||
}
|
||
let mut texts = vec![];
|
||
out.shapes.iter().for_each(|c| collect(&c.shape, &mut texts));
|
||
texts
|
||
};
|
||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||
let (tab, back) = (key(egui::Key::Tab, egui::Modifiers::NONE), key(egui::Key::Tab, egui::Modifiers::SHIFT));
|
||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||
let color = |app: &VectorcraftApp| app.ui.dialog.as_ref().unwrap().fields["color"].clone();
|
||
type Open = fn(&mut VectorcraftApp);
|
||
let opens: [(&str, Open, &str); 2] = [
|
||
("Color Picker", |app| drop(app.run("ui.colorPicker", json!({"color": "#ffffff"}))), "FFFFFF"),
|
||
("Swatch Options", |app| drop(app.run("ui.swatchOptions", json!({"name": "Sky"}))), "3366CC"),
|
||
];
|
||
for (name, open, hex) in opens {
|
||
open(&mut app);
|
||
let before = color(&app);
|
||
for _ in 0..3 {
|
||
frame(&mut app, vec![]);
|
||
}
|
||
let at = frame(&mut app, vec![]).into_iter().find(|(t, _)| t == hex).unwrap_or_else(|| panic!("{name}: no # field shows {hex}")).1;
|
||
// Click into the # field, then Shift+Tab to the field before it and type 40 over its value.
|
||
frame(&mut app, vec![egui::Event::PointerMoved(at), button(at, true)]);
|
||
frame(&mut app, vec![button(at, false)]);
|
||
frame(&mut app, vec![back.clone()]);
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("40".into())]);
|
||
// Tab commits it and gives the # field the focus; the next Tab moves on.
|
||
frame(&mut app, vec![tab.clone()]);
|
||
let edited = color(&app);
|
||
assert_ne!(edited, before, "{name}: the field before # took 40");
|
||
for events in [vec![], vec![tab.clone()], vec![]] {
|
||
frame(&mut app, events);
|
||
}
|
||
assert_eq!(color(&app), edited, "{name}: passing through # put the color from before the edit back");
|
||
// Click into the # field, select its text, type a hex over it and press Tab.
|
||
frame(&mut app, vec![egui::Event::PointerMoved(at), button(at, true)]);
|
||
frame(&mut app, vec![button(at, false)]);
|
||
frame(&mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND)]);
|
||
frame(&mut app, vec![egui::Event::Text("00FF00".into())]);
|
||
frame(&mut app, vec![tab.clone()]);
|
||
assert!(frame(&mut app, vec![]).iter().any(|(t, _)| t == "00FF00"), "{name}: the # field took 00FF00");
|
||
cancel(&mut app);
|
||
frame(&mut app, vec![]);
|
||
}
|
||
}
|
||
|
||
/// #1000: a count typed in Object › Create Object Mosaic… reaches the command (a whole number typed
|
||
/// in a command's dialog is an integer), so the mosaic has that many columns, not the 10 × 10 grid.
|
||
#[test]
|
||
fn object_mosaic_makes_the_grid_typed_in_its_dialog() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 60, "height": 40})).unwrap();
|
||
app.run("object.rasterize", json!({"ppi": 72})).unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
};
|
||
let entry = crate::menus::menu_entries(&app).into_iter().find(|e| e.label == "Create Object Mosaic…").expect("menu item");
|
||
let (cmd, params) = crate::menus::click_target(&entry.label, entry.command.as_deref().unwrap(), &entry.params);
|
||
crate::menus::invoke(&mut app, &cmd, params);
|
||
// Its first field, Columns, is focused: type 3 over it and press Enter.
|
||
frame(&mut app, vec![]);
|
||
frame(&mut app, vec![egui::Event::Text("3".into())]);
|
||
let enter = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||
frame(&mut app, vec![enter]);
|
||
assert!(app.ui.dialog.is_none());
|
||
let mut tiles = None;
|
||
app.session.doc().unwrap().doc.walk(|n| {
|
||
if n.name.as_deref() == Some("Object Mosaic") {
|
||
tiles = n.children().map(|c| c.len());
|
||
}
|
||
});
|
||
assert_eq!(tiles, Some(3 * 10));
|
||
}
|
||
|
||
/// #793: Artboard Options lists its fields as Name, Width, Height, not by key.
|
||
#[test]
|
||
fn artboard_options_lists_name_then_width_and_height() {
|
||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||
app.session.execute("file.new", &json!({"width": 612, "height": 792})).unwrap();
|
||
open(&mut app, "artboardOptions", json!({"index": 0, "name": "Artboard 1", "width": 612, "height": 792, "x": 0, "y": 0}));
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
let at = |s: &str| text.find(s).unwrap_or_else(|| panic!("{s} in {text}"));
|
||
assert!(at("Name") < at("Width") && at("Width") < at("Height"), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_transform_effect_dialog_has_its_controls_previews_them_and_shows_them_again() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": "distort.transform"})).unwrap();
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
for s in [
|
||
"Transform",
|
||
"Scale",
|
||
"Move",
|
||
"Rotate",
|
||
"Horizontal:",
|
||
"Vertical:",
|
||
"Angle:",
|
||
"Copies:",
|
||
"Reflect X",
|
||
"Reflect Y",
|
||
"Reference Point",
|
||
"Random",
|
||
"Transform Objects",
|
||
"Transform Patterns",
|
||
"Scale Strokes & Effects",
|
||
"Preview",
|
||
] {
|
||
assert!(text.lines().any(|l| l == s), "{s} in {text}");
|
||
}
|
||
assert!(app.session.in_interaction(), "the preview runs");
|
||
// The options start at the catalog defaults (#885).
|
||
let d = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!((d.bool("objects"), d.bool("patterns"), d.bool("strokes")), (true, false, false));
|
||
let effect = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].clone();
|
||
// The reference point and Random update the preview.
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
for (k, v) in [("moveH", json!(30)), ("copies", json!(3)), ("reference", json!(8))] {
|
||
d.fields.insert(k.into(), v);
|
||
}
|
||
frame(&mut app, Default::default());
|
||
assert_eq!((effect(&app).params["reference"].clone(), effect(&app).params["copies"].clone()), (json!(8), json!(3)));
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("random".into(), json!(true));
|
||
frame(&mut app, Default::default());
|
||
assert_eq!(effect(&app).params["random"], json!(true));
|
||
// So does Scale Strokes & Effects.
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("strokes".into(), json!(true));
|
||
frame(&mut app, Default::default());
|
||
assert_eq!(effect(&app).params["strokes"], json!(true));
|
||
confirm(&mut app).unwrap();
|
||
assert!(!app.session.in_interaction());
|
||
assert_eq!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects.len(), 1);
|
||
// Editing it from the Appearance panel shows the same values.
|
||
app.run("effect.dialog", json!({"effect": "distort.transform", "index": 0})).unwrap();
|
||
let d = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!((d.f64("reference", 4.0), d.f64("copies", 0.0), d.f64("moveH", 0.0), d.bool("random")), (8.0, 3.0, 30.0, true));
|
||
assert_eq!((d.bool("objects"), d.bool("strokes")), (true, true));
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
assert!(text.lines().any(|l| l == "Reference Point"), "{text}");
|
||
}
|
||
|
||
/// Drop Shadow's dialog has Color and Darkness as radio buttons, each with its field; a click on
|
||
/// Darkness previews the shadow with it and OK applies it.
|
||
#[test]
|
||
fn drop_shadow_chooses_color_or_darkness() {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("effect.dialog", json!({"effect": "stylize.dropShadow"})).unwrap();
|
||
let ctx = egui::Context::default();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
let mut time = 0.0;
|
||
let mut run = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||
time += 0.1;
|
||
let mut out = ctx.run_ui(egui::RawInput { events, time: Some(time), ..Default::default() }, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
out
|
||
};
|
||
run(&mut app, vec![]);
|
||
let out = run(&mut app, vec![]);
|
||
let text = crate::tests_labels::shapes_text(&out);
|
||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||
for want in ["Mode:", "Opacity:", "Blur:", "Color", "Darkness"] {
|
||
assert!(lines.contains(&want), "no {want:?} in {lines:?}");
|
||
}
|
||
assert!(!lines.contains(&"Shade:"), "{lines:?}");
|
||
fn find(shape: &egui::Shape) -> Option<egui::Pos2> {
|
||
match shape {
|
||
egui::Shape::Text(t) if t.galley.text().trim() == "Darkness" => Some(t.visual_bounding_rect().center()),
|
||
egui::Shape::Vec(v) => v.iter().find_map(find),
|
||
_ => None,
|
||
}
|
||
}
|
||
let at = out.shapes.iter().find_map(|s| find(&s.shape)).unwrap();
|
||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||
for events in [vec![egui::Event::PointerMoved(at)], vec![button(true)], vec![button(false)], vec![], vec![]] {
|
||
run(&mut app, events);
|
||
}
|
||
let effect = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0].clone();
|
||
assert!(app.session.in_interaction(), "the preview runs");
|
||
assert_eq!(effect(&app).params["shade"], json!("darkness"));
|
||
confirm(&mut app).unwrap();
|
||
let params = effect(&app).params;
|
||
assert_eq!((params["shade"].clone(), params["darkness"].clone()), (json!("darkness"), json!(100.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_position_moves_art_only_with_the_tool_option_on() {
|
||
for move_art in [false, true] {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||
app.select_tool("artboard");
|
||
app.session.set_tool_option("moveArt", &json!(move_art));
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("x".into(), json!(50));
|
||
// Draw the dialog once and confirm through its registry, as the OK button does.
|
||
frame(&mut app, Default::default());
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect.x0, 50.0);
|
||
assert_eq!(
|
||
app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap().x0,
|
||
if move_art { 60.0 } else { 10.0 }
|
||
);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(*app.session.doc().unwrap().doc, *before);
|
||
}
|
||
}
|
||
|
||
/// Opened while another tool is in use, as the Artboards panel opens it (#1876), Artboard Options
|
||
/// moves the art with a new X only when the Artboard tool's Move Artwork with Artboard option is
|
||
/// on.
|
||
#[test]
|
||
fn artboard_options_position_follows_the_tool_option_with_another_tool() {
|
||
for move_art in [false, true] {
|
||
let mut app = app();
|
||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||
app.run("tool.setOption", json!({"tool": "artboard", "key": "moveArt", "value": move_art})).unwrap();
|
||
app.run("tool.options", json!({"tool": "artboard"})).unwrap();
|
||
assert_ne!(app.session.tool_id(), "artboard");
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("x".into(), json!(50));
|
||
frame(&mut app, Default::default());
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect.x0, 50.0);
|
||
assert_eq!(
|
||
app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap().x0,
|
||
if move_art { 60.0 } else { 10.0 }
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Artboard Options shows the artboard's background color on its Background Color: row (None,
|
||
/// White, Black, or Custom for another color) and not as a text field. OK keeps the color while it
|
||
/// applies another field, and a null `color` clears it.
|
||
#[test]
|
||
fn artboard_options_shows_keeps_and_clears_the_background_color() {
|
||
let mut app = app();
|
||
let color = |app: &VectorcraftApp| app.session.doc().unwrap().doc.artboards[0].color.map(|c| c.to_hex());
|
||
let lines = |app: &mut VectorcraftApp| {
|
||
let text = crate::tests_labels::painted_text(app, |app, ui| show(app, ui.ctx()));
|
||
text.lines().map(str::trim).map(str::to_string).collect::<Vec<_>>()
|
||
};
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let shown = lines(&mut app);
|
||
assert!(shown.iter().any(|l| l == "Background Color:") && shown.iter().any(|l| l == "None"), "{shown:?}");
|
||
app.ui.dialog = None;
|
||
app.run("artboard.setProps", json!({"index": 0, "color": "#336699"})).unwrap();
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let shown = lines(&mut app);
|
||
assert!(shown.iter().any(|l| l == "Custom") && !shown.iter().any(|l| l == "Color:" || l.starts_with('[')), "{shown:?}");
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Card"));
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!((app.session.doc().unwrap().doc.artboards[0].name.as_str(), color(&app).as_deref()), ("Card", Some("#336699")));
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!(null));
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!(color(&app), None);
|
||
}
|
||
|
||
/// Artboard Options for a new artboard (`ui.newArtboard`) gives the new artboard the background
|
||
/// color picked on its Background Color: row, and none when nothing was picked.
|
||
#[test]
|
||
fn new_artboard_options_give_the_new_artboard_its_background_color() {
|
||
let mut app = app();
|
||
for (picked, want) in [(Some(json!("#336699")), Some("#336699")), (None, None)] {
|
||
app.run("ui.newArtboard", json!({})).unwrap();
|
||
if let Some(c) = picked {
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), c);
|
||
}
|
||
super::confirm(&mut app).unwrap();
|
||
let ab = app.session.doc().unwrap().doc.artboards.last().cloned().unwrap();
|
||
assert_eq!(ab.color.map(|c| c.to_hex()).as_deref(), want);
|
||
}
|
||
}
|
||
|
||
/// Artboard Options shows Show Center Mark and Show Cross Hairs, ticked as the artboard has them
|
||
/// whether it opens from the Artboards panel or from a double-click with the Artboard tool. OK
|
||
/// sets them in one undo step, and a new artboard's Artboard Options gives it the ones ticked.
|
||
#[test]
|
||
fn artboard_options_show_center_mark_and_cross_hairs() {
|
||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||
let mut app = app();
|
||
let marks = |app: &VectorcraftApp, i: usize| {
|
||
let a = &app.session.doc().unwrap().doc.artboards[i];
|
||
(a.show_center_mark, a.show_cross_hairs)
|
||
};
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
assert!(text.contains("Show Center Mark") && text.contains("Show Cross Hairs"), "{text}");
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
assert!(!d.bool("showCenterMark") && !d.bool("showCrossHairs"));
|
||
d.fields.insert("showCenterMark".into(), json!(true));
|
||
let undo = app.session.doc().unwrap().history.undo.len();
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!(marks(&app, 0), (true, false));
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||
// Opened again, it shows them; unticking one clears it.
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
assert!(d.bool("showCenterMark"));
|
||
d.fields.insert("showCenterMark".into(), json!(false));
|
||
d.fields.insert("showCrossHairs".into(), json!(true));
|
||
super::confirm(&mut app).unwrap();
|
||
assert_eq!(marks(&app, 0), (false, true));
|
||
// A double-click with the Artboard tool on the artboard's empty space.
|
||
app.select_tool("artboard");
|
||
let view = app.view_info();
|
||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] {
|
||
crate::canvas::dispatch(&mut app, &PointerEvent::new(kind, 100.0, 100.0), view);
|
||
}
|
||
let d = app.ui.dialog.clone().expect("Artboard Options opened");
|
||
assert_eq!((d.kind.as_str(), d.bool("showCenterMark"), d.bool("showCrossHairs")), ("artboardOptions", false, true));
|
||
cancel(&mut app);
|
||
// A new artboard takes the ones ticked, and none when none is.
|
||
for (ticked, want) in [(Some("showCenterMark"), (true, false)), (None, (false, false))] {
|
||
app.run("ui.newArtboard", json!({})).unwrap();
|
||
if let Some(k) = ticked {
|
||
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), json!(true));
|
||
}
|
||
super::confirm(&mut app).unwrap();
|
||
let last = app.session.doc().unwrap().doc.artboards.len() - 1;
|
||
assert_eq!(marks(&app, last), want);
|
||
}
|
||
}
|
||
|
||
/// #1482: with Artboards 1 and 3 selected with the Artboard tool, a new Width in Artboard Options
|
||
/// goes to both, each keeping its top-left corner and its height, in one undo step. The new name
|
||
/// goes only to the artboard the dialog shows, and Artboard 2 keeps its size. A name alone resizes
|
||
/// nothing.
|
||
#[test]
|
||
fn artboard_options_width_resizes_every_selected_artboard() {
|
||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||
app.run("artboard.setProps", json!({"index": 2, "width": 300, "height": 150})).unwrap();
|
||
app.select_tool("artboard");
|
||
crate::panels::artboards::select(&mut app, 0);
|
||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||
let boards = |app: &VectorcraftApp| app.session.doc().unwrap().doc.artboards.clone();
|
||
let before = boards(&app);
|
||
let undo = app.session.doc().unwrap().history.undo.len();
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let fields = &mut app.ui.dialog.as_mut().unwrap().fields;
|
||
fields.insert("width".into(), json!("250 pt"));
|
||
fields.insert("name".into(), json!("Cover"));
|
||
frame(&mut app, Default::default());
|
||
super::confirm(&mut app).unwrap();
|
||
let mut want = before.clone();
|
||
want[0].name = "Cover".into();
|
||
for i in [0, 2] {
|
||
want[i].rect.x1 = want[i].rect.x0 + 250.0;
|
||
}
|
||
assert_eq!(boards(&app), want);
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(boards(&app), before);
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Cover"));
|
||
frame(&mut app, Default::default());
|
||
super::confirm(&mut app).unwrap();
|
||
want = before.clone();
|
||
want[0].name = "Cover".into();
|
||
assert_eq!(boards(&app), want, "a name alone");
|
||
}
|
||
|
||
/// #1482 with #1876: a Width typed in Artboard Options resizes each selected artboard about the
|
||
/// reference point the form uses, the center by default, as the Transform panel's W does.
|
||
#[test]
|
||
fn artboard_options_width_resizes_every_selected_artboard_about_the_reference_point() {
|
||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||
app.run("file.new", json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||
app.select_tool("artboard");
|
||
crate::panels::artboards::select(&mut app, 0);
|
||
app.session.set_tool_option("selected", &json!([0, 1]));
|
||
let before: Vec<vectorcraft_geom::Rect> = app.session.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect();
|
||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||
let mut f = Frames::new();
|
||
let shown = f.texts(&mut app);
|
||
let (width, height) = (field_after(&shown, "Width:"), field_after(&shown, "Height:"));
|
||
f.press(&mut app, width, true);
|
||
f.press(&mut app, width, false);
|
||
f.run(&mut app, vec![egui::Event::Text("500".into())]);
|
||
// A click on Height ends the Width field's editing.
|
||
f.press(&mut app, height, true);
|
||
f.press(&mut app, height, false);
|
||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||
let after: Vec<vectorcraft_geom::Rect> = app.session.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect();
|
||
let want: Vec<vectorcraft_geom::Rect> = before.iter().map(|r| vectorcraft_geom::Rect::new(r.x0 - 50.0, r.y0, r.x1 + 50.0, r.y1)).collect();
|
||
assert_eq!(after, want);
|
||
}
|
||
|
||
/// The strings a frame painted, each with its rectangle.
|
||
fn painted(out: &egui::FullOutput) -> Vec<(String, egui::Rect)> {
|
||
fn walk(s: &egui::Shape, v: &mut Vec<(String, egui::Rect)>) {
|
||
match s {
|
||
egui::Shape::Text(t) => v.push((t.galley.text().to_string(), egui::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
|
||
}
|
||
|
||
/// Where to click in the field after the label `label` among the `shown` strings.
|
||
fn field_after(shown: &[(String, egui::Rect)], label: &str) -> egui::Pos2 {
|
||
let r = shown.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("no {label:?} in {shown:?}")).1;
|
||
egui::pos2(r.right() + 40.0, r.center().y)
|
||
}
|
||
|
||
/// A 400 × 300 pt artboard at the page's origin with the ruler origin at (120, 50), and its
|
||
/// Artboard Options open.
|
||
fn artboard_options_from_a_ruler_origin() -> VectorcraftApp {
|
||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||
app.run("document.setRulerOrigin", json!({"x": 120, "y": 50})).unwrap();
|
||
assert_eq!(app.ruler_origin(), vectorcraft_geom::Point::new(120.0, 50.0));
|
||
app.run("tool.options", json!({"tool": "artboard"})).unwrap();
|
||
app
|
||
}
|
||
|
||
/// #1876: Artboard Options has X and Y beside the reference point locator, counted from the ruler
|
||
/// origin (#1212) as the Transform panel's are. The artboard moves to a typed X and Y, its reference
|
||
/// point landing there, in one undo step. The shape dialogs still show no X and Y (theirs are where
|
||
/// the click was).
|
||
#[test]
|
||
fn artboard_options_x_and_y_count_from_the_ruler_origin_at_the_reference_point() {
|
||
let mut app = artboard_options_from_a_ruler_origin();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
let undo = app.session.doc().unwrap().history.undo.len();
|
||
let mut f = Frames::new();
|
||
let has = |shown: &[(String, egui::Rect)], s: &str| shown.iter().any(|(t, _)| t == s);
|
||
// The center by default: (200, 150) on the page, 80 and 100 from the ruler origin.
|
||
let shown = f.texts(&mut app);
|
||
for s in ["X:".to_string(), "Y:".to_string(), Unit::Points.format(80.0), Unit::Points.format(100.0)] {
|
||
assert!(has(&shown, &s), "{s} in {shown:?}");
|
||
}
|
||
// The bottom-right corner: (400, 300) on the page, 280 and 250 from the ruler origin.
|
||
let corner = f.control(&mut app, "Bottom Right", egui::accesskit::Role::RadioButton);
|
||
f.press(&mut app, corner, true);
|
||
f.press(&mut app, corner, false);
|
||
let shown = f.texts(&mut app);
|
||
for v in [280.0, 250.0] {
|
||
assert!(has(&shown, &Unit::Points.format(v)), "{v} in {shown:?}");
|
||
}
|
||
// Typing X 300 and Y 0 puts that corner at (420, 50) on the page.
|
||
for (label, typed) in [("X:", "300"), ("Y:", "0")] {
|
||
let at = field_after(&shown, label);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
f.run(&mut app, vec![egui::Event::Text(typed.into())]);
|
||
}
|
||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(20.0, -250.0, 420.0, 50.0));
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
open(&mut app, "rectangle", json!({"x": 10, "y": 20, "width": 30, "height": 40}));
|
||
let shown = f.texts(&mut app);
|
||
assert!(has(&shown, "Width:") && !has(&shown, "X:") && !has(&shown, "Y:"), "{shown:?}");
|
||
}
|
||
|
||
/// #1876: a new Width or Height in Artboard Options keeps the reference point where it was, as the
|
||
/// Transform panel's does: about the center by default, so X and Y still read the same, and from
|
||
/// the top-left corner with that reference point.
|
||
#[test]
|
||
fn artboard_options_size_keeps_the_reference_point() {
|
||
for (point, want) in
|
||
[(None, vectorcraft_geom::Rect::new(-50.0, 0.0, 450.0, 300.0)), (Some("Top Left"), vectorcraft_geom::Rect::new(0.0, 0.0, 500.0, 300.0))]
|
||
{
|
||
let mut app = artboard_options_from_a_ruler_origin();
|
||
let mut f = Frames::new();
|
||
if let Some(point) = point {
|
||
let at = f.control(&mut app, point, egui::accesskit::Role::RadioButton);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
}
|
||
let shown = f.texts(&mut app);
|
||
let (width, height) = (field_after(&shown, "Width:"), field_after(&shown, "Height:"));
|
||
f.press(&mut app, width, true);
|
||
f.press(&mut app, width, false);
|
||
f.run(&mut app, vec![egui::Event::Text("500".into())]);
|
||
// A click on Height ends the Width field's editing; X reads as before.
|
||
f.press(&mut app, height, true);
|
||
f.press(&mut app, height, false);
|
||
let shown = f.texts(&mut app);
|
||
let x = Unit::Points.format(if point.is_some() { -120.0 } else { 80.0 });
|
||
assert!(shown.iter().any(|(t, _)| *t == x), "{x} in {shown:?}");
|
||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, want, "{point:?}");
|
||
}
|
||
}
|
||
|
||
/// Zig Zag's dialog has Size and Ridges per segment sliders, and Relative / Absolute and Smooth /
|
||
/// Corner as radio buttons, not dropdowns (#1133).
|
||
#[test]
|
||
fn zig_zag_has_sliders_and_radio_buttons() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": "distort.zigZag"})).unwrap();
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||
for want in ["Size:", "Relative", "Absolute", "Ridges per segment:", "Points", "Smooth", "Corner"] {
|
||
assert!(lines.contains(&want), "no {want:?} in {lines:?}");
|
||
}
|
||
assert!(!lines.contains(&"Ridges:"), "{lines:?}");
|
||
}
|
||
|
||
/// Screen readers get the number and text of the field beside each dialog slider, in the field's
|
||
/// unit, with the rail's range (extended to a number past the rail's end): Transform Each's Scale
|
||
/// and Move, Zig Zag's Size and Ridges, a form slider (Vector Halftone's Frequency), the Pencil's
|
||
/// Fidelity, the Color Picker's channel slider, the Perspective Grid's Opacity, Revolve's angle and
|
||
/// Offset, Extrude & Bevel's Rotation X and Y, Swatch Options' components and the flattener's Raster/Vector Balance.
|
||
#[test]
|
||
fn dialog_sliders_report_the_numbers_their_fields_show() {
|
||
use crate::widgets::accessibility_tests::{slider_value, tree};
|
||
use egui::accesskit::Role::Slider;
|
||
fn set(app: &mut VectorcraftApp, k: &str, v: serde_json::Value) {
|
||
app.ui.dialog.as_mut().expect("a dialog is open").fields.insert(k.into(), v);
|
||
}
|
||
let v = |n: f64, (lo, hi): (f64, f64), text: &str| (Some(n), Some(lo), Some(hi), Some(text.to_string()));
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
crate::menus::invoke(&mut app, "object.transformEach", json!({}));
|
||
set(&mut app, "scaleH", json!(250));
|
||
set(&mut app, "moveH", json!(-36));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
let horizontal: Vec<_> = shown
|
||
.iter()
|
||
.filter(|n| n.role() == Slider && n.label() == Some("Horizontal:"))
|
||
.map(|n| (n.numeric_value(), n.min_numeric_value(), n.max_numeric_value(), n.value().map(str::to_string)))
|
||
.collect();
|
||
assert_eq!(
|
||
horizontal,
|
||
[(Some(250.0), Some(0.0), Some(250.0), Some("250%".into())), (Some(-36.0), Some(-1000.0), Some(1000.0), Some("-36 pt".into()))]
|
||
);
|
||
app.ui.dialog = None;
|
||
app.run("effect.dialog", json!({"effect": "distort.zigZag"})).unwrap();
|
||
set(&mut app, "relative", json!(true));
|
||
set(&mut app, "size", json!(30));
|
||
set(&mut app, "ridges", json!(6));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Size:"), v(30.0, (0.0, 100.0), "30%"));
|
||
assert_eq!(slider_value(&shown, "Ridges per segment:"), (Some(6.0), Some(0.0), Some(100.0), None));
|
||
set(&mut app, "relative", json!(false));
|
||
set(&mut app, "size", json!(12));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Size:"), v(12.0, (0.0, 100.0), "12 pt"), "Absolute: a distance");
|
||
open(&mut app, halftone::KIND, halftone::fields());
|
||
set(&mut app, "frequency", json!(60));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Frequency"), v(60.0, (1.0, 150.0), "60 lpi"));
|
||
app.ui.dialog = None;
|
||
app.run("tool.options", json!({"tool": "pencil"})).unwrap();
|
||
set(&mut app, "fidelity", json!(2.5));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Fidelity:"), v(2.5, (0.5, 20.0), "2.5pt"));
|
||
app.ui.dialog = None;
|
||
app.run("paint.setFill", json!({"color": "#ff8000"})).unwrap();
|
||
app.run("ui.colorPicker", json!({})).unwrap();
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "H"), v(30.0, (0.0, 360.0), "30°"));
|
||
set(&mut app, "channel", json!("green"));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "G"), v(128.0, (0.0, 255.0), "128"));
|
||
app.ui.dialog = None;
|
||
app.run("ui.perspectiveGridDialog", json!({})).unwrap();
|
||
set(&mut app, "opacity", json!(40));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Opacity:"), v(40.0, (0.0, 100.0), "40%"));
|
||
app.ui.dialog = None;
|
||
app.run("path.create", json!({"d": "M150 40 L180 60 L180 140 L150 160"})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": vectorcraft_effects::REVOLVE, "item": null})).unwrap();
|
||
set(&mut app, "angle", json!(270));
|
||
set(&mut app, "offset", json!(600));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
// The 3D dialogs' fields are DragValues, which keep a trailing zero at one place.
|
||
assert_eq!(slider_value(&shown, "Revolve angle"), v(270.0, (0.0, 360.0), "270.0°"));
|
||
assert_eq!(slider_value(&shown, "Offset"), v(600.0, (0.0, 600.0), "600.0 pt"), "past the rail's 500 pt");
|
||
app.ui.dialog = None;
|
||
app.run("effect.dialog", json!({"effect": vectorcraft_effects::EXTRUDE, "item": null})).unwrap();
|
||
set(&mut app, "__tab", json!("rotation"));
|
||
set(&mut app, "rotationX", json!(30));
|
||
set(&mut app, "rotationY", json!(12.25));
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "Rotation X"), v(30.0, (-180.0, 180.0), "30.0°"), "at most 2 places");
|
||
assert_eq!(slider_value(&shown, "Rotation Y"), v(12.25, (-180.0, 180.0), "12.25°"));
|
||
app.ui.dialog = None;
|
||
app.run("swatch.new", json!({"name": "Ink", "color": {"c": 0, "m": 80, "y": 60, "k": 0}, "spot": true})).unwrap();
|
||
app.run("ui.swatchOptions", json!({"name": "Ink"})).unwrap();
|
||
let shown = tree(|ui| show(&mut app, ui.ctx()));
|
||
assert_eq!(slider_value(&shown, "M"), v(80.0, (0.0, 100.0), "80%"));
|
||
let mut o = vectorcraft_engine::cmd::FlattenOptions { balance: 30.0, ..Default::default() };
|
||
let shown = tree(|ui| {
|
||
flatten::options_editor(ui, "flatten-test", &mut o, true);
|
||
});
|
||
assert_eq!(slider_value(&shown, "Raster/Vector Balance:"), v(30.0, (0.0, 100.0), "30"));
|
||
}
|
||
|
||
/// Dragging Pattern Brush Options' Scale slider past its right end sets Scale to 10000%, and
|
||
/// dragging Art Brush Options' Width slider past its left end sets Width to 1%; OK gives each
|
||
/// brush its new value.
|
||
#[test]
|
||
fn brush_options_sliders_set_pattern_scale_and_art_width() {
|
||
use egui::accesskit::Role::Slider;
|
||
let mut app = app();
|
||
for (brush, label, key, by, want) in [("Diamonds", "Scale:", "scale", 400.0, 10000.0), ("Arrow", "Width:", "width", -400.0, 1.0)] {
|
||
app.run("ui.brushOptions", json!({ "name": brush })).unwrap();
|
||
let mut f = Frames::new();
|
||
let at = f.control(&mut app, label, Slider);
|
||
f.drag(&mut app, at, egui::vec2(by, 0.0));
|
||
assert_eq!(app.ui.dialog.as_ref().expect("Brush Options is open").f64(key, 0.0), want, "{brush}");
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(app.run("brush.get", json!({ "name": brush })).unwrap()[key], json!(want), "{brush}");
|
||
}
|
||
}
|
||
|
||
/// Frames of the dialog layer on one context (so a drag and the keyboard focus last from frame to
|
||
/// frame), with the screen-reader tree turned on.
|
||
struct Frames {
|
||
ctx: egui::Context,
|
||
time: f64,
|
||
}
|
||
|
||
impl Frames {
|
||
fn new() -> Self {
|
||
let ctx = egui::Context::default();
|
||
ctx.enable_accesskit();
|
||
theme::install_fonts(&ctx);
|
||
theme::apply(&ctx, Default::default());
|
||
Self { ctx, time: 0.0 }
|
||
}
|
||
|
||
fn run(&mut self, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> egui::FullOutput {
|
||
self.time += 0.1;
|
||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 900.0));
|
||
let input = egui::RawInput { events, time: Some(self.time), screen_rect: Some(screen), ..Default::default() };
|
||
let mut out = self.ctx.run_ui(input, |ui| show(app, ui.ctx()));
|
||
out.textures_delta.clear();
|
||
out
|
||
}
|
||
|
||
/// The strings the dialog layer paints once it has settled, each with its rectangle.
|
||
fn texts(&mut self, app: &mut VectorcraftApp) -> Vec<(String, egui::Rect)> {
|
||
self.run(app, vec![]);
|
||
painted(&self.run(app, vec![]))
|
||
}
|
||
|
||
/// The center of the button that screen readers name `label`.
|
||
fn button(&mut self, app: &mut VectorcraftApp, label: &str) -> egui::Pos2 {
|
||
self.control(app, label, egui::accesskit::Role::Button)
|
||
}
|
||
|
||
/// The center of the control of `role` that screen readers name `label`.
|
||
fn control(&mut self, app: &mut VectorcraftApp, label: &str, role: egui::accesskit::Role) -> egui::Pos2 {
|
||
self.run(app, vec![]);
|
||
let out = self.run(app, vec![]);
|
||
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
|
||
let node = nodes.iter().find(|(_, n)| n.label() == Some(label) && n.role() == role);
|
||
let b = node.unwrap_or_else(|| panic!("no {role:?} named {label:?}")).1.bounds().expect("bounds");
|
||
egui::pos2((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5)
|
||
}
|
||
|
||
fn press(&mut self, app: &mut VectorcraftApp, at: egui::Pos2, pressed: bool) {
|
||
let button = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
|
||
self.run(app, vec![egui::Event::PointerMoved(at), button]);
|
||
}
|
||
|
||
fn drag(&mut self, app: &mut VectorcraftApp, from: egui::Pos2, by: egui::Vec2) {
|
||
self.press(app, from, true);
|
||
for k in 1..=4 {
|
||
self.run(app, vec![egui::Event::PointerMoved(from + by * k as f32 / 4.0)]);
|
||
}
|
||
self.press(app, from + by, false);
|
||
}
|
||
|
||
fn key(&mut self, app: &mut VectorcraftApp, key: egui::Key, modifiers: egui::Modifiers) {
|
||
let event = egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||
self.run(app, vec![egui::Event::ModifiersChanged(modifiers), event]);
|
||
self.run(app, vec![egui::Event::ModifiersChanged(egui::Modifiers::NONE)]);
|
||
}
|
||
}
|
||
|
||
/// The corners in the open dialog, and those of the first object's first effect.
|
||
fn free_distort_corners(app: &VectorcraftApp) -> (serde_json::Value, serde_json::Value) {
|
||
let s = app.session.active().unwrap();
|
||
let node = s.doc.node(s.selection.objects[0]).unwrap();
|
||
(app.ui.dialog.as_ref().unwrap().fields["corners"].clone(), node.appearance.effects[0].params["corners"].clone())
|
||
}
|
||
|
||
/// Free Distort's dialog moves the corner whose handle is dragged and leaves the other three; the
|
||
/// preview and OK apply them, and the arrow keys move the handle that a click focused.
|
||
#[test]
|
||
fn free_distort_drags_one_corner_and_previews_it() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
app.run("effect.dialog", json!({"effect": "distort.freeDistort"})).unwrap();
|
||
let mut f = Frames::new();
|
||
// The box keeps the object's 2:1 proportions: 180 by 90 screen points.
|
||
let top_right = f.button(&mut app, "Top Right");
|
||
f.drag(&mut app, top_right, egui::vec2(27.0, 9.0));
|
||
let want = json!([[0.0, 0.0], [1.15, 0.1], [1.0, 1.0], [0.0, 1.0]]);
|
||
assert_eq!(free_distort_corners(&app), (want.clone(), want.clone()), "the dragged corner alone moves, and the preview follows");
|
||
let bottom_left = f.button(&mut app, "Bottom Left");
|
||
f.press(&mut app, bottom_left, true);
|
||
f.press(&mut app, bottom_left, false);
|
||
f.key(&mut app, egui::Key::ArrowLeft, egui::Modifiers::NONE);
|
||
f.key(&mut app, egui::Key::ArrowDown, egui::Modifiers::SHIFT);
|
||
let want = json!([[0.0, 0.0], [1.15, 0.1], [1.0, 1.0], [-0.01, 1.1]]);
|
||
assert_eq!(free_distort_corners(&app), (want.clone(), want.clone()));
|
||
confirm(&mut app).unwrap();
|
||
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
|
||
let s = app.session.active().unwrap();
|
||
assert_eq!(s.doc.node(s.selection.objects[0]).unwrap().appearance.effects[0].params["corners"], want);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "one undo step");
|
||
}
|
||
|
||
/// An arrow key moves a corner that lies outside the dialog's area by one step, and Reset puts the
|
||
/// corners of an applied Free Distort back on the box; OK keeps that.
|
||
#[test]
|
||
fn free_distort_reset_puts_the_corners_back() {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
app.run("select.all", json!({})).unwrap();
|
||
let moved = json!([[0.2, 0.1], [0.9, 0.0], [1.0, 3.0], [0.0, 1.0]]);
|
||
app.run("effect.apply", json!({"effect": "distort.freeDistort", "params": {"corners": moved}})).unwrap();
|
||
app.run("effect.dialog", json!({"effect": "distort.freeDistort", "index": 0})).unwrap();
|
||
let mut f = Frames::new();
|
||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["corners"], moved);
|
||
let bottom_right = f.button(&mut app, "Bottom Right");
|
||
f.press(&mut app, bottom_right, true);
|
||
f.press(&mut app, bottom_right, false);
|
||
f.key(&mut app, egui::Key::ArrowUp, egui::Modifiers::NONE);
|
||
let stepped = json!([[0.2, 0.1], [0.9, 0.0], [1.0, 2.99], [0.0, 1.0]]);
|
||
assert_eq!(free_distort_corners(&app), (stepped.clone(), stepped));
|
||
let reset = f.button(&mut app, "Reset");
|
||
f.press(&mut app, reset, true);
|
||
f.press(&mut app, reset, false);
|
||
f.run(&mut app, vec![]);
|
||
let rest = json!(super::free_distort::IDENTITY);
|
||
assert_eq!(free_distort_corners(&app), (rest.clone(), rest.clone()));
|
||
confirm(&mut app).unwrap();
|
||
let s = app.session.active().unwrap();
|
||
let effects = &s.doc.node(s.selection.objects[0]).unwrap().appearance.effects;
|
||
assert_eq!((effects.len(), effects[0].params["corners"].clone()), (1, rest));
|
||
}
|
||
|
||
/// The slider that screen readers name `label` in `out`'s tree: its node id, number and text.
|
||
fn slider_node(out: &egui::FullOutput, label: &str) -> (egui::accesskit::NodeId, Option<f64>, Option<String>) {
|
||
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
|
||
let (id, node) = nodes
|
||
.iter()
|
||
.find(|(_, n)| n.role() == egui::accesskit::Role::Slider && n.label() == Some(label))
|
||
.unwrap_or_else(|| panic!("no slider named {label:?}"));
|
||
(*id, node.numeric_value(), node.value().map(str::to_string))
|
||
}
|
||
|
||
/// A form slider that a click gave the keyboard focus (Vector Halftone's Frequency, 1 to 150 lpi)
|
||
/// steps the number of the field beside it with the arrow keys: one with → and ↑, back with ← and
|
||
/// ↓, ten with Shift, and ten with Page Up and Page Down. Home and End take it to the ends of its
|
||
/// range, a step stops there, and screen readers read each new number.
|
||
#[test]
|
||
fn a_focused_dialog_slider_steps_with_the_arrow_keys() {
|
||
use egui::Key::{ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Home, PageDown, PageUp};
|
||
use egui::Modifiers as M;
|
||
let mut app = app();
|
||
open(&mut app, halftone::KIND, halftone::fields());
|
||
let mut f = Frames::new();
|
||
let at = f.control(&mut app, "Frequency", egui::accesskit::Role::Slider);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
let frequency = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Vector Halftone is open").f64("frequency", 0.0);
|
||
let start = frequency(&app);
|
||
assert!((2.0..=139.0).contains(&start), "the click set {start}");
|
||
let mut seen = vec![];
|
||
for (key, mods, want) in [
|
||
(ArrowRight, M::NONE, start + 1.0),
|
||
(ArrowUp, M::NONE, start + 2.0),
|
||
(ArrowLeft, M::NONE, start + 1.0),
|
||
(ArrowDown, M::NONE, start),
|
||
(ArrowRight, M::SHIFT, start + 10.0),
|
||
(ArrowLeft, M::SHIFT, start),
|
||
(Home, M::NONE, 1.0),
|
||
(ArrowLeft, M::NONE, 1.0),
|
||
(PageUp, M::NONE, 11.0),
|
||
(End, M::NONE, 150.0),
|
||
(ArrowUp, M::SHIFT, 150.0),
|
||
(PageDown, M::NONE, 140.0),
|
||
(PageUp, M::NONE, 150.0),
|
||
] {
|
||
f.key(&mut app, key, mods);
|
||
seen.push((key, mods.shift, frequency(&app)));
|
||
assert_eq!(frequency(&app), want, "{seen:?}");
|
||
}
|
||
f.key(&mut app, ArrowLeft, M::NONE);
|
||
let out = f.run(&mut app, vec![]);
|
||
assert_eq!((frequency(&app), slider_node(&out, "Frequency").1), (149.0, Some(149.0)));
|
||
assert_eq!(slider_node(&out, "Frequency").2.as_deref(), Some("149 lpi"));
|
||
}
|
||
|
||
/// Tab leaves a focused dialog slider for its field and Shift+Tab comes back to it, whose arrow
|
||
/// keys then step the number again: the Perspective Grid's Opacity. OK gives the grid the new
|
||
/// opacity.
|
||
#[test]
|
||
fn tab_and_shift_tab_reach_a_dialog_slider_for_the_arrow_keys() {
|
||
use egui::Key::{ArrowRight, Tab};
|
||
use egui::Modifiers as M;
|
||
let mut app = app();
|
||
app.run("ui.perspectiveGridDialog", json!({})).unwrap();
|
||
let opacity = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Define Grid is open").f64("opacity", 0.0);
|
||
let mut f = Frames::new();
|
||
let at = f.control(&mut app, "Opacity:", egui::accesskit::Role::Slider);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
let start = opacity(&app);
|
||
f.key(&mut app, Tab, M::NONE);
|
||
f.key(&mut app, ArrowRight, M::NONE);
|
||
assert_eq!(opacity(&app), start, "the field has the focus, and → moves its caret");
|
||
f.key(&mut app, Tab, M::SHIFT);
|
||
f.key(&mut app, ArrowRight, M::NONE);
|
||
f.key(&mut app, ArrowRight, M::SHIFT);
|
||
assert_eq!(opacity(&app), start + 11.0);
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(app.run("perspective.grid.get", json!({})).unwrap()["define"]["opacity"].as_f64(), Some(start + 11.0));
|
||
}
|
||
|
||
/// Screen readers can step a dialog slider: the Pencil's Fidelity, a field with tenths, goes to
|
||
/// the next whole number on a decrement or an increment, as the slider's ← and → do.
|
||
#[test]
|
||
fn screen_readers_step_a_dialog_slider() {
|
||
use egui::accesskit::{Action, ActionRequest, TreeId};
|
||
let mut app = app();
|
||
app.run("tool.options", json!({"tool": "pencil"})).unwrap();
|
||
app.ui.dialog.as_mut().expect("Pencil Tool Options is open").fields.insert("fidelity".into(), json!(2.5));
|
||
let fidelity = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Pencil Tool Options is open").f64("fidelity", 0.0);
|
||
let mut f = Frames::new();
|
||
f.run(&mut app, vec![]);
|
||
let out = f.run(&mut app, vec![]);
|
||
let (node, ..) = slider_node(&out, "Fidelity:");
|
||
let request = |action| egui::Event::AccessKitActionRequest(ActionRequest { action, target_node: node, target_tree: TreeId::ROOT, data: None });
|
||
f.run(&mut app, vec![request(Action::Decrement)]);
|
||
assert_eq!(fidelity(&app), 2.0, "from 2.5");
|
||
f.run(&mut app, vec![request(Action::Increment)]);
|
||
f.run(&mut app, vec![request(Action::Increment)]);
|
||
assert_eq!(fidelity(&app), 4.0);
|
||
}
|
||
|
||
/// Transform Each's sliders step the numbers of their fields: a Scale past the rail's 200% steps
|
||
/// down from 200% and End sets 200%, → at the end leaves it as it is, and a Move slider steps one
|
||
/// millimeter in a millimeter document.
|
||
#[test]
|
||
fn transform_each_sliders_step_the_numbers_their_fields_show() {
|
||
use egui::Key::{ArrowLeft, ArrowRight, End, Tab};
|
||
use egui::Modifiers as M;
|
||
let mut app = app();
|
||
app.run("document.setUnits", json!({"units": "millimeters"})).unwrap();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||
crate::menus::invoke(&mut app, "object.transformEach", json!({}));
|
||
let set =
|
||
|app: &mut VectorcraftApp, k: &str, v: serde_json::Value| app.ui.dialog.as_mut().expect("Transform Each is open").fields.insert(k.into(), v);
|
||
let get = |app: &VectorcraftApp, k: &str| app.ui.dialog.as_ref().expect("Transform Each is open").f64(k, f64::NAN);
|
||
let mut f = Frames::new();
|
||
let at = f.control(&mut app, "Horizontal:", egui::accesskit::Role::Slider);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
set(&mut app, "scaleH", json!(300));
|
||
f.key(&mut app, ArrowRight, M::NONE);
|
||
assert_eq!(get(&app, "scaleH"), 300.0, "→ at the rail's end");
|
||
f.key(&mut app, End, M::NONE);
|
||
assert_eq!(get(&app, "scaleH"), 200.0);
|
||
set(&mut app, "scaleH", json!(300));
|
||
f.key(&mut app, ArrowLeft, M::NONE);
|
||
assert_eq!(get(&app, "scaleH"), 199.0);
|
||
// Tab goes through the Scale fields and sliders to the Move slider.
|
||
set(&mut app, "moveH", json!(0));
|
||
for _ in 0..4 {
|
||
f.key(&mut app, Tab, M::NONE);
|
||
}
|
||
f.key(&mut app, ArrowRight, M::NONE);
|
||
assert!((get(&app, "moveH") - 72.0 / 25.4).abs() < 1e-9, "1 mm: {} pt", get(&app, "moveH"));
|
||
}
|
||
|
||
/// The sliders the flattener options and the 3D dialogs draw take the arrow keys as well: Flatten
|
||
/// Transparency's Raster/Vector Balance and 3D Revolve's angle each step by ten with Shift, and
|
||
/// down to the next whole number with ↓.
|
||
#[test]
|
||
fn the_flattener_and_3d_sliders_step_with_the_arrow_keys() {
|
||
use egui::Modifiers as M;
|
||
let mut app = app();
|
||
app.run("path.create", json!({"d": "M150 40 L180 60 L180 140 L150 160"})).unwrap();
|
||
for (open, label, key) in [("flatten", "Raster/Vector Balance:", "balance"), ("revolve", "Revolve angle", "angle")] {
|
||
if open == "flatten" {
|
||
app.run("ui.flattenTransparencyDialog", json!({})).unwrap();
|
||
} else {
|
||
app.run("effect.dialog", json!({"effect": vectorcraft_effects::REVOLVE, "item": null})).unwrap();
|
||
}
|
||
let value = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("the dialog is open").f64(key, -1.0);
|
||
let mut f = Frames::new();
|
||
let at = f.control(&mut app, label, egui::accesskit::Role::Slider);
|
||
f.press(&mut app, at, true);
|
||
f.press(&mut app, at, false);
|
||
let start = value(&app);
|
||
f.key(&mut app, egui::Key::ArrowRight, M::SHIFT);
|
||
assert_eq!(value(&app), start + 10.0, "{label}");
|
||
// A plain step goes to the next whole number below.
|
||
f.key(&mut app, egui::Key::ArrowDown, M::NONE);
|
||
assert_eq!(value(&app), (start + 10.0).ceil() - 1.0, "{label} from {start}");
|
||
app.ui.dialog = None;
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn polar_grid_shared_canvas_click_defaults_units_counts_and_history() {
|
||
use vectorcraft_doc::NodeId;
|
||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||
let mut app = app();
|
||
app.select_tool("polarGrid");
|
||
app.run("tool.setOption", json!({"tool": "polarGrid", "values": {"concentric": 2, "radial": 3}})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
let undo = app.session.doc().unwrap().history.undo.len();
|
||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||
crate::canvas::dispatch(&mut app, &PointerEvent::new(kind, 30.0, 40.0), Default::default());
|
||
}
|
||
assert_eq!(DialogKind::of("polarGrid"), Some(DialogKind::Shape));
|
||
let d = app.ui.dialog.as_mut().expect("canvas click opens the shared shape form");
|
||
assert_eq!(d.fields["width"], json!("100 pt"));
|
||
assert_eq!(d.fields["height"], json!("100 pt"));
|
||
assert_eq!((d.fields["concentric"].clone(), d.fields["radial"].clone()), (json!(2), json!(3)));
|
||
d.fields.insert("width".into(), json!("25.4 mm"));
|
||
d.fields.insert("height".into(), json!("0.5 in"));
|
||
frame(&mut app, Default::default());
|
||
let result = confirm(&mut app).unwrap();
|
||
assert!(app.ui.dialog.is_none());
|
||
let s = app.session.doc().unwrap();
|
||
let group = s.doc.node(NodeId(result["id"].as_u64().unwrap())).unwrap();
|
||
let children = group.children().unwrap();
|
||
assert_eq!(children.len(), 6);
|
||
assert_eq!(children.iter().filter(|n| n.path_data().unwrap().is_closed()).count(), 3);
|
||
assert_eq!(s.history.undo.len(), undo + 1);
|
||
let made = s.doc.clone();
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, made);
|
||
}
|
||
|
||
/// Part of gap G11: an Arc tool click opens its options at the pointer; OK draws an arc there.
|
||
#[test]
|
||
fn arc_click_opens_options_and_ok_draws() {
|
||
use vectorcraft_doc::NodeKind;
|
||
let mut app = app();
|
||
open_tool_dialog(&mut app, "arc", json!({"x": 100.0, "y": 50.0}));
|
||
let d = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!(d.kind, "arc");
|
||
assert!(d.fields.contains_key("lengthX") && d.fields.contains_key("lengthY"));
|
||
super::confirm(&mut app).unwrap();
|
||
let n = app.session.doc().unwrap().doc.layers[0].children().unwrap().last().unwrap();
|
||
assert!(matches!(n.kind, NodeKind::Path { .. }));
|
||
let pd = n.path_data().unwrap();
|
||
let a0 = pd.subpaths[0].anchors[0].p;
|
||
let a1 = pd.subpaths[0].anchors[1].p;
|
||
assert!((a0.x - 100.0).abs() < 1e-6 && (a0.y - 50.0).abs() < 1e-6);
|
||
assert!((a1.x - 200.0).abs() < 1e-6 && (a1.y - 150.0).abs() < 1e-6);
|
||
}
|
||
|
||
/// #1377: Polar Grid's dialog has a skew slider under each divider count, Create Compound Path
|
||
/// From Ellipses and Fill Grid, opened with the tool's stored values; OK draws with them and
|
||
/// stores them.
|
||
#[test]
|
||
fn polar_grid_dialog_edits_skews_compound_and_fill() {
|
||
use vectorcraft_doc::{NodeId, NodeKind};
|
||
let mut app = app();
|
||
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
|
||
app.run("tool.setOption", json!({"tool": "polarGrid", "values": {"concentric": 3, "radial": 4, "radialSkew": 500}})).unwrap();
|
||
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
|
||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
let labels = [
|
||
"Concentric Dividers:",
|
||
"Skew:",
|
||
"Inside",
|
||
"Outside",
|
||
"Radial Dividers:",
|
||
"Skew:",
|
||
"Left",
|
||
"Right",
|
||
"Create Compound Path From Ellipses",
|
||
"Fill Grid",
|
||
];
|
||
let mut from = 0;
|
||
for label in labels {
|
||
from += text[from..].find(&format!("{label}\n")).unwrap_or_else(|| panic!("no {label:?} in order in {text}")) + label.len();
|
||
}
|
||
assert!(!text.contains("Use Outside Rectangle As Frame"), "{text}");
|
||
let d = app.ui.dialog.as_mut().unwrap();
|
||
assert_eq!((d.fields["concentricSkew"].clone(), d.fields["radialSkew"].clone()), (json!(0.0), json!(500.0)));
|
||
assert_eq!((d.fields["compound"].clone(), d.fields["fill"].clone()), (json!(false), json!(false)));
|
||
for (key, value) in [("concentricSkew", json!(100)), ("compound", json!(true)), ("fill", json!(true))] {
|
||
d.fields.insert(key.into(), value);
|
||
}
|
||
let result = confirm(&mut app).unwrap();
|
||
let s = app.session.doc().unwrap();
|
||
let children = s.doc.node(NodeId(result["id"].as_u64().unwrap())).unwrap().children().unwrap();
|
||
// Two rings under four radial dividers, all in the current fill.
|
||
assert_eq!(children.iter().map(|n| matches!(n.kind, NodeKind::Compound { .. })).collect::<Vec<_>>(), [true, true, false, false, false, false]);
|
||
assert!(children.iter().all(|n| n.appearance.fill().is_some_and(|f| f.paint != vectorcraft_color::Paint::None)));
|
||
let widths: Vec<f64> = children[0].children().unwrap().iter().map(|n| n.geometric_bounds().unwrap().width()).collect();
|
||
assert!((widths[0] - 100.0).abs() < 1e-6 && (widths[1] - 93.75).abs() < 1e-6, "outward: {widths:?}");
|
||
let tips: Vec<f64> = children[3..].iter().map(|n| n.path_data().unwrap().subpaths[0].anchors[1].p.x).collect();
|
||
assert!(tips.iter().all(|x| *x > 60.0), "right of the top divider: {tips:?}");
|
||
let saved = json!({"concentric": 3, "radial": 4, "concentricSkew": 100.0, "radialSkew": 500.0, "compound": true, "fill": true});
|
||
assert_eq!(app.session.tool_options_of("polarGrid"), saved);
|
||
}
|
||
|
||
#[test]
|
||
fn polar_grid_shared_cancel_reopen_zero_counts_and_failed_creation() {
|
||
let mut app = app();
|
||
app.run("tool.setOption", json!({"tool": "polarGrid", "values": {"concentric": 2, "radial": 3}})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
|
||
let d = app.ui.dialog.as_mut().expect("shared Polar Grid form opens");
|
||
d.fields.insert("concentric".into(), json!(8));
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
|
||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["concentric"], json!(2));
|
||
for key in ["concentric", "radial"] {
|
||
app.ui.dialog.as_mut().unwrap().fields.insert(key.into(), json!(0));
|
||
}
|
||
let result = confirm(&mut app).unwrap();
|
||
let group = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(result["id"].as_u64().unwrap())).unwrap();
|
||
assert_eq!(group.children().unwrap().len(), 1);
|
||
assert!(group.children().unwrap()[0].path_data().unwrap().is_closed());
|
||
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
|
||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["radial"], json!(0));
|
||
cancel(&mut app);
|
||
let layer = app.session.doc().unwrap().doc.layers[0].id;
|
||
app.run("layer.setProps", json!({"ids": [layer.0], "locked": true})).unwrap();
|
||
let options = app.session.tool_options_of("polarGrid");
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
open_tool_dialog(&mut app, "polarGrid", json!({"x": 10, "y": 20}));
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("radial".into(), json!(9));
|
||
assert!(confirm(&mut app).is_err());
|
||
assert!(app.ui.dialog.is_none());
|
||
assert_eq!(app.session.tool_options_of("polarGrid"), options);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
|
||
/// Open the real Preset menu before choosing a Fit; these actions are no longer body buttons.
|
||
fn artboard_fit_preset_at(frames: &mut Frames, app: &mut VectorcraftApp, label: &str) -> egui::Pos2 {
|
||
let texts = frames.texts(app);
|
||
assert!(texts.iter().all(|(t, _)| !matches!(t.as_str(), "Fit to Artwork Bounds" | "Fit to Selected Art")), "Fit lives only in Preset");
|
||
let row = texts.iter().find(|(t, _)| t == "Preset:").unwrap().1;
|
||
let combo = frames.ctx.viewport(|vp| {
|
||
vp.prev_pass
|
||
.widgets
|
||
.layers()
|
||
.flat_map(|(_, widgets)| widgets.iter())
|
||
.filter(|w| w.sense.senses_click() && w.rect.y_range().contains(row.center().y) && w.rect.left() > row.right())
|
||
.map(|w| w.rect)
|
||
.min_by(|a, b| a.left().total_cmp(&b.left()))
|
||
.unwrap()
|
||
.center()
|
||
});
|
||
frames.press(app, combo, true);
|
||
frames.press(app, combo, false);
|
||
frames.texts(app).into_iter().find(|(text, _)| text.trim() == label).unwrap().1.center()
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_drafts_cancel_and_apply_without_moving_art() {
|
||
for selected in [false, true] {
|
||
let mut app = app();
|
||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.select_tool("artboard");
|
||
app.session.set_tool_option("scaleArt", &json!(true));
|
||
app.session.set_tool_option("moveArt", &json!(true));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
for apply in [false, true] {
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let label = if selected { "Fit to Selected Art" } else { "Fit to Artwork Bounds" };
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, label);
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "draft only");
|
||
if !apply {
|
||
cancel(&mut app);
|
||
continue;
|
||
}
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, before.layers, "art stays in place");
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 110.0, 100.0));
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Size edits honor the live tool switch; drafts/cancel and one-step history remain intact.
|
||
#[test]
|
||
fn artboard_options_size_changes_honor_scale_artwork() {
|
||
for scale in [false, true] {
|
||
for (choice, width, height) in
|
||
[(json!(null), 400.0, 200.0), (json!({"preset":[612.0,792.0]}), 792.0, 612.0), (json!({"landscape":false}), 100.0, 200.0)]
|
||
{
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":200,"height":100})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":20,"y":10,"width":40,"height":20})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.select_tool("artboard");
|
||
app.session.set_tool_option("scaleArt", &json!(scale));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
for apply in [false, true] {
|
||
artboard_options::open(&mut app).unwrap();
|
||
let draft = app.ui.dialog.as_mut().unwrap();
|
||
draft.fields.insert("width".into(), json!(width));
|
||
draft.fields.insert("height".into(), json!(height));
|
||
if !choice.is_null() {
|
||
draft.fields.insert("__sizeChoice".into(), choice.clone());
|
||
}
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "size change remains a draft");
|
||
if !apply {
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
continue;
|
||
}
|
||
let steps = app.session.doc().unwrap().history.undo.len();
|
||
confirm(&mut app).unwrap();
|
||
let after = app.session.doc().unwrap().doc.clone();
|
||
let bounds = vectorcraft_render::encode::art_bounds(&after).unwrap();
|
||
let (sx, sy) = if scale { (width / 200.0, height / 100.0) } else { (1.0, 1.0) };
|
||
for (actual, expected) in [(bounds.x0, 20.0 * sx), (bounds.y0, 10.0 * sy), (bounds.x1, 60.0 * sx), (bounds.y1, 30.0 * sy)] {
|
||
assert!((actual - expected).abs() < 1e-6, "{actual} != {expected}, scale={scale}, choice={choice}");
|
||
}
|
||
assert_eq!((after.artboards[0].rect.width(), after.artboards[0].rect.height()), (width, height));
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_shared_preset_drafts_and_resizes_selected_boards_in_their_orientations() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":200,"height":100,"artboards":3})).unwrap();
|
||
app.run("artboard.setProps", json!({"index":2,"width":150,"height":300})).unwrap();
|
||
app.select_tool("artboard");
|
||
crate::panels::artboards::select(&mut app, 0);
|
||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
for apply in [false, true] {
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let at = |texts: &[(String, egui::Rect)], label: &str| {
|
||
texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
|
||
};
|
||
let custom = at(&frames.texts(&mut app), "Custom");
|
||
frames.press(&mut app, custom, true);
|
||
frames.press(&mut app, custom, false);
|
||
let print = at(&frames.texts(&mut app), "Print");
|
||
frames.run(&mut app, vec![egui::Event::PointerMoved(print)]);
|
||
let letter = at(&frames.texts(&mut app), "Letter");
|
||
frames.press(&mut app, letter, true);
|
||
frames.press(&mut app, letter, false);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "preset is a draft until OK");
|
||
let d = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!((d.f64("width", 0.0), d.f64("height", 0.0)), (792.0, 612.0));
|
||
if !apply {
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
continue;
|
||
}
|
||
let steps = app.session.doc().unwrap().history.undo.len();
|
||
confirm(&mut app).unwrap();
|
||
let doc = &app.session.doc().unwrap().doc;
|
||
assert_eq!((doc.artboards[0].rect.width(), doc.artboards[0].rect.height()), (792.0, 612.0));
|
||
assert_eq!((doc.artboards[2].rect.width(), doc.artboards[2].rect.height()), (612.0, 792.0));
|
||
assert_eq!(doc.artboards[1], before.artboards[1], "unselected board stays unchanged");
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_orientation_applies_to_other_selected_boards_when_active_is_already_portrait() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":100,"height":200,"artboards":3})).unwrap();
|
||
app.run("artboard.setProps", json!({"index":2,"width":300,"height":150})).unwrap();
|
||
app.select_tool("artboard");
|
||
crate::panels::artboards::select(&mut app, 0);
|
||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let texts = frames.texts(&mut app);
|
||
let row = texts.iter().find(|(t, _)| t == "Preset:").unwrap().1;
|
||
let mut buttons: Vec<egui::Rect> = frames.ctx.viewport(|vp| {
|
||
vp.prev_pass
|
||
.widgets
|
||
.layers()
|
||
.flat_map(|(_, w)| w.iter())
|
||
.filter(|w| w.sense.senses_click() && w.rect.y_range().contains(row.center().y) && w.rect.left() > row.right())
|
||
.map(|w| w.rect)
|
||
.collect()
|
||
});
|
||
buttons.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||
let portrait = buttons[buttons.len() - 2].center();
|
||
frames.press(&mut app, portrait, true);
|
||
frames.press(&mut app, portrait, false);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "orientation remains a draft");
|
||
confirm(&mut app).unwrap();
|
||
let doc = &app.session.doc().unwrap().doc;
|
||
assert_eq!(doc.artboards[0], before.artboards[0], "active board was already portrait");
|
||
assert_eq!((doc.artboards[2].rect.width(), doc.artboards[2].rect.height()), (150.0, 300.0));
|
||
assert_eq!(doc.artboards[1], before.artboards[1]);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_changes_only_active_board_even_with_an_artboard_multiselection() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":200,"height":200,"artboards":3})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.select_tool("artboard");
|
||
crate::panels::artboards::select(&mut app, 0);
|
||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let fit = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, fit, true);
|
||
frames.press(&mut app, fit, false);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "fit remains a draft");
|
||
confirm(&mut app).unwrap();
|
||
let doc = &app.session.doc().unwrap().doc;
|
||
assert_eq!(doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 110.0, 100.0));
|
||
assert_eq!(&doc.artboards[1..], &before.artboards[1..]);
|
||
assert_eq!(doc.layers, before.layers, "fitting never moves artwork");
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_artwork_ignores_draft_membership_and_invalidates_on_edits() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let dialog = app.ui.dialog.as_mut().unwrap();
|
||
dialog.fields.insert("width".into(), json!(1));
|
||
dialog.fields.insert("height".into(), json!(1));
|
||
let mut frames = Frames::new();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!(
|
||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||
(30.0, 40.0, 80.0, 60.0),
|
||
"Fit queries all artwork, not the shrunken draft"
|
||
);
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||
// A remote command can edit the document while the modal remains open.
|
||
app.run("shape.rectangle", json!({"x":600,"y":120,"width":40,"height":30})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!(
|
||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||
(30.0, 40.0, 610.0, 110.0),
|
||
"A document revision must invalidate cached fit bounds"
|
||
);
|
||
confirm(&mut app).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 640.0, 150.0));
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 640.0, 150.0));
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_artwork_includes_all_boards_and_is_independent_of_target() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":400,"height":320,"artboards":2})).unwrap();
|
||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||
for (index, width, height) in [(0, 80.0, 60.0), (1, 40.0, 30.0)] {
|
||
let rect = boards[index].rect;
|
||
app.run("shape.rectangle", json!({"x":rect.x0+30.0,"y":rect.y0+40.0,"width":width,"height":height})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
}
|
||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||
let expected =
|
||
vectorcraft_geom::Rect::new(boards[0].rect.x0 + 30.0, boards[0].rect.y0 + 40.0, boards[0].rect.x0 + 110.0, boards[0].rect.y0 + 100.0).union(
|
||
vectorcraft_geom::Rect::new(boards[1].rect.x0 + 30.0, boards[1].rect.y0 + 40.0, boards[1].rect.x0 + 70.0, boards[1].rect.y0 + 70.0),
|
||
);
|
||
app.select_tool("artboard");
|
||
let mut frames = Frames::new();
|
||
for index in [0, 1, 0] {
|
||
crate::panels::artboards::select(&mut app, index);
|
||
artboard_options::open(&mut app).unwrap();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!(
|
||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||
(expected.x0, expected.y0, expected.width(), expected.height())
|
||
);
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_artwork_new_board_includes_art_outside_its_draft() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":450,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":30,"y":170,"width":20,"height":20})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||
app.run("ui.newArtboard", json!({})).unwrap();
|
||
assert!(app.ui.dialog.as_ref().unwrap().bool("new"));
|
||
let mut frames = Frames::new();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||
assert_eq!((dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)), (30.0, 40.0, 500.0, 150.0));
|
||
confirm(&mut app).unwrap();
|
||
let after = app.session.doc().unwrap().doc.artboards.clone();
|
||
assert_eq!(after.len(), 2);
|
||
assert_eq!(after[0], boards[0]);
|
||
assert_eq!(after[1].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 530.0, 190.0));
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc.artboards, after);
|
||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||
}
|
||
|
||
/// Fit and background color share one draft and one undo step for existing and new boards.
|
||
#[test]
|
||
fn artboard_options_fit_and_background_color_coexist_through_cancel_undo_and_redo() {
|
||
for new in [false, true] {
|
||
for apply in [false, true] {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||
app.run("artboard.setProps", json!({"index":0,"color":"#336699"})).unwrap();
|
||
let x = if new { 450.0 } else { 30.0 };
|
||
app.run("shape.rectangle", json!({"x":x,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
let steps = app.session.doc().unwrap().history.undo.len();
|
||
if new {
|
||
app.run("ui.newArtboard", json!({})).unwrap();
|
||
} else {
|
||
artboard_options::open(&mut app).unwrap();
|
||
}
|
||
let labels = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||
assert!(labels.contains("Background Color:") && labels.contains("Preset:") && !labels.contains("Fit to Artwork Bounds"), "{labels}");
|
||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#c05030"));
|
||
let mut frames = Frames::new();
|
||
let fit = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, fit, true);
|
||
frames.press(&mut app, fit, false);
|
||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["color"], json!("#c05030"));
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "both changes remain a draft");
|
||
if !apply {
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps);
|
||
continue;
|
||
}
|
||
confirm(&mut app).unwrap();
|
||
let after = app.session.doc().unwrap().doc.clone();
|
||
let board = &after.artboards[usize::from(new)];
|
||
assert_eq!(board.rect, vectorcraft_geom::Rect::new(x, 40.0, x + 80.0, 100.0));
|
||
assert_eq!(board.color.map(|c| c.to_hex()).as_deref(), Some("#c05030"));
|
||
assert_eq!(after.layers, before.layers, "fitting never moves the art");
|
||
if new {
|
||
assert_eq!(after.artboards[0], before.artboards[0]);
|
||
}
|
||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Moving only the board, with no resize, must not translate its art even when Move Artwork is on.
|
||
#[test]
|
||
fn artboard_options_position_only_fit_keeps_art_with_move_art_enabled() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":80,"height":60})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":10,"y":10,"width":20,"height":20})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.run("shape.rectangle", json!({"x":70,"y":50,"width":20,"height":20})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
app.run("tool.setOption", json!({"tool":"artboard", "values":{"moveArt":true}})).unwrap();
|
||
assert!(crate::panels::artboards::move_art(&app));
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
confirm(&mut app).unwrap();
|
||
let after = app.session.doc().unwrap().doc.clone();
|
||
assert_eq!(after.artboards[0].rect, vectorcraft_geom::Rect::new(10.0, 10.0, 90.0, 70.0));
|
||
assert_eq!(after.layers, before.layers, "position-only fitting never moves art");
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_fit_preset_disabled_without_art_or_selection() {
|
||
for has_art in [false, true] {
|
||
for label in ["Fit to Artwork Bounds", "Fit to Selected Art"] {
|
||
if has_art && label == "Fit to Artwork Bounds" {
|
||
continue;
|
||
}
|
||
let mut app = app();
|
||
if has_art {
|
||
app.run("shape.rectangle", json!({"x":600,"y":40,"width":80,"height":60})).unwrap();
|
||
app.run("select.none", json!({})).unwrap();
|
||
}
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, label);
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
let draft = app.ui.dialog.as_ref().unwrap();
|
||
assert_ne!(draft.fields.get("__fit"), Some(&json!(true)), "disabled {label} must not become a fit draft");
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
cancel(&mut app);
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn artboard_options_selected_fit_spans_other_boards_and_off_board_art() {
|
||
let mut app = app();
|
||
app.run("file.new", json!({"width":200,"height":200,"artboards":3})).unwrap();
|
||
for (x, y, w, h) in [(30, 40, 80, 60), (1000, 500, 50, 40)] {
|
||
app.run("shape.rectangle", json!({"x":x,"y":y,"width":w,"height":h})).unwrap();
|
||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||
}
|
||
app.run("select.all", json!({})).unwrap();
|
||
let before = app.session.doc().unwrap().doc.clone();
|
||
artboard_options::open(&mut app).unwrap();
|
||
let mut frames = Frames::new();
|
||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Selected Art");
|
||
frames.press(&mut app, at, true);
|
||
frames.press(&mut app, at, false);
|
||
assert_eq!(app.session.doc().unwrap().doc, before, "selection fit is a draft");
|
||
confirm(&mut app).unwrap();
|
||
let after = app.session.doc().unwrap().doc.clone();
|
||
assert_eq!(after.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 1050.0, 540.0));
|
||
assert_eq!(after.artboards[1..], before.artboards[1..]);
|
||
assert_eq!(after.layers, before.layers);
|
||
app.run("edit.undo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||
app.run("edit.redo", json!({})).unwrap();
|
||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||
}
|