Dialogs: ui.dialog.set returns an error for a field the open dialog does not read

ui.dialog.set stored any field name it was given, so a misspelled field
(`discrad` for `discard`) was kept without effect and the dialog then
ran with its old value. It now answers such a field with an error that
lists the dialog's fields, and leaves the dialog unchanged.

A dialog accepts the fields it holds, `__` fields, `discard` with a
discard button, `preview` with a live preview, the fields it reads
before a control adds them (each spec's `inputs`, for example Copy and
the object options of the Transform dialogs, Move's `distance` and
`angle`, the reference point, `path` of the export options dialogs,
`searchFolder`, Brush Options' `tips`, the Justification and
Hyphenation values that open blank, the Corners `kind` and `radius` of
corners that differ, the Gradient Stop popover's `color`, `opacity`,
`location` and `spread`, and a new artboard's `color` and display
marks), and the fields it passes to the command it runs: every
parameter of the generic `command` dialog's command, and the
`textureImage` that Load Texture… sets in the Glass and Texturizer
effects. Edit Selection now opens holding `deleted<i>` for each of its
rows, so setting a row number it does not have returns an error, and
Transparency Flattener Presets opens holding the selected preset's name
and options instead of adding them on its first frame.
This commit is contained in:
dotCooCoo
2026-10-26 09:09:45 +07:00
committed by bflatastic
parent 056274f5d6
commit 0641412cce
29 changed files with 257 additions and 28 deletions
+92 -1
View File
@@ -15,7 +15,8 @@
//! - `ui.move {x, y}` / `ui.click {x, y, button?, count?, shift?…}` / `ui.drag {x, y, toX, toY, steps?}`:
//! real egui pointer input in screen points (reaches every widget: panels, flyouts, dialogs)
//! - `ui.set {brightness?, panel?, dockTab?, rulers?, outline?, …}`
//! - `ui.dialog.set {field, value}` / `ui.dialog.confirm` / `ui.dialog.cancel`
//! - `ui.dialog.set {field, value}` / `ui.dialog.confirm` / `ui.dialog.cancel` (setting a field the
//! dialog does not read is an error that lists its fields: [`crate::dialogs::accepts_field`])
//! - `ui.resize {width, height}`, `ui.focus`, `ui.screenshot {path?, data?}` (`data: true`: the PNG
//! comes back as `pngBase64` as well)
//! - `ui.render {path?, scale?, artboard?, data?}`: render an artboard headlessly (`artboard`: its
@@ -409,6 +410,10 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
"ui.dialog.set" => match app.ui.dialog.as_mut() {
Some(d) => {
let Some(f) = s("field") else { return err("missing `field`") };
if !crate::dialogs::accepts_field(d, f) {
let names = crate::dialogs::field_names(d).join(", ");
return err(format!("the `{}` dialog has no field `{f}`; its fields: {names}", d.kind));
}
d.fields.insert(f.to_string(), p.get("value").cloned().unwrap_or(Value::Null));
ok(serde_json::to_value(&*d).unwrap_or_default())
}
@@ -583,6 +588,7 @@ mod tests {
use vectorcraft_engine::Session;
use super::{ControlRequest, Outcome, handle};
use crate::state::Dialog;
use crate::{Services, VectorcraftApp};
fn input_request(app: &mut VectorcraftApp, method: &str, params: Value) -> Value {
@@ -783,4 +789,89 @@ mod tests {
assert_eq!(click(&mut app), Some(b), "a click on a selected object makes it the key");
assert_eq!(click(&mut app), Some(Value::Null), "a click on the key again lets it go");
}
/// `ui.dialog.set` of a field the open dialog does not have is an error that lists its fields,
/// and the dialog stays as it was. Fields a dialog may not hold yet are still set: `discard`
/// with a discard button, `preview` with a live preview, `__` fields and the spec's `inputs`.
#[test]
fn dialog_set_refuses_a_field_the_dialog_does_not_have() {
let mut app = VectorcraftApp::new(Session::new(), Services::default());
let fields = json!({"index": 0, "uid": 1, "name": "Untitled-1", "then": "close"});
app.ui.dialog = Some(Dialog::new(crate::unsaved::KIND, fields.clone()));
let r = input_request(&mut app, "ui.dialog.set", json!({"field": "discrad", "value": true}));
assert_eq!(r["ok"], false, "{r}");
assert_eq!(r["error"], "the `saveChanges` dialog has no field `discrad`; its fields: discard, index, name, then, uid");
assert_eq!(app.ui.dialog.as_ref().map(|d| Value::Object(d.fields.clone())), Some(fields));
for field in ["name", "discard", "__note"] {
let r = input_request(&mut app, "ui.dialog.set", json!({"field": field, "value": true}));
assert_eq!(r["ok"], true, "{field}: {r}");
}
// Move opens without Copy, which its checkbox adds once clicked; Scale's options and the
// reference point belong to the other transform dialogs.
let set = |app: &mut VectorcraftApp, kind: &str, fields: Value, cases: &[(&str, bool)]| {
app.ui.dialog = Some(Dialog::new(kind, fields));
for (field, ok) in cases {
let r = input_request(app, "ui.dialog.set", json!({"field": field, "value": true}));
assert_eq!(r["ok"], *ok, "{kind} {field}: {r}");
}
};
let moved =
[("copy", true), ("preview", true), ("distance", true), ("discard", false), ("copies", false), ("corners", false), ("origin", false)];
set(&mut app, "move", json!({"dx": "0 pt", "dy": "0 pt"}), &moved);
set(
&mut app,
"scale",
json!({"sx": 100, "sy": 100, "uniform": true}),
&[("corners", true), ("strokes", true), ("origin", true), ("distance", false)],
);
set(&mut app, "rotate", json!({"angle": 0}), &[("origin", true), ("objects", true), ("corners", false)]);
set(&mut app, "rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}), &[("radius", true), ("sides", false)]);
set(&mut app, "ellipse", json!({"x": 0, "y": 0, "width": 10, "height": 10}), &[("radius", false)]);
// Corners that differ in kind and radius open without either.
let mixed = json!({"id": 1, "corners": [0, 1, 2, 3], "relativeRadii": false});
set(&mut app, vectorcraft_tools::corners::DIALOG, mixed, &[("kind", true), ("radius", true), ("radii", false)]);
set(&mut app, "saveForOffice", json!({"ppi": 150, "transparent": false, "artboard": 0}), &[("path", true)]);
set(&mut app, "brushOptions", json!({"name": "Dots"}), &[("tips", true), ("tip", false)]);
set(&mut app, "justification", json!({"wordDesired": 100}), &[("wordMin", true), ("singleWord", true), ("wordMid", false)]);
set(&mut app, "hyphenation", json!({"hyphenate": true}), &[("afterFirst", true), ("hyphenationZone", true), ("afterfirst", false)]);
set(&mut app, "effectGallery", json!({"stack": []}), &[("browseEffect", true), ("browseParams", true), ("browse", false)]);
// A new artboard's dialog opens without a background color or display marks.
let new_board = json!({"new": true, "name": "Artboard 2", "x": 0, "y": 0, "width": 10, "height": 10});
let marks = [
("color", true),
("showCenterMark", true),
("showCrossHairs", true),
("showVideoSafeAreas", true),
("videoRulerPixelAspectRatio", true),
("showCenterMarks", false),
];
set(&mut app, "artboardOptions", new_board, &marks);
// Glass and Texturizer take the texture Load Texture… reads; other effects don't.
set(&mut app, "effect", json!({"__effect": "distort.glass"}), &[("textureImage", true)]);
set(&mut app, "effect", json!({"__effect": "blur.gaussian"}), &[("textureImage", false)]);
// The generic dialog takes every parameter of its command.
set(&mut app, "command", json!({"__command": "shape.rectangle", "x": 0, "y": 0}), &[("width", true), ("widht", false)]);
// Transparency Flattener Presets holds the selected preset's options before its first frame.
crate::dialogs::flattener_presets::open(&mut app, None);
let r = input_request(&mut app, "ui.dialog.set", json!({"field": "textToOutlines", "value": true}));
assert_eq!(r["ok"], true, "{r}");
// Edit Selection takes `deleted<i>` for each of its rows only.
app.run("file.new", json!({})).unwrap();
app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
for name in ["A", "B"] {
app.run("select.save", json!({ "name": name })).unwrap();
}
crate::dialogs::edit_selection::open(&mut app).unwrap();
for (field, ok) in [("deleted1", true), ("deleted", false), ("deletedx", false), ("deleted01", false), ("deleted2", false)] {
let r = input_request(&mut app, "ui.dialog.set", json!({"field": field, "value": true}));
assert_eq!(r["ok"], ok, "{field}: {r}");
}
let r = input_request(&mut app, "ui.dialog.set", json!({"field": "name", "value": "A"}));
assert_eq!(
r["error"],
"the `editSelection` dialog has no field `name`; its fields: count, deleted0, deleted1, name0, name1, orig0, orig1, selected"
);
app.ui.dialog = None;
assert_eq!(input_request(&mut app, "ui.dialog.set", json!({"field": "name", "value": "A"}))["error"], "no dialog open");
}
}
@@ -23,7 +23,15 @@ use crate::panels::properties::ArtboardSizeChoice;
use crate::panels::transform::{artboard_rect, reference_index, reference_locator};
use crate::state::Dialog;
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Artboard Options").into(), body, confirm, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Artboard Options").into(),
body,
confirm,
// A new artboard's dialog opens without these; its swatch, checkboxes and Video Ruler Pixel
// Aspect Ratio field add them.
inputs: |_| &["color", "showCenterMark", "showCrossHairs", "showVideoSafeAreas", PIXEL_ASPECT],
..DialogSpec::FORM
};
/// Where the fields put the artboard: `x` and `y` are its top-left corner in document points, as
/// `artboard.setProps` and `artboard.new` take them.
+1 -1
View File
@@ -30,7 +30,7 @@ use crate::{VectorcraftApp, widgets};
/// The dialog kind of Brush Options.
pub const KIND: &str = "brushOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm: ok, min_width: 440.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm: ok, min_width: 440.0, inputs: |_| &["tips"], ..DialogSpec::FORM };
/// The field of a Brush Options dialog that New Brush opened.
const CREATE: &str = "__create";
+7
View File
@@ -19,9 +19,16 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
false
},
confirm,
passes: takes,
..DialogSpec::FORM
};
/// Does the dialog's command take parameter `key` (its params description names it)?
fn takes(d: &Dialog, key: &str) -> bool {
let id = d.str("__command");
vectorcraft_engine::find_command(&id).is_some_and(|c| vectorcraft_engine::check_keys(&id, c.params, &serde_json::json!({ key: null })).is_ok())
}
/// The parameters of the commands this dialog edits that are distances.
fn lengths(command: &str) -> &'static [&'static str] {
match command {
+9 -1
View File
@@ -21,7 +21,15 @@ use crate::{VectorcraftApp, widgets};
/// The dialog kind of Corners (the corner widgets' double-click).
pub const KIND: &str = vectorcraft_tools::corners::DIALOG;
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Corners").into(), body, confirm, min_width: 280.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Corners").into(),
body,
confirm,
min_width: 280.0,
// Corners that differ open without these; the kind buttons and the radius field add them.
inputs: |_| &["kind", "radius"],
..DialogSpec::FORM
};
/// Open Corners for `{id?, corners?}`: the path `id` (default: the selected one) and its
/// `corners` (default: the selected corners), filled in with their kind and radius.
@@ -25,6 +25,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
ok: Some("Export"),
min_width: 420.0,
max_width: Some(460.0),
inputs: |_| &["path"],
..DialogSpec::FORM
};
@@ -28,6 +28,7 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
for (i, n) in names.iter().enumerate() {
fields[format!("orig{i}")] = json!(n);
fields[format!("name{i}")] = json!(n);
fields[format!("deleted{i}")] = json!(false);
}
app.ui.dialog = Some(Dialog::new(KIND, fields));
Ok(Value::Null)
+2
View File
@@ -22,6 +22,8 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
body,
confirm,
preview: true,
// Load Texture… adds the texture it reads.
passes: |d, field| field == vectorcraft_effects::pixel::TEXTURE_IMAGE && matches!(d.str("__effect").as_str(), GLASS | TEXTURIZER),
..DialogSpec::FORM
};
@@ -30,6 +30,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
preview: true,
min_width: 720.0,
max_width: Some(820.0),
inputs: |_| &["browseEffect", "browseParams"],
..DialogSpec::FORM
};
@@ -25,6 +25,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
ok: Some("Export"),
min_width: 420.0,
max_width: Some(460.0),
inputs: |_| &["path"],
..DialogSpec::FORM
};
+1
View File
@@ -39,6 +39,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
ok: Some("Relink"),
min_width: 440.0,
max_width: Some(560.0),
inputs: |_| &["searchFolder"],
..DialogSpec::FORM
};
+22 -10
View File
@@ -8,8 +8,8 @@
//! edited; on a built-in preset they go back).
use serde_json::{Value, json};
use vectorcraft_engine::cmd::FlattenOptions;
use vectorcraft_engine::cmd::flatten::{PRESET_EXTS, PRESET_FORMAT};
use vectorcraft_engine::cmd::{FlattenOptions, FlattenerPreset};
use super::flatten::{options_editor, put_options};
use super::{DialogSpec, form};
@@ -35,10 +35,28 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
..DialogSpec::FORM
};
/// Open the presets manager on `selected` (default: the first preset).
/// Open the presets manager on `selected` (default: the first preset), holding its name and
/// options.
pub fn open(app: &mut VectorcraftApp, selected: Option<&str>) {
let first = FlattenOptions::preset_label(FlattenOptions::PRESETS[0]).unwrap_or_default();
app.ui.dialog = Some(Dialog::new(KIND, json!({ "selected": selected.unwrap_or(first) })));
let mut d = Dialog::new(KIND, json!({ "selected": selected.unwrap_or(first) }));
show_selected(&app.session.flattener_presets(), &mut d);
app.ui.dialog = Some(d);
}
/// The index in `presets` of the preset `selected` names (the first when none), whose name and
/// options the fields take when they hold another preset's.
fn show_selected(presets: &[FlattenerPreset], d: &mut Dialog) -> usize {
let i = presets.iter().position(|p| p.name.eq_ignore_ascii_case(d.str("selected").trim())).unwrap_or(0);
if let Some(preset) = presets.get(i)
&& d.str(SHOWN) != preset.name
{
put_options(&mut d.fields, &preset.options);
for k in ["selected", "name", SHOWN] {
d.fields.insert(k.into(), json!(preset.name));
}
}
i
}
/// What a button asked for, done once the list and fields are drawn.
@@ -54,14 +72,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let t = Tokens::get(ui.ctx());
let presets = app.session.flattener_presets();
let builtins = FlattenOptions::PRESETS.len();
let i = presets.iter().position(|p| p.name.eq_ignore_ascii_case(d.str("selected").trim())).unwrap_or(0);
let i = show_selected(&presets, d);
let (preset, builtin) = (&presets[i], i < builtins);
if d.str(SHOWN) != preset.name {
put_options(&mut d.fields, &preset.options);
for k in ["selected", "name", SHOWN] {
d.fields.insert(k.into(), json!(preset.name));
}
}
let mut act = None;
let mut renaming = false;
ui.horizontal_top(|ui| {
+1 -1
View File
@@ -27,7 +27,7 @@ use crate::state::Dialog;
use crate::theme::Tokens;
use crate::widgets::{self, Live};
pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm);
pub(super) const SPEC: DialogSpec = DialogSpec { inputs: |_| &["color", "opacity", "location", "spread"], ..DialogSpec::window(show, confirm) };
/// The field holding the frame the popover was first drawn in.
const OPENED: &str = "__opened";
+9 -1
View File
@@ -23,7 +23,15 @@ pub const KIND: &str = "hyphenation";
/// The field holding the values the dialog opened with.
const INITIAL: &str = "__initial";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Hyphenation").into(), body, confirm, min_width: 300.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Hyphenation").into(),
body,
confirm,
min_width: 300.0,
// A value the paragraphs don't share opens blank, without its field.
inputs: |_| &["hyphenate", "wordsLongerThan", "afterFirst", "beforeLast", "hyphenLimit", "hyphenationZone", "hyphenateCapitalized"],
..DialogSpec::FORM
};
/// Open Hyphenation on the paragraph attributes the Paragraph panel shows.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
@@ -38,6 +38,22 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
preview: true,
min_width: 460.0,
ok_enabled: Some(|_, d| problem(d).is_none()),
// A value the paragraphs don't share opens blank, without its field.
inputs: |_| {
&[
"wordMin",
"wordDesired",
"wordMax",
"letterMin",
"letterDesired",
"letterMax",
"glyphMin",
"glyphDesired",
"glyphMax",
"autoLeading",
"singleWord",
]
},
..DialogSpec::FORM
};
@@ -37,6 +37,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
discard: Some("Find Fonts"),
min_width: 440.0,
max_width: Some(540.0),
inputs: |_| &["searchFolder"],
..DialogSpec::FORM
};
@@ -29,6 +29,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
discard: Some("Ignore"),
min_width: 380.0,
max_width: Some(460.0),
inputs: |_| &["searchFolder"],
..DialogSpec::FORM
};
+34
View File
@@ -174,6 +174,12 @@ pub(crate) struct DialogSpec {
pub max_width: Option<f32>,
/// The body runs a live preview interaction that Cancel rolls back.
pub preview: bool,
/// The fields that a dialog of a kind reads but may not hold yet, which `ui.dialog.set` also
/// takes (see [`accepts_field`]).
pub inputs: fn(&str) -> &'static [&'static str],
/// Whether the dialog passes a field it neither holds nor lists in `inputs` on to what it
/// runs (a parameter of the generic dialog's command), so `ui.dialog.set` takes it too.
pub passes: fn(&Dialog, &str) -> bool,
/// The OK button's label when it depends on the app (Export for Screens' "Download" on the
/// web); `ok` when none. Only with an `ok`.
pub ok_label: Option<fn(&VectorcraftApp) -> &'static str>,
@@ -201,6 +207,8 @@ impl DialogSpec {
min_width: 320.0,
max_width: None,
preview: false,
inputs: |_| &[],
passes: |_, _| false,
ok_label: None,
ok_enabled: None,
};
@@ -378,6 +386,32 @@ fn spec(kind: &str) -> &'static DialogSpec {
DialogKind::of(kind).map_or(&FALLBACK, DialogKind::spec)
}
/// Whether `ui.dialog.set` can set `field` on `d`: a field it holds, a `__` field (the dialog's
/// own state), `discard` on a dialog with a discard button, `preview` on one with a live preview,
/// one of its spec's `inputs` for its kind, or one it `passes` on.
pub fn accepts_field(d: &Dialog, field: &str) -> bool {
let spec = spec(&d.kind);
d.fields.contains_key(field)
|| field.starts_with("__")
|| (field == "discard" && spec.discard.is_some())
|| (field == "preview" && spec.preview)
|| (spec.inputs)(&d.kind).contains(&field)
|| (spec.passes)(d, field)
}
/// The fields `ui.dialog.set` can set on `d`, sorted, without the `__` ones: those it holds,
/// `discard`, `preview` and the spec's `inputs` for its kind.
pub fn field_names(d: &Dialog) -> Vec<String> {
let spec = spec(&d.kind);
let mut names: Vec<String> = d.fields.keys().filter(|k| !k.starts_with("__")).cloned().collect();
names.extend(spec.discard.map(|_| "discard".to_string()));
names.extend(spec.preview.then(|| "preview".to_string()));
names.extend((spec.inputs)(&d.kind).iter().map(|i| i.to_string()));
names.sort_unstable();
names.dedup();
names
}
/// Run a command and close the dialog (whether or not the command succeeded).
fn run_and_close(app: &mut VectorcraftApp, id: &str, params: Value) -> DialogResult {
let r = app.run(id, params);
+2 -2
View File
@@ -23,8 +23,8 @@ pub(super) const KIND: &str = "newDocument";
/// More Settings.
pub(super) const MORE: &str = "newDocumentMore";
pub(super) const SPEC: DialogSpec = DialogSpec::window(show, confirm);
pub(super) const MORE_SPEC: DialogSpec = DialogSpec::window(show_more, confirm);
pub(super) const SPEC: DialogSpec = DialogSpec { inputs: |_| &["presetName"], ..DialogSpec::window(show, confirm) };
pub(super) const MORE_SPEC: DialogSpec = DialogSpec { inputs: |_| &["presetName"], ..DialogSpec::window(show_more, confirm) };
/// The fields `file.new` takes.
const PARAMS: [&str; 12] = [
+9 -2
View File
@@ -11,8 +11,15 @@ use crate::{VectorcraftApp, io, widgets};
pub(crate) const KIND: &str = "saveForOffice";
pub(super) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Save for Office Documents").into(), body, confirm, ok: Some("Save…"), min_width: 340.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Save for Office Documents").into(),
body,
confirm,
ok: Some("Save…"),
min_width: 340.0,
inputs: |_| &["path"],
..DialogSpec::FORM
};
/// Open the dialog (150 ppi on white, the first artboard).
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
+2 -1
View File
@@ -18,7 +18,8 @@ use crate::state::Dialog;
use crate::theme::Tokens;
use crate::{VectorcraftApp, io, widgets};
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, ok: Some("Export"), min_width: 360.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec =
DialogSpec { heading, body, confirm, ok: Some("Export"), min_width: 360.0, inputs: |_| &["path"], ..DialogSpec::FORM };
/// Resolution presets (pixels per inch).
pub(super) const RESOLUTIONS: [f64; 3] = [72.0, 150.0, 300.0];
@@ -40,6 +40,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
ok_label: Some(|app| if io::is_web(app) { "Download" } else { "Save…" }),
discard: Some("Done"),
min_width: PREVIEW_W + GAP + SIDE_W,
inputs: |_| &["path", "preset"],
..DialogSpec::FORM
};
+2 -1
View File
@@ -27,7 +27,8 @@ use crate::{VectorcraftApp, io, widgets};
/// Dialog kind.
pub const KIND: &str = "saveOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 380.0, ok_enabled: Some(can_save), ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec =
DialogSpec { heading, body, confirm, min_width: 380.0, ok_enabled: Some(can_save), inputs: |_| &["all", "boards"], ..DialogSpec::FORM };
/// OK saves a legacy `.ai` file only with an artboard checked.
fn can_save(app: &VectorcraftApp, d: &Dialog) -> bool {
+2 -1
View File
@@ -27,7 +27,8 @@ use crate::{VectorcraftApp, io, widgets};
pub(super) const KIND: &str = "savePdf";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Save PDF").into(), body, confirm, ok: Some("Save PDF"), ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Save PDF").into(), body, confirm, ok: Some("Save PDF"), inputs: |_| &["path"], ..DialogSpec::FORM };
/// The dialog kind of the preset editor (New / Edit in Edit → PDF Presets).
pub(super) const PRESET_KIND: &str = "pdfPreset";
+7 -1
View File
@@ -14,7 +14,13 @@ use crate::panels::transform::artboard_rect;
use crate::state::Dialog;
use crate::widgets;
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(d).into(), body, confirm, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |d| title(d).into(),
body,
confirm,
inputs: |kind| if kind == "rectangle" { &["radius"] } else { &[] },
..DialogSpec::FORM
};
/// The field that marks a dialog opened as its tool's Tool Options rather than by a click.
const TOOL_OPTIONS: &str = "__toolOptions";
@@ -19,6 +19,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
ok: Some("OK"),
min_width: 380.0,
max_width: Some(560.0),
inputs: |_| &["path", "showCode"],
..DialogSpec::FORM
};
+11 -1
View File
@@ -13,7 +13,17 @@ 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, preview: true, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).into(), body, confirm, preview: true, inputs, ..DialogSpec::FORM };
/// The fields a transform dialog reads that it may not hold yet: Copy and the object options,
/// Move's Distance and Angle, Scale's scale options, and the reference point of the others.
fn inputs(kind: &str) -> &'static [&'static str] {
match kind {
"move" => &["copy", "objects", "patterns", "distance", "angle"],
"scale" => &["copy", "objects", "patterns", "corners", "strokes", "origin"],
_ => &["copy", "objects", "patterns", "origin"],
}
}
fn title(kind: &str) -> &'static str {
match kind {
+9 -2
View File
@@ -21,8 +21,15 @@ pub const KIND: &str = "transformEach";
const CMD: &str = "object.transformEach";
pub(super) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Transform Each").into(), body, confirm, preview: true, min_width: 360.0, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Transform Each").into(),
body,
confirm,
preview: true,
min_width: 360.0,
inputs: |_| &["objects", "patterns", "corners", "strokes"],
..DialogSpec::FORM
};
/// The fields a fresh dialog starts with.
pub fn fields() -> Value {
+2 -1
View File
@@ -102,8 +102,9 @@ the two can still win that race.
| `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above |
| `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it |
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?, imageTracePreview?}` | `imageTracePreview`: the Image Trace panel's Preview (`ui.inspect` shows it as `ui.image_trace_preview`) |
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog |
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog. `ui.dialog.set` takes the fields the dialog has (`ui.inspect`), `__` fields, `discard` on a dialog with a discard button, `preview` on one with a live preview, the fields that the dialog reads but adds only when a control changes (such as `searchFolder`, or `copy` in the Transform dialogs), and the fields it passes to the command it runs: every parameter of the `command` dialog's command (`ui.paramDialog`), and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects; any other field is an error that lists the dialog's fields, and the dialog stays as it was |
| `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. The inline PNG is capped at 64 MiB: with a `path` the file is still written and `pngBase64` is omitted when the image is larger, while a path-less `data: true` request fails if either the raw RGBA image or the encoded PNG exceeds the cap. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog |
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
| `ui.resize` / `ui.focus` | | |
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path}` / `{path?}` / `{path?, format?, artboard?, range?, scale?, …}` | `app.open` reads every format `document.open` reads (see `document.formats`) and returns its result (`{index, title, format, warnings, …}`; `warnings` say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. `app.export` encodes through the engine's `document.export` (same options; the document keeps its path) and writes `path` through the host; without `path` it returns `{dataBase64, format, bytes}`, as headless mode does. `app.quit`, `file.close` and `file.closeAll` first open a `saveChanges` dialog for each modified document (they return `{"pending": "saveChanges"}`): `ui.dialog.confirm` saves, `ui.dialog.set {field: "discard", value: true}` then confirm discards, `ui.dialog.cancel` cancels the whole close or quit |