Fix #1412: the Transform dialogs preview on the canvas, and arrow keys step their angles and percentages
This commit is contained in:
parent
a41d59604b
commit
4d17702536
|
|
@ -61,6 +61,16 @@ fn lengths(kind: &str) -> &'static [&'static str] {
|
|||
}
|
||||
}
|
||||
|
||||
/// The fields of the form dialogs ([`grid`]) that are angles or percentages, with what follows the
|
||||
/// number: fields that ↑/↓ step by one (Shift: ten) and that do math, as the distances do.
|
||||
fn suffix(kind: &str, key: &str) -> Option<&'static str> {
|
||||
match (kind, key) {
|
||||
("rotate" | "shear", "angle") => Some("°"),
|
||||
("scale", "sx" | "sy") => Some("%"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The fields of the form dialogs ([`grid`]) that come first, in this order (the others follow by
|
||||
/// name).
|
||||
fn order(kind: &str) -> &'static [&'static str] {
|
||||
|
|
@ -193,6 +203,12 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
|||
let options = choices(&d.kind, &k);
|
||||
if lengths.contains(&k.as_str()) {
|
||||
length_field(ui, d, &k, &humanized(&k), unit);
|
||||
} else if let Some(suffix) = suffix(&d.kind, &k) {
|
||||
row_label(ui, &humanized(&k));
|
||||
if let Some(v) = crate::widgets::plain_field(ui, ("dlg-num", &k), d.f64(&k, 0.0), suffix, 3, FIELD_W) {
|
||||
d.fields.insert(k, json!(v));
|
||||
}
|
||||
ui.end_row();
|
||||
} else if let Some(ends) = skew_ends(&d.kind, &k) {
|
||||
let max = vectorcraft_geom::shapes::GRID_SKEW_MAX;
|
||||
row_label(ui, tl!("Skew:"));
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ fn kinds_map_to_their_dialogs() {
|
|||
assert_eq!(DialogKind::of(kind), Some(want), "{kind}");
|
||||
}
|
||||
assert_eq!(DialogKind::of("nope"), None);
|
||||
assert!(spec("effect").preview && spec("recolor").preview && !spec("move").preview);
|
||||
assert!(spec("effect").preview && spec("recolor").preview && !spec("rectangle").preview);
|
||||
assert!(spec("preferences").window.is_some() && spec("rectangle").window.is_none());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Transform dialogs: Move, Rotate, Scale, Reflect and Shear (with Transform Patterns and Copy;
|
||||
//! Scale also has Uniform, Scale Corners and Scale Strokes & Effects).
|
||||
//! Transform dialogs: Move, Rotate, Scale, Reflect and Shear (with Transform Patterns, Copy and
|
||||
//! Preview; Scale also has Uniform, Scale Corners and Scale Strokes & Effects). With Preview on
|
||||
//! (`preview`, off when the dialog opens), the transform shows on the canvas as the values change.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -7,7 +8,7 @@ use super::{DialogSpec, form, run_and_close};
|
|||
use crate::VectorcraftApp;
|
||||
use crate::state::Dialog;
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).into(), body, confirm, ..DialogSpec::FORM };
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).into(), body, confirm, preview: true, ..DialogSpec::FORM };
|
||||
|
||||
fn title(kind: &str) -> &'static str {
|
||||
match kind {
|
||||
|
|
@ -29,6 +30,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
d.fields.insert("patterns".into(), Value::Bool(transform_patterns(app, d)));
|
||||
form::check(ui, d, "patterns", tl!("Transform Patterns"));
|
||||
form::check(ui, d, "copy", tl!("Copy (make a transformed copy)"));
|
||||
let (cmd, mut params) = command(app, d);
|
||||
if d.kind == "scale" {
|
||||
add_scale_options(app, d, &mut params);
|
||||
}
|
||||
form::preview(app, ui, d, title(&d.kind), cmd, params);
|
||||
false
|
||||
}
|
||||
|
||||
|
|
@ -80,24 +86,37 @@ fn origin_params(d: &Dialog, mut p: Value) -> Value {
|
|||
p
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
/// The dialog's command and its params (Scale's without its scale options).
|
||||
fn command(app: &VectorcraftApp, d: &Dialog) -> (&'static str, Value) {
|
||||
let (copy, patterns) = (d.bool("copy"), transform_patterns(app, d));
|
||||
let (id, params) = match d.kind.as_str() {
|
||||
"move" => {
|
||||
return run_and_close(app, "object.move", json!({"dx": d.f64("dx", 0.0), "dy": d.f64("dy", 0.0), "copy": copy, "patterns": patterns}));
|
||||
}
|
||||
// Move has no reference point.
|
||||
"move" => return ("object.move", json!({"dx": d.f64("dx", 0.0), "dy": d.f64("dy", 0.0), "copy": copy, "patterns": patterns})),
|
||||
"rotate" => ("object.rotate", json!({"angle": d.f64("angle", 0.0), "copy": copy, "patterns": patterns})),
|
||||
"scale" => {
|
||||
let sx = d.f64("sx", 100.0);
|
||||
let sy = if d.bool("uniform") { sx } else { d.f64("sy", 100.0) };
|
||||
("object.scale", with_scale_options(app, d, json!({"sx": sx, "sy": sy, "copy": copy, "patterns": patterns}))?)
|
||||
("object.scale", json!({"sx": sx, "sy": sy, "copy": copy, "patterns": patterns}))
|
||||
}
|
||||
"reflect" => {
|
||||
("object.reflect", json!({"axis": d.fields.get("axis").cloned().unwrap_or(json!("vertical")), "copy": copy, "patterns": patterns}))
|
||||
}
|
||||
_ => ("object.shear", json!({"angle": d.f64("angle", 0.0), "axis": d.str("axis"), "copy": copy, "patterns": patterns})),
|
||||
};
|
||||
run_and_close(app, id, origin_params(d, params))
|
||||
(id, origin_params(d, params))
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
// The preview is rolled back and the command run as without one, so its undo step, its journal
|
||||
// entry and Transform Again are the same either way.
|
||||
if app.session.in_interaction() {
|
||||
app.session.cancel_interaction().map_err(|e| e.to_string())?;
|
||||
}
|
||||
let (id, mut params) = command(app, d);
|
||||
if d.kind == "scale" {
|
||||
params = with_scale_options(app, d, params)?;
|
||||
}
|
||||
run_and_close(app, id, params)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -133,6 +152,68 @@ mod tests {
|
|||
assert_eq!((cmd.as_str(), &p["strokes"], &p["corners"]), ("object.scale", &json!(false), &json!(true)));
|
||||
}
|
||||
|
||||
/// #1412: Preview shows the transform on the canvas as the values change, ↑/↓ in the angle
|
||||
/// field step it by 1° (Shift: 10°), Cancel rolls the preview back and OK keeps one undo step
|
||||
/// that Transform Again repeats.
|
||||
#[test]
|
||||
fn preview_follows_the_stepped_angle() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 60, "height": 20})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let frame = |app: &mut VectorcraftApp, key: Option<(egui::Key, egui::Modifiers)>| {
|
||||
let events = key.map(|(key, modifiers)| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers });
|
||||
let mut out =
|
||||
ctx.run_ui(egui::RawInput { events: events.into_iter().collect(), ..Default::default() }, |ui| crate::dialogs::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
let size = |app: &VectorcraftApp| {
|
||||
let b = app.session.doc().unwrap().doc.layers[0].children().unwrap()[0].geometric_bounds().unwrap();
|
||||
(b.width().round(), b.height().round())
|
||||
};
|
||||
let undo_steps = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len();
|
||||
let steps = undo_steps(&app);
|
||||
crate::menus::invoke(&mut app, "object.rotate", json!({}));
|
||||
frame(&mut app, None);
|
||||
assert!(!app.session.in_interaction(), "Preview is off when the dialog opens");
|
||||
// ↑ nine times with Shift: 90°, in the dialog only.
|
||||
for _ in 0..9 {
|
||||
frame(&mut app, Some((egui::Key::ArrowUp, egui::Modifiers::SHIFT)));
|
||||
}
|
||||
frame(&mut app, Some((egui::Key::ArrowUp, egui::Modifiers::NONE)));
|
||||
frame(&mut app, Some((egui::Key::ArrowDown, egui::Modifiers::NONE)));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["angle"], json!(90.0));
|
||||
assert_eq!(size(&app), (60.0, 20.0));
|
||||
// Preview on: the canvas shows it, and follows the next steps.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true));
|
||||
frame(&mut app, None);
|
||||
assert!(app.session.in_interaction());
|
||||
assert_eq!(size(&app), (20.0, 60.0));
|
||||
for _ in 0..9 {
|
||||
frame(&mut app, Some((egui::Key::ArrowUp, egui::Modifiers::SHIFT)));
|
||||
}
|
||||
assert_eq!(size(&app), (60.0, 20.0), "180°");
|
||||
// Cancel rolls it back.
|
||||
for _ in 0..9 {
|
||||
frame(&mut app, Some((egui::Key::ArrowDown, egui::Modifiers::SHIFT)));
|
||||
}
|
||||
crate::dialogs::cancel(&mut app);
|
||||
assert!(!app.session.in_interaction());
|
||||
assert_eq!((size(&app), undo_steps(&app)), ((60.0, 20.0), steps));
|
||||
// OK keeps the previewed transform as one undo step, and Transform Again repeats it.
|
||||
crate::menus::invoke(&mut app, "object.rotate", json!({}));
|
||||
app.ui.dialog.as_mut().unwrap().fields.extend([("preview".to_string(), json!(true)), ("angle".to_string(), json!(90))]);
|
||||
frame(&mut app, None);
|
||||
assert_eq!(size(&app), (20.0, 60.0));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(!app.session.in_interaction() && app.ui.dialog.is_none());
|
||||
assert_eq!((size(&app), undo_steps(&app)), ((20.0, 60.0), steps + 1));
|
||||
assert_eq!(app.session.journal.last().unwrap().0, "object.rotate");
|
||||
app.run("object.transformAgain", json!({})).unwrap();
|
||||
assert_eq!(size(&app), (60.0, 20.0));
|
||||
}
|
||||
|
||||
/// Transform Patterns starts from General › Transform Pattern Tiles (#394) and goes with the
|
||||
/// command.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue