Merge pr/643: UI: the Effects dialog, as in InDesign (#643)

# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/object.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/i18n/ar.rs
#	crates/ui-egui/src/i18n/it.rs
#	crates/ui-egui/src/i18n/ja.rs
#	crates/ui-egui/src/i18n/pt_br.rs
#	crates/ui-egui/src/i18n/uk.rs
#	crates/ui-egui/src/menus.rs
#	crates/ui-egui/src/panels/properties.rs
This commit is contained in:
aiongg
2026-10-13 00:12:33 +00:00
15 changed files with 1052 additions and 2 deletions
+1
View File
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-12** · The Effects dialog, as in InDesign: Object › Effects (Transparency…, Drop Shadow… and the rest, and their shortcuts) and the Effects panel's fx button open it on the chosen effect, turned on. Transparency and every effect in one window, in the document's units, with X/Y offsets, Preview on the page, Cancel putting things back, and OK as one undo step (`object.effects`). Shadows default to InDesign's 120° light angle. No readiness percentage change.
- **2026-10-12** · Editing type on a path: the Type tool clicks, drags and double-clicks along the curve (the character under the pointer, not one on the straightened line), a click on the letters themselves hits the frame, and the caret and highlight bend with the path. No readiness percentage change.
- **2026-10-12** · Type on a path has start and end brackets, as in InDesign: selected path type shows them across the path, and dragging either with the Selection or Direct Selection tool moves where the text starts or ends (text past the end bracket is overset). `type.pathOptions {end}`; IDML reads and writes EndBracket. No readiness percentage change.
- **2026-10-12** · The Pen tool continues an open path from either end point, as in InDesign: the cursor shows it over an end point, a click there carries on drawing that path, a drag first reshapes the end point's direction lines, and the other end closes it. Object › Paths › Reverse Path (`path.reverse`); `path.setAnchor`; `path.appendAnchor` and `path.close` take a subpath. No readiness percentage change.
+2 -2
View File
@@ -920,7 +920,7 @@ impl Default for DropShadow {
on: false,
color: designcraft_color::swatch::BLACK.into(),
opacity: 0.75,
angle: 135.0,
angle: 120.0,
distance: 7.0,
size: 5.0,
spread: 0.0,
@@ -952,7 +952,7 @@ impl Default for InnerShadow {
on: false,
color: designcraft_color::swatch::BLACK.into(),
opacity: 0.75,
angle: 135.0,
angle: 120.0,
distance: 7.0,
size: 7.0,
choke: 0.0,
+181
View File
@@ -531,6 +531,15 @@ pub fn specs() -> Vec<CommandSpec> {
)
}
),
cmd!(
"object.effects",
"Effects",
[],
None,
"{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, feather? (width), directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step",
has_selection,
set_effects
),
cmd!("object.feather", "Basic Feather", ["Object", "Effects"], None, "{width (0 = off), ids?}", has_selection, |s, p| {
let w = f64_or(p, "width", 9.0).max(0.0);
set_flag(s, p, move |i| i.effects.feather = w, false)
@@ -1905,6 +1914,136 @@ pub(super) fn match_attributes(s: &mut Session, p: &Value) -> Result<Value> {
)
}
/// `patch` merged into `base`: objects field by field (recursively), anything else replaced.
fn merge_json(base: &mut Value, patch: &Value) {
match (base, patch) {
(Value::Object(b), Value::Object(p)) => {
for (k, v) in p {
merge_json(b.entry(k.clone()).or_insert(Value::Null), v);
}
}
(b, p) => *b = p.clone(),
}
}
/// Keep effect values in their ranges: opacities 0–1, sizes and distances ≥ 0, percentages 0–100.
fn sanitize_effects(e: &mut designcraft_doc::Effects) -> Result<()> {
const ID: &str = "object.effects";
let len = |v: &mut f64| -> Result<()> {
if !v.is_finite() {
return Err(bad(ID, "values must be finite"));
}
*v = v.clamp(0.0, 10_000.0);
Ok(())
};
let pct = |v: &mut f64| -> Result<()> {
if !v.is_finite() {
return Err(bad(ID, "values must be finite"));
}
*v = v.clamp(0.0, 100.0);
Ok(())
};
let ang = |v: &mut f64| -> Result<()> {
if !v.is_finite() {
return Err(bad(ID, "values must be finite"));
}
*v = (*v + 180.0).rem_euclid(360.0) - 180.0;
Ok(())
};
let op = |v: &mut f32| -> Result<()> {
if !v.is_finite() {
return Err(bad(ID, "values must be finite"));
}
*v = v.clamp(0.0, 1.0);
Ok(())
};
let ds = &mut e.drop_shadow;
op(&mut ds.opacity)?;
ang(&mut ds.angle)?;
len(&mut ds.distance)?;
len(&mut ds.size)?;
pct(&mut ds.spread)?;
let is = &mut e.inner_shadow;
op(&mut is.opacity)?;
ang(&mut is.angle)?;
len(&mut is.distance)?;
len(&mut is.size)?;
pct(&mut is.choke)?;
let og = &mut e.outer_glow;
op(&mut og.opacity)?;
len(&mut og.size)?;
pct(&mut og.spread)?;
let ig = &mut e.inner_glow;
op(&mut ig.opacity)?;
len(&mut ig.size)?;
pct(&mut ig.choke)?;
let bv = &mut e.bevel;
len(&mut bv.size)?;
len(&mut bv.depth)?;
ang(&mut bv.angle)?;
op(&mut bv.highlight_opacity)?;
op(&mut bv.shadow_opacity)?;
let sa = &mut e.satin;
op(&mut sa.opacity)?;
ang(&mut sa.angle)?;
len(&mut sa.distance)?;
len(&mut sa.size)?;
len(&mut e.feather)?;
for w in &mut e.directional_feather.widths {
len(w)?;
}
let gf = &mut e.gradient_feather;
ang(&mut gf.angle)?;
op(&mut gf.start)?;
op(&mut gf.end)?;
Ok(())
}
fn set_effects(s: &mut Session, p: &Value) -> Result<Value> {
const ID: &str = "object.effects";
let patch = match p.get("effects") {
None | Some(Value::Null) => None,
Some(v @ Value::Object(_)) => Some(v.clone()),
Some(_) => return Err(bad(ID, "effects must be an object")),
};
let opacity = match p.get("opacity") {
None | Some(Value::Null) => None,
Some(v) => Some(v.as_f64().filter(|o| o.is_finite()).ok_or_else(|| bad(ID, "opacity must be a number"))?.clamp(0.0, 1.0) as f32),
};
let blend = match p.get("blend") {
None | Some(Value::Null) => None,
Some(v) => {
let raw = v.as_str().ok_or_else(|| bad(ID, "blend must be a string"))?;
Some(BlendMode::parse(raw).ok_or_else(|| bad(ID, format!("unknown blend mode `{raw}`")))?)
}
};
let ids = targets(s, p)?;
s.edit(|d, _| {
let mut changed = 0;
for id in &ids {
let Some(it) = d.item_mut(*id) else { continue };
if it.locked {
continue;
}
if let Some(patch) = &patch {
let mut cur = serde_json::to_value(&it.effects).map_err(|e| bad(ID, e.to_string()))?;
merge_json(&mut cur, patch);
let mut fx: designcraft_doc::Effects = serde_json::from_value(cur).map_err(|e| bad(ID, format!("effects: {e}")))?;
sanitize_effects(&mut fx)?;
it.effects = fx;
}
if let Some(o) = opacity {
it.opacity = o;
}
if let Some(b) = blend {
it.blend = b;
}
changed += 1;
}
Ok(json!({"changed": changed}))
})
}
fn set_flag(s: &mut Session, p: &Value, f: impl Fn(&mut Item), deselect: bool) -> Result<Value> {
let ids = targets(s, p)?;
s.edit(|d, sel| {
@@ -4411,3 +4550,45 @@ mod path_edit_tests {
assert!(s.execute("path.close", &json!({"id": id, "subpath": 4})).is_err());
}
}
#[cfg(test)]
mod effects_command_tests {
use designcraft_doc::ItemId;
use serde_json::json;
#[test]
fn effects_set_together_in_one_undo_step_and_keep_what_is_not_given() {
let mut s = crate::Session::new();
s.execute("file.new", &json!({})).unwrap();
let id = s.execute("frame.create", &json!({"rect": [100, 100, 300, 300]})).unwrap()["id"].as_u64().unwrap();
s.execute("object.dropShadow", &json!({"on": true, "distance": 10, "ids": [id]})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
let r = s
.execute(
"object.effects",
&json!({"ids": [id], "opacity": 0.5, "blend": "multiply",
"effects": {"innerShadow": {"on": true, "angle": 120, "distance": 7, "size": 7}, "dropShadow": {"size": 9}, "feather": 4}}),
)
.unwrap();
assert_eq!(r["changed"], 1);
let st = s.doc().unwrap();
assert_eq!(st.history.undo.len(), undo + 1, "one undo step");
let it = st.doc.item(ItemId(id)).unwrap();
let e = &it.effects;
assert!(e.inner_shadow.on && e.inner_shadow.angle == 120.0 && e.inner_shadow.size == 7.0);
assert!(e.drop_shadow.on && e.drop_shadow.distance == 10.0 && e.drop_shadow.size == 9.0, "untouched fields stay");
assert_eq!((e.feather, it.opacity, it.blend), (4.0, 0.5, designcraft_color::BlendMode::Multiply));
// Out of range values are kept in range; garbage changes nothing.
s.execute("object.effects", &json!({"ids": [id], "effects": {"outerGlow": {"on": true, "opacity": 3, "spread": 400, "size": -5}}})).unwrap();
let og = s.doc().unwrap().doc.item(ItemId(id)).unwrap().effects.outer_glow.clone();
assert_eq!((og.opacity, og.spread, og.size), (1.0, 100.0, 0.0));
let before = s.doc().unwrap().doc.clone();
for bad in [json!({"effects": {"dropShadow": {"distance": "far"}}}), json!({"effects": 5}), json!({"blend": "loud"}), json!({"opacity": "x"})]
{
let mut p = bad.clone();
p["ids"] = json!([id]);
assert!(s.execute("object.effects", &p).is_err(), "{bad}");
}
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
}
}
+2
View File
@@ -123,6 +123,7 @@ fn drop_shadow_of_text_follows_the_glyphs() {
d.story_mut(sid).unwrap().format_chars(0..6, |f| f.over.size = Some(40.0));
let ds = &mut d.item_mut(fid).unwrap().effects.drop_shadow;
ds.on = true;
ds.angle = 135.0;
ds.distance = 4.0;
ds.size = 3.0;
let img = render(&d);
@@ -155,6 +156,7 @@ fn inner_shadow_darkens_inside_the_lit_edge() {
let id = rect_item(&mut d, Rect::new(30.0, 30.0, 130.0, 130.0), "C=0 M=0 Y=100 K=0");
let s = &mut d.item_mut(id).unwrap().effects.inner_shadow;
s.on = true;
s.angle = 135.0;
s.distance = 8.0;
s.size = 8.0;
s.opacity = 1.0;
+4
View File
@@ -1523,6 +1523,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"swatchOptions" => crate::i18n::tr(&app.ui.language, "Swatch Options"),
"colorGroupOptions" => crate::i18n::tr(&app.ui.language, "Color Group Options"),
"layerSettings" => crate::i18n::tr(&app.ui.language, "Layer Options"),
crate::effects_dialog::ID => crate::i18n::tr(&app.ui.language, "Effects"),
id => match id.strip_prefix("cmd:").and_then(designcraft_engine::find_command) {
Some(c) => crate::i18n::tr(&app.ui.language, c.label.trim_end_matches('…')),
None => crate::i18n::tr(&app.ui.language, "Dialog"),
@@ -1537,6 +1538,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"closeDocument" | "updateLink" => 380.0,
"newDocument" => 960.0,
"numberingSection" => 560.0,
crate::effects_dialog::ID => 720.0,
_ => 640.0,
};
let width = preferred_width.min(available.x);
@@ -1741,6 +1743,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"swatchOptions" => options::swatch_options(app, ui, &mut d),
"colorGroupOptions" => options::color_group_options(app, ui, &mut d),
"layerSettings" => options::layer_options(app, ui, &mut d),
crate::effects_dialog::ID => crate::effects_dialog::show(app, ui, &mut d),
"preferences" => preferences(app, ui, &mut d, body_height),
"print" => print_dialog(app, ui, &mut d),
"pdfImport" => {
@@ -2732,6 +2735,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
app.run("footnote.options", p)
}
"alert" => Ok(Value::Null),
crate::effects_dialog::ID => crate::effects_dialog::confirm(app, &d),
"findChange" if d.b("objectMode") => Ok(Value::Null),
"findChange" => app.run("find.change", json!({"find": d.s("find"), "change": d.s("change"), "grep": d.b("grep"), "caseSensitive": d.b("caseSensitive"), "wholeWord": d.b("wholeWord"), "scope": d.s("scope")})),
STEP_AND_REPEAT => {
+675
View File
@@ -0,0 +1,675 @@
//! Object › Effects (and the Effects panel's fx button): InDesign's Effects dialog. Transparency
//! and every effect in one window: the list on the left (a checkbox turns an effect on, a click on
//! its name shows its settings and turns it on), the settings on the right in the document's
//! units, Preview on the page while it is open, and OK as one undo step (`object.effects`).
use designcraft_geom::{Unit, format_measure, units::parse_measure};
use serde_json::{Value, json};
use crate::DesignApp;
use crate::dialogs::Dialog;
use crate::theme::semibold;
/// The dialog's id.
pub const ID: &str = "effects";
/// The params a previewing dialog shows on the page (its interaction is open while it is set).
const PREVIEWED: &str = "_previewed";
/// The dialog's pages: (key in `Effects` JSON, label). Transparency is the object's own.
pub const PAGES: [(&str, &str); 10] = [
("transparency", "Transparency"),
("dropShadow", "Drop Shadow"),
("innerShadow", "Inner Shadow"),
("outerGlow", "Outer Glow"),
("innerGlow", "Inner Glow"),
("bevel", "Bevel and Emboss"),
("satin", "Satin"),
("feather", "Basic Feather"),
("directionalFeather", "Directional Feather"),
("gradientFeather", "Gradient Feather"),
];
/// The page an effect command opens (Object › Effects › Drop Shadow…, its shortcut).
pub fn page_of_command(id: &str) -> Option<&'static str> {
Some(match id {
"object.dropShadow" => "dropShadow",
"object.innerShadow" => "innerShadow",
"object.outerGlow" => "outerGlow",
"object.innerGlow" => "innerGlow",
"object.bevel" => "bevel",
"object.satin" => "satin",
"object.feather" => "feather",
"object.directionalFeather" => "directionalFeather",
"object.gradientFeather" => "gradientFeather",
_ => return None,
})
}
/// Open the dialog on `page` for the selected objects, the page's effect turned on (as InDesign
/// does when an effect is chosen from a menu). Nothing selected: a status message.
pub fn open(app: &mut DesignApp, page: &str) {
let Some(st) = app.session.active() else { return };
let ids: Vec<u64> = st.selection.items.iter().map(|i| i.0).collect();
let Some(it) = st.selection.items.first().and_then(|i| st.doc.item(*i)) else {
app.status(crate::i18n::tr(&app.ui.language, "Select an object.").to_string());
return;
};
let mut fx = effects_json(&it.effects);
let fields = json!({"ids": ids, "fx": Value::Null, "opacity": it.opacity, "blend": it.blend.label(), "preview": true, "page": "transparency"});
let mut d = Dialog::new(ID, fields);
let page = PAGES.iter().find(|p| p.0 == page).map_or("transparency", |p| p.0);
turn_on(&mut fx, page);
d.fields.insert("fx".into(), fx);
d.fields.insert("page".into(), json!(page));
app.ui.dialog = Some(d);
}
/// Every effect's settings, those still at their defaults included (the document leaves them out).
fn effects_json(e: &designcraft_doc::Effects) -> Value {
use designcraft_doc::{Bevel, DirectionalFeather, DropShadow, GradientFeather, InnerGlow, InnerShadow, OuterGlow, Satin};
let mut fx = serde_json::to_value(e).unwrap_or_else(|_| json!({}));
let defaults = [
("dropShadow", serde_json::to_value(DropShadow::default())),
("innerShadow", serde_json::to_value(InnerShadow::default())),
("outerGlow", serde_json::to_value(OuterGlow::default())),
("innerGlow", serde_json::to_value(InnerGlow::default())),
("bevel", serde_json::to_value(Bevel::default())),
("satin", serde_json::to_value(Satin::default())),
("directionalFeather", serde_json::to_value(DirectionalFeather::default())),
("gradientFeather", serde_json::to_value(GradientFeather::default())),
];
for (k, v) in defaults {
if !fx[k].is_object()
&& let Ok(v) = v
{
fx[k] = v;
}
}
fx
}
/// Turn the effect of `page` on, as choosing it does (Basic Feather: its default width).
fn turn_on(fx: &mut Value, page: &str) {
match page {
"transparency" => {}
"feather" => {
if fx["feather"].as_f64().unwrap_or(0.0) <= 0.0 {
fx["feather"] = json!(9.0);
}
}
_ => {
if fx[page].is_object() {
fx[page]["on"] = json!(true);
}
}
}
}
fn is_on(fx: &Value, page: &str) -> bool {
match page {
"transparency" => true,
"feather" => fx["feather"].as_f64().unwrap_or(0.0) > 0.0,
_ => fx[page]["on"].as_bool().unwrap_or(false),
}
}
/// The `object.effects` params the dialog stands for.
pub fn params(d: &Dialog) -> Value {
json!({"ids": d.fields.get("ids").cloned().unwrap_or(json!([])), "effects": d.fields.get("fx").cloned().unwrap_or(json!({})),
"opacity": d.fields.get("opacity").and_then(Value::as_f64).unwrap_or(1.0), "blend": d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal")})
}
/// Apply: the preview goes and the settings are made once, as one undo step.
pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
if d.fields.contains_key(PREVIEWED) {
app.session.cancel_interaction();
}
app.run("object.effects", params(d))
}
/// Preview on: the effects show on the page (redone when a value changes); off: they go.
fn preview(app: &mut DesignApp, d: &mut Dialog) {
let shown = d.fields.get(PREVIEWED).cloned();
if !d.fields.get("preview").and_then(Value::as_bool).unwrap_or(false) {
if shown.is_some() {
app.session.cancel_interaction();
d.fields.remove(PREVIEWED);
}
return;
}
let p = params(d);
if shown.as_ref() == Some(&p) {
return;
}
if shown.is_none() && app.session.begin_interaction("Effects").is_err() {
return;
}
match app.session.preview("object.effects", p.clone()) {
Ok(()) => d.fields.remove("status"),
Err(e) => d.fields.insert("status".into(), json!(e.to_string())),
};
d.fields.insert(PREVIEWED.into(), p);
}
/// A number of `fx` at `path` (`["dropShadow", "distance"]`).
fn num(d: &Dialog, path: &[&str]) -> f64 {
let mut v = d.fields.get("fx").unwrap_or(&Value::Null);
for k in path {
v = &v[*k];
}
v.as_f64().unwrap_or(0.0)
}
fn set(d: &mut Dialog, path: &[&str], value: Value) {
let Some(mut v) = d.fields.get_mut("fx") else { return };
for k in &path[..path.len() - 1] {
if !v[*k].is_object() {
v[*k] = json!({});
}
v = &mut v[*k];
}
if let Some(last) = path.last() {
v[*last] = value;
}
}
/// A distance in the document's unit (stored in points).
fn measure(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], unit: Unit) {
let mut v = num(d, path);
let w = egui::DragValue::new(&mut v)
.range(0.0..=10_000.0)
.speed(0.25)
.custom_formatter(move |n, _| format_measure(n, unit))
.custom_parser(move |s| parse_measure(s, unit).ok());
if ui.add_sized([90.0, 20.0], w).changed() {
set(d, path, json!(v));
}
}
/// An opacity (0–1, shown as %).
fn percent_of_one(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) {
let mut v = num(d, path) * 100.0;
if ui.add_sized([70.0, 20.0], egui::DragValue::new(&mut v).range(0.0..=100.0).suffix("%").max_decimals(0)).changed() {
set(d, path, json!(v / 100.0));
}
}
/// A percentage (0–100).
fn percent(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) {
let mut v = num(d, path);
if ui.add_sized([70.0, 20.0], egui::DragValue::new(&mut v).range(0.0..=100.0).suffix("%").max_decimals(0)).changed() {
set(d, path, json!(v));
}
}
fn angle(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], enabled: bool, global: f64) {
let mut v = if enabled { num(d, path) } else { global };
let r = ui.add_enabled(enabled, egui::DragValue::new(&mut v).range(-180.0..=180.0).suffix("°").max_decimals(1));
if r.changed() {
set(d, path, json!(v));
}
}
fn flag(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], label: &str) {
let mut v = {
let mut cur = d.fields.get("fx").unwrap_or(&Value::Null);
for k in path {
cur = &cur[*k];
}
cur.as_bool().unwrap_or(false)
};
if ui.checkbox(&mut v, crate::rtl::widget(ui, label)).changed() {
set(d, path, json!(v));
}
}
/// A swatch of the document.
fn swatch(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) {
let cur = {
let mut v = d.fields.get("fx").unwrap_or(&Value::Null);
for k in path {
v = &v[*k];
}
v.as_str().unwrap_or("[Black]").to_string()
};
let names: Vec<String> = app
.session
.active()
.map_or_else(Vec::new, |st| st.doc.swatches.iter().filter(|w| !w.hidden || w.name == cur).map(|w| w.name.clone()).collect());
egui::ComboBox::from_id_salt(("fx_swatch", path.join("."))).selected_text(&cur).width(150.0).show_ui(ui, |ui| {
for n in names {
if ui.selectable_label(n == cur, &n).clicked() {
set(d, path, json!(n));
}
}
});
}
fn heading(ui: &mut egui::Ui, text: &str) {
ui.add_space(6.0);
crate::rtl::label(ui, egui::RichText::new(text).font(semibold(12.0)));
}
/// Distance and angle as an X (`y`: false) or Y offset: InDesign's, the shadow falls away from
/// the light. Editing an offset sets the distance and (without Global Light) the angle.
fn offset(ui: &mut egui::Ui, d: &mut Dialog, key: &str, unit: Unit, global: f64, y_axis: bool) {
let gl = d.fields.get("fx").is_some_and(|f| f[key]["globalLight"].as_bool().unwrap_or(false));
let a = if gl { global } else { num(d, &[key, "angle"]) }.to_radians();
let dist = num(d, &[key, "distance"]);
let (mut x, mut y) = (-dist * a.cos(), dist * a.sin());
let v = if y_axis { &mut y } else { &mut x };
let w =
egui::DragValue::new(v).speed(0.25).custom_formatter(move |n, _| format_measure(n, unit)).custom_parser(move |s| parse_measure(s, unit).ok());
if ui.add_sized([90.0, 20.0], w).changed() {
set(d, &[key, "distance"], json!(x.hypot(y)));
if !gl {
set(d, &[key, "angle"], json!(y.atan2(-x).to_degrees()));
}
}
}
/// The settings of `page`.
fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
let lang = app.ui.language.clone();
let tr = |s: &str| crate::i18n::tr(&lang, s).to_string();
let unit = app.session.active().map_or(Unit::Points, |st| st.doc.settings.horizontal_units);
let global = app.session.active().map_or(120.0, |st| st.doc.settings.global_light);
let row = |ui: &mut egui::Ui, l: &str| crate::rtl::label(ui, tr(l));
crate::rtl::label(ui, egui::RichText::new(tr(PAGES.iter().find(|p| p.0 == page).map_or("Transparency", |p| p.1))).font(semibold(14.0)));
let grid = |id: &str| egui::Grid::new(("fx_grid", id.to_string())).num_columns(4).spacing([8.0, 6.0]);
match page {
"transparency" => {
heading(ui, &tr("Basic Blending"));
grid("t").show(ui, |ui| {
row(ui, "Mode:");
let cur = d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal").to_string();
egui::ComboBox::from_id_salt("fx_blend").selected_text(tr(&cur)).width(150.0).show_ui(ui, |ui| {
for m in designcraft_color::BlendMode::ALL {
if ui.selectable_label(m.label() == cur, tr(m.label())).clicked() {
d.fields.insert("blend".into(), json!(m.label()));
}
}
});
row(ui, "Opacity:");
let mut o = d.fields.get("opacity").and_then(Value::as_f64).unwrap_or(1.0) * 100.0;
if ui.add_sized([70.0, 20.0], egui::DragValue::new(&mut o).range(0.0..=100.0).suffix("%").max_decimals(0)).changed() {
d.fields.insert("opacity".into(), json!(o / 100.0));
}
ui.end_row();
});
}
"dropShadow" | "innerShadow" => {
let key = page;
heading(ui, &tr("Blending"));
grid("b").show(ui, |ui| {
row(ui, "Color:");
swatch(app, ui, d, &[key, "color"]);
row(ui, "Opacity:");
percent_of_one(ui, d, &[key, "opacity"]);
ui.end_row();
});
heading(ui, &tr("Position"));
grid("p").show(ui, |ui| {
row(ui, "Distance:");
measure(ui, d, &[key, "distance"], unit);
row(ui, "X Offset:");
offset(ui, d, key, unit, global, false);
ui.end_row();
row(ui, "Angle:");
let gl = d.fields.get("fx").is_some_and(|f| f[key]["globalLight"].as_bool().unwrap_or(false));
angle(ui, d, &[key, "angle"], !gl, global);
row(ui, "Y Offset:");
offset(ui, d, key, unit, global, true);
ui.end_row();
crate::rtl::label(ui, "");
flag(ui, d, &[key, "globalLight"], &tr("Use Global Light"));
ui.end_row();
});
heading(ui, &tr("Options"));
grid("o").show(ui, |ui| {
row(ui, "Size:");
measure(ui, d, &[key, "size"], unit);
if key == "dropShadow" {
row(ui, "Spread:");
percent(ui, d, &[key, "spread"]);
} else {
row(ui, "Choke:");
percent(ui, d, &[key, "choke"]);
}
ui.end_row();
});
}
"outerGlow" | "innerGlow" => {
let key = page;
heading(ui, &tr("Blending"));
grid("b").show(ui, |ui| {
row(ui, "Color:");
swatch(app, ui, d, &[key, "color"]);
row(ui, "Opacity:");
percent_of_one(ui, d, &[key, "opacity"]);
ui.end_row();
});
heading(ui, &tr("Options"));
grid("o").show(ui, |ui| {
row(ui, "Size:");
measure(ui, d, &[key, "size"], unit);
if key == "outerGlow" {
row(ui, "Spread:");
percent(ui, d, &[key, "spread"]);
} else {
row(ui, "Choke:");
percent(ui, d, &[key, "choke"]);
ui.end_row();
row(ui, "Source:");
let mut center = d.fields.get("fx").is_some_and(|f| f["innerGlow"]["center"].as_bool().unwrap_or(false));
let label = |c: bool| tr(if c { "Center" } else { "Edge" });
egui::ComboBox::from_id_salt("fx_source").selected_text(label(center)).show_ui(ui, |ui| {
for c in [false, true] {
if ui.selectable_value(&mut center, c, label(c)).clicked() {
set(d, &["innerGlow", "center"], json!(c));
}
}
});
}
ui.end_row();
});
}
"bevel" => {
heading(ui, &tr("Structure"));
grid("s").show(ui, |ui| {
row(ui, "Size:");
measure(ui, d, &["bevel", "size"], unit);
row(ui, "Depth:");
let mut v = num(d, &["bevel", "depth"]);
if ui.add_sized([70.0, 20.0], egui::DragValue::new(&mut v).range(1.0..=1000.0).suffix("%").max_decimals(0)).changed() {
set(d, &["bevel", "depth"], json!(v));
}
ui.end_row();
});
heading(ui, &tr("Shading"));
grid("sh").show(ui, |ui| {
row(ui, "Angle:");
let gl = d.fields.get("fx").is_some_and(|f| f["bevel"]["globalLight"].as_bool().unwrap_or(false));
angle(ui, d, &["bevel", "angle"], !gl, global);
flag(ui, d, &["bevel", "globalLight"], &tr("Use Global Light"));
ui.end_row();
row(ui, "Highlight:");
swatch(app, ui, d, &["bevel", "highlight"]);
row(ui, "Opacity:");
percent_of_one(ui, d, &["bevel", "highlightOpacity"]);
ui.end_row();
row(ui, "Shadow:");
swatch(app, ui, d, &["bevel", "shadow"]);
row(ui, "Opacity:");
percent_of_one(ui, d, &["bevel", "shadowOpacity"]);
ui.end_row();
});
}
"satin" => {
heading(ui, &tr("Blending"));
grid("b").show(ui, |ui| {
row(ui, "Color:");
swatch(app, ui, d, &["satin", "color"]);
row(ui, "Opacity:");
percent_of_one(ui, d, &["satin", "opacity"]);
ui.end_row();
});
heading(ui, &tr("Options"));
grid("o").show(ui, |ui| {
row(ui, "Angle:");
angle(ui, d, &["satin", "angle"], true, global);
row(ui, "Distance:");
measure(ui, d, &["satin", "distance"], unit);
ui.end_row();
row(ui, "Size:");
measure(ui, d, &["satin", "size"], unit);
crate::rtl::label(ui, "");
flag(ui, d, &["satin", "invert"], &tr("Invert"));
ui.end_row();
});
}
"feather" => {
heading(ui, &tr("Options"));
grid("o").show(ui, |ui| {
row(ui, "Feather Width:");
measure(ui, d, &["feather"], unit);
ui.end_row();
});
}
"directionalFeather" => {
heading(ui, &tr("Feather Widths"));
grid("w").show(ui, |ui| {
for (i, l) in ["Top:", "Left:", "Bottom:", "Right:"].into_iter().enumerate() {
row(ui, l);
let mut v = d.fields.get("fx").and_then(|f| f["directionalFeather"]["widths"][i].as_f64()).unwrap_or(0.0);
let w = egui::DragValue::new(&mut v)
.range(0.0..=10_000.0)
.speed(0.25)
.custom_formatter(move |n, _| format_measure(n, unit))
.custom_parser(move |s| parse_measure(s, unit).ok());
if ui.add_sized([90.0, 20.0], w).changed() {
let mut ws: Vec<f64> =
(0..4).map(|k| d.fields.get("fx").and_then(|f| f["directionalFeather"]["widths"][k].as_f64()).unwrap_or(0.0)).collect();
if let Some(slot) = ws.get_mut(i) {
*slot = v;
}
set(d, &["directionalFeather", "widths"], json!(ws));
}
if i % 2 == 1 {
ui.end_row();
}
}
});
}
"gradientFeather" => {
heading(ui, &tr("Gradient Stops"));
grid("g").show(ui, |ui| {
row(ui, "Start Opacity:");
percent_of_one(ui, d, &["gradientFeather", "start"]);
row(ui, "End Opacity:");
percent_of_one(ui, d, &["gradientFeather", "end"]);
ui.end_row();
});
heading(ui, &tr("Options"));
grid("o").show(ui, |ui| {
row(ui, "Type:");
let mut radial = d.fields.get("fx").is_some_and(|f| f["gradientFeather"]["radial"].as_bool().unwrap_or(false));
let label = |r: bool| tr(if r { "Radial" } else { "Linear" });
egui::ComboBox::from_id_salt("fx_gtype").selected_text(label(radial)).show_ui(ui, |ui| {
for r in [false, true] {
if ui.selectable_value(&mut radial, r, label(r)).clicked() {
set(d, &["gradientFeather", "radial"], json!(r));
}
}
});
row(ui, "Angle:");
angle(ui, d, &["gradientFeather", "angle"], true, global);
ui.end_row();
});
}
_ => {}
}
}
/// One line per applied level, as in InDesign's summary: "OBJECT: Normal 100%; Drop Shadow".
fn summary(lang: &str, d: &Dialog) -> String {
let fx = d.fields.get("fx").cloned().unwrap_or(Value::Null);
let blend = d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal");
let opacity = d.fields.get("opacity").and_then(Value::as_f64).unwrap_or(1.0);
let on: Vec<String> = PAGES.iter().skip(1).filter(|p| is_on(&fx, p.0)).map(|p| crate::i18n::tr(lang, p.1).to_string()).collect();
let effects = if on.is_empty() { crate::i18n::tr(lang, "(no effects)").to_string() } else { on.join(", ") };
format!("{}: {} {:.0}%; {}", crate::i18n::tr(lang, "OBJECT").to_uppercase(), crate::i18n::tr(lang, blend), opacity * 100.0, effects)
}
/// The dialog's body.
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
let lang = app.ui.language.clone();
let tr = |s: &str| crate::i18n::tr(&lang, s).to_string();
ui.horizontal(|ui| {
crate::rtl::label(ui, tr("Settings for:"));
// Stroke, Fill and Text levels aren't in the document model yet: the object only.
ui.add_enabled_ui(false, |ui| {
egui::ComboBox::from_id_salt("fx_level").selected_text(tr("Object")).width(110.0).show_ui(ui, |_| {});
});
});
ui.add_space(6.0);
let page = d.fields.get("page").and_then(Value::as_str).unwrap_or("transparency").to_string();
ui.horizontal_top(|ui| {
ui.vertical(|ui| {
ui.set_width(180.0);
for (key, label) in PAGES {
ui.horizontal(|ui| {
if key == "transparency" {
ui.add_space(24.0);
} else {
let fx = d.fields.get("fx").cloned().unwrap_or(Value::Null);
let mut on = is_on(&fx, key);
if ui.checkbox(&mut on, "").changed() {
if on {
if let Some(f) = d.fields.get_mut("fx") {
turn_on(f, key);
}
} else if key == "feather" {
set(d, &["feather"], json!(0.0));
} else {
set(d, &[key, "on"], json!(false));
}
}
}
if ui.selectable_label(page == key, tr(label)).clicked() {
d.fields.insert("page".into(), json!(key));
if let Some(f) = d.fields.get_mut("fx") {
turn_on(f, key);
}
}
});
}
ui.add_space(8.0);
crate::rtl::label(ui, egui::RichText::new(summary(&lang, d)).size(11.0).color(crate::theme::Tokens::get(ui.ctx()).text_dim));
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.set_min_width(380.0);
page_ui(app, ui, d, &page);
});
});
ui.add_space(10.0);
let mut p = d.fields.get("preview").and_then(Value::as_bool).unwrap_or(false);
if ui.checkbox(&mut p, crate::rtl::widget(ui, tr("Preview"))).changed() {
d.fields.insert("preview".into(), json!(p));
}
if let Some(st) = d.fields.get("status").and_then(Value::as_str) {
crate::rtl::label(ui, egui::RichText::new(st).color(crate::theme::Tokens::get(ui.ctx()).text_dim));
}
preview(app, d);
}
#[cfg(test)]
mod tests {
use super::*;
fn app_with_frame() -> (DesignApp, u64) {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
app.run("document.preferences", json!({"horizontalUnits": "inches", "verticalUnits": "inches"})).unwrap();
let id = app.run("frame.create", json!({"rect": [100, 100, 300, 300]})).unwrap()["id"].as_u64().unwrap();
app.run("object.fill", json!({"swatch": "[Paper]"})).unwrap();
app.run("selection.set", json!({"ids": [id]})).unwrap();
(app, id)
}
fn frame(app: &mut DesignApp, ctx: &egui::Context) -> Vec<String> {
let mut texts = Vec::new();
for _ in 0..2 {
let input =
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()));
out.textures_delta.clear();
fn walk(s: egui::epaint::Shape, texts: &mut Vec<String>) {
match s {
egui::epaint::Shape::Text(t) => texts.push(t.galley.text().to_string()),
egui::epaint::Shape::Vec(v) => v.into_iter().for_each(|s| walk(s, texts)),
_ => {}
}
}
texts.clear();
for c in out.shapes {
walk(c.shape, &mut texts);
}
}
texts
}
#[test]
fn the_effects_dialog_opens_on_the_chosen_effect_previews_and_applies_once() {
let (mut app, id) = app_with_frame();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx, "en");
let undo = |app: &DesignApp| app.session.doc().unwrap().history.undo.len();
let fx = |app: &DesignApp| app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().effects.clone();
let u0 = undo(&app);
// Object › Effects › Inner Shadow… (as the menu activates it): the dialog, on that page.
crate::menus::activate(&mut app, "object.innerShadow", &Value::Null);
assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some(ID));
let texts = frame(&mut app, &ctx);
for t in [
"Effects",
"Settings for:",
"Transparency",
"Inner Shadow",
"Drop Shadow",
"Blending",
"Position",
"Distance:",
"Angle:",
"Use Global Light",
"Options",
"Size:",
"Choke:",
"Preview",
"OK",
"Cancel",
] {
assert!(texts.iter().any(|x| x.trim() == t), "{t}: {texts:?}");
}
assert!(texts.iter().any(|x| x.contains("0.097 in")), "distance in inches: {texts:?}");
// Preview: the shadow is on the page while the dialog is open, without an undo step.
frame(&mut app, &ctx);
assert!(fx(&app).inner_shadow.on);
assert_eq!(undo(&app), u0);
// Cancel: as before.
crate::dialogs::cancel(&mut app);
assert!(!fx(&app).inner_shadow.on);
assert_eq!(undo(&app), u0);
// Again, with a change, then OK: one undo step.
crate::menus::activate(&mut app, "object.innerShadow", &Value::Null);
frame(&mut app, &ctx);
if let Some(d) = app.ui.dialog.as_mut() {
set(d, &["innerShadow", "size"], json!(9.0));
d.fields.insert("opacity".into(), json!(0.8));
}
frame(&mut app, &ctx);
crate::dialogs::confirm(&mut app).unwrap();
let e = fx(&app);
assert!(e.inner_shadow.on && e.inner_shadow.size == 9.0);
assert_eq!(undo(&app), u0 + 1, "one undo step");
assert!((app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().opacity - 0.8).abs() < 1e-6);
app.run("edit.undo", json!({})).unwrap();
assert!(!fx(&app).inner_shadow.on);
}
#[test]
fn offsets_follow_distance_and_angle_as_in_indesign() {
let (mut app, _) = app_with_frame();
open(&mut app, "dropShadow");
let d = app.ui.dialog.as_mut().unwrap();
set(d, &["dropShadow", "distance"], json!(7.0));
set(d, &["dropShadow", "angle"], json!(120.0));
// InDesign shows 7 pt at 120° as X 0.0486 in, Y 0.0842 in.
let a = 120f64.to_radians();
assert!(((-7.0 * a.cos()) / 72.0 - 0.0486).abs() < 1e-4 && ((7.0 * a.sin()) / 72.0 - 0.0842).abs() < 1e-4);
let p = params(d);
assert_eq!(p["effects"]["dropShadow"]["on"], true, "chosen from the menu: on");
assert_eq!(summary("en", d), "OBJECT: Normal 100%; Drop Shadow");
}
}
+34
View File
@@ -67,6 +67,40 @@ const TABLE: &[(&str, [&str; 5])] = &[
),
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
("Effects…", ["Effekte…", "Effets…", "Efectos…", "効果…", "效果…"]),
("Settings for:", ["Einstellungen für:", "Paramètres pour :", "Ajustes para:", "設定対象:", "设置:"]),
("Transparency", ["Transparenz", "Transparence", "Transparencia", "透明", "透明度"]),
("Transparency…", ["Transparenz…", "Transparence…", "Transparencia…", "透明…", "透明度…"]),
("Basic Blending", ["Grundlegendes Füllen", "Fusion de base", "Fusión básica", "基本的な描画モード", "基本混合"]),
("Opacity:", ["Deckkraft:", "Opacité :", "Opacidad:", "不透明度:", "不透明度:"]),
("Blending", ["Füllmethode", "Fusion", "Fusión", "描画モード", "混合"]),
("Distance:", ["Abstand:", "Distance :", "Distancia:", "距離:", "距离:"]),
("X Offset:", ["X-Versatz:", "Décalage X :", "Desplazamiento X:", "X オフセット:", "X 位移:"]),
("Y Offset:", ["Y-Versatz:", "Décalage Y :", "Desplazamiento Y:", "Y オフセット:", "Y 位移:"]),
("Spread:", ["Größe ändern:", "Étendue :", "Extensión:", "スプレッド:", "扩展:"]),
("Choke:", ["Unterfüllen:", "Contraction :", "Contracción:", "チョーク:", "收缩:"]),
("Source:", ["Quelle:", "Source :", "Origen:", "ソース:", "源:"]),
("Edge", ["Kante", "Contour", "Borde", "エッジ", "边缘"]),
("Depth:", ["Tiefe:", "Profondeur :", "Profundidad:", "深さ:", "深度:"]),
("Highlight:", ["Lichter:", "Lumière :", "Iluminación:", "ハイライト:", "高光:"]),
("Shadow:", ["Tiefen:", "Ombre :", "Sombra:", "シャドウ:", "阴影:"]),
("Feather Width:", ["Breite der weichen Kante:", "Largeur du contour progressif :", "Anchura de desvanecimiento:", "ぼかしの幅:", "羽化宽度:"]),
("Feather Widths", ["Breiten der weichen Kante", "Largeurs du contour progressif", "Anchuras de desvanecimiento", "ぼかしの幅", "羽化宽度"]),
("Gradient Stops", ["Verlaufsregler", "Arrêts de dégradé", "Paradas de degradado", "グラデーション分岐点", "渐变色标"]),
("Start Opacity:", ["Anfangsdeckkraft:", "Opacité de début :", "Opacidad inicial:", "開始の不透明度:", "起始不透明度:"]),
("End Opacity:", ["Enddeckkraft:", "Opacité de fin :", "Opacidad final:", "終了の不透明度:", "结束不透明度:"]),
("(no effects)", ["(keine Effekte)", "(aucun effet)", "(sin efectos)", "(効果なし)", "(无效果)"]),
("OBJECT", ["OBJEKT", "OBJET", "OBJETO", "オブジェクト", "对象"]),
(
"Add an object effect to the selected target",
[
"Dem ausgewählten Ziel einen Objekteffekt hinzufügen",
"Ajouter un effet d’objet à la cible sélectionnée",
"Añadir un efecto de objeto al destino seleccionado",
"選択した対象にオブジェクト効果を追加",
"为所选目标添加对象效果",
],
),
("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]),
("Duplicate", ["Duplizieren", "Dupliquer", "Duplicar", "複製", "重复"]),
("Select All", ["Alles auswählen", "Tout sélectionner", "Seleccionar todo", "すべてを選択", "全选"]),
+24
View File
@@ -33,6 +33,30 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste Vector Art", "لصق رسم متجه"),
("Reverse Path", "عكس المسار"),
("Set Anchor", "تعيين نقطة الارتساء"),
("Effects…", "التأثيرات…"),
("Settings for:", "الإعدادات لـ:"),
("Transparency…", "الشفافية…"),
("Basic Blending", "المزج الأساسي"),
("Opacity:", "العتامة:"),
("Blending", "المزج"),
("Distance:", "المسافة:"),
("X Offset:", "إزاحة X:"),
("Y Offset:", "إزاحة Y:"),
("Spread:", "الانتشار:"),
("Choke:", "الانكماش:"),
("Source:", "المصدر:"),
("Edge", "الحافة"),
("Depth:", "العمق:"),
("Highlight:", "الإبراز:"),
("Shadow:", "الظل:"),
("Feather Width:", "عرض التدرج:"),
("Feather Widths", "عروض التدرج"),
("Gradient Stops", "نقاط التدرج"),
("Start Opacity:", "عتامة البداية:"),
("End Opacity:", "عتامة النهاية:"),
("(no effects)", "(بلا تأثيرات)"),
("OBJECT", "الكائن"),
("Add an object effect to the selected target", "إضافة تأثير كائن إلى الهدف المحدد"),
("Clear", "مسح"),
("Delete", "حذف"),
("Duplicate", "تكرار"),
+24
View File
@@ -31,6 +31,30 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste Vector Art", "Incolla grafica vettoriale"),
("Reverse Path", "Inverti tracciato"),
("Set Anchor", "Imposta ancoraggio"),
("Effects…", "Effetti…"),
("Settings for:", "Impostazioni per:"),
("Transparency…", "Trasparenza…"),
("Basic Blending", "Fusione di base"),
("Opacity:", "Opacità:"),
("Blending", "Fusione"),
("Distance:", "Distanza:"),
("X Offset:", "Spostamento X:"),
("Y Offset:", "Spostamento Y:"),
("Spread:", "Estensione:"),
("Choke:", "Contrazione:"),
("Source:", "Origine:"),
("Edge", "Bordo"),
("Depth:", "Profondità:"),
("Highlight:", "Luce:"),
("Shadow:", "Ombra:"),
("Feather Width:", "Larghezza sfumatura:"),
("Feather Widths", "Larghezze sfumatura"),
("Gradient Stops", "Interruzioni sfumatura"),
("Start Opacity:", "Opacità iniziale:"),
("End Opacity:", "Opacità finale:"),
("(no effects)", "(nessun effetto)"),
("OBJECT", "OGGETTO"),
("Add an object effect to the selected target", "Aggiungi un effetto oggetto all'obiettivo selezionato"),
("Clear", "Cancella"),
("Duplicate", "Duplica"),
("Select All", "Seleziona tutto"),
+24
View File
@@ -33,6 +33,30 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste Vector Art", "ベクトルアートをペースト"),
("Reverse Path", "パスの方向反転"),
("Set Anchor", "アンカーポイントを設定"),
("Effects…", "効果…"),
("Settings for:", "設定対象:"),
("Transparency…", "透明…"),
("Basic Blending", "基本的な描画モード"),
("Opacity:", "不透明度:"),
("Blending", "描画モード"),
("Distance:", "距離:"),
("X Offset:", "X オフセット:"),
("Y Offset:", "Y オフセット:"),
("Spread:", "スプレッド:"),
("Choke:", "チョーク:"),
("Source:", "ソース:"),
("Edge", "エッジ"),
("Depth:", "深さ:"),
("Highlight:", "ハイライト:"),
("Shadow:", "シャドウ:"),
("Feather Width:", "ぼかしの幅:"),
("Feather Widths", "ぼかしの幅"),
("Gradient Stops", "グラデーション分岐点"),
("Start Opacity:", "開始の不透明度:"),
("End Opacity:", "終了の不透明度:"),
("(no effects)", "(効果なし)"),
("OBJECT", "オブジェクト"),
("Add an object effect to the selected target", "選択した対象にオブジェクト効果を追加"),
("Clear", "消去"),
("Duplicate", "複製"),
("Select All", "すべてを選択"),
+24
View File
@@ -34,6 +34,30 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste Vector Art", "Colar arte vetorial"),
("Reverse Path", "Inverter demarcador"),
("Set Anchor", "Definir ponto de ancoragem"),
("Effects…", "Efeitos…"),
("Settings for:", "Configurações para:"),
("Transparency…", "Transparência…"),
("Basic Blending", "Mesclagem básica"),
("Opacity:", "Opacidade:"),
("Blending", "Mesclagem"),
("Distance:", "Distância:"),
("X Offset:", "Deslocamento X:"),
("Y Offset:", "Deslocamento Y:"),
("Spread:", "Espalhamento:"),
("Choke:", "Contração:"),
("Source:", "Origem:"),
("Edge", "Borda"),
("Depth:", "Profundidade:"),
("Highlight:", "Realce:"),
("Shadow:", "Sombra:"),
("Feather Width:", "Largura do difuminado:"),
("Feather Widths", "Larguras do difuminado"),
("Gradient Stops", "Paradas de gradiente"),
("Start Opacity:", "Opacidade inicial:"),
("End Opacity:", "Opacidade final:"),
("(no effects)", "(sem efeitos)"),
("OBJECT", "OBJETO"),
("Add an object effect to the selected target", "Adicionar um efeito de objeto ao alvo selecionado"),
("Clear", "Limpar"),
("Delete", "Excluir"),
("Duplicate", "Duplicar"),
+24
View File
@@ -32,6 +32,30 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Paste Vector Art", "Вставити векторну графіку"),
("Reverse Path", "Розвернути контур"),
("Set Anchor", "Задати прив’язку"),
("Effects…", "Ефекти…"),
("Settings for:", "Параметри для:"),
("Transparency…", "Прозорість…"),
("Basic Blending", "Базове змішування"),
("Opacity:", "Непрозорість:"),
("Blending", "Змішування"),
("Distance:", "Відстань:"),
("X Offset:", "Зсув X:"),
("Y Offset:", "Зсув Y:"),
("Spread:", "Розмах:"),
("Choke:", "Стискання:"),
("Source:", "Джерело:"),
("Edge", "Край"),
("Depth:", "Глибина:"),
("Highlight:", "Світло:"),
("Shadow:", "Тінь:"),
("Feather Width:", "Ширина розмиття:"),
("Feather Widths", "Ширина розмиття"),
("Gradient Stops", "Точки градієнта"),
("Start Opacity:", "Початкова непрозорість:"),
("End Opacity:", "Кінцева непрозорість:"),
("(no effects)", "(без ефектів)"),
("OBJECT", "ОБ'ЄКТ"),
("Add an object effect to the selected target", "Додати ефект об'єкта до вибраної цілі"),
("Clear", "Очистити"),
("Duplicate", "Дублювати"),
("Select All", "Виділити все"),
+1
View File
@@ -14,6 +14,7 @@ pub mod credits;
pub mod cursors;
pub mod dialogs;
pub mod dock;
pub mod effects_dialog;
pub mod i18n;
pub mod icons;
pub mod menus;
+18
View File
@@ -32,6 +32,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
),
("app.loadCharacterStyles", "Load Character Styles…", None, "{} — the same for its character styles"),
("app.loadTextStyles", "Load All Text Styles…", None, "{} — the same for its paragraph and character styles"),
(
"effects.dialog",
"Effects…",
None,
"{page?: transparency|dropShadow|innerShadow|outerGlow|innerGlow|bevel|satin|feather|directionalFeather|gradientFeather} — the Effects dialog for the selected objects",
),
("app.packageDialog", "Package…", Some("Cmd+Alt+Shift+P"), "{} — choose the package folder to create (file.package)"),
("app.saveSwatches", "Save Swatches for Exchange…", None, "{path?} — write the colour swatches as .ase"),
("app.exportPng", "Export Page as PNG…", Some("Cmd+E"), "{}"),
@@ -561,6 +567,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
"cmd:object.cornerOptions",
"-",
">Effects",
"ui:effects.dialog|Transparency…|{\"page\": \"transparency\"}",
"cmd:object.dropShadow",
"cmd:object.innerShadow",
"cmd:object.outerGlow",
@@ -990,6 +997,10 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
None => Ok(Value::Null),
}
}
"effects.dialog" => {
crate::effects_dialog::open(app, p.get("page").and_then(Value::as_str).unwrap_or("transparency"));
Ok(Value::Null)
}
"app.loadSwatches" => {
if let Some(pick) = app.services.pick_open.as_mut() {
return Some(match pick("swatches") {
@@ -2144,6 +2155,13 @@ pub fn activate(app: &mut DesignApp, id: &str, params: &Value) {
if waits_for_ime(app, id) {
return;
}
// Object › Effects › Drop Shadow… (and its shortcut): the Effects dialog on that page.
if params.is_null()
&& let Some(page) = crate::effects_dialog::page_of_command(id)
{
crate::effects_dialog::open(app, page);
return;
}
if params.is_null() && id == "file.close" {
close_document(app, None);
return;
+14
View File
@@ -2358,6 +2358,20 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
let _ = app.run("object.transparencyGroup", json!({"knockout": ko}));
}
});
// The fx button, as at the bottom of InDesign's Effects panel: each effect opens the Effects
// dialog on its page.
let fx = ui
.add(egui::Button::new(egui::RichText::new("fx").italics().strong()).min_size(vec2(28.0, 20.0)))
.on_hover_text(crate::i18n::tr(&app.ui.language, "Add an object effect to the selected target"));
egui::Popup::menu(&fx).show(|ui| {
for (page, label) in crate::effects_dialog::PAGES {
let text = format!("{}…", crate::i18n::tr(&app.ui.language, label));
if ui.button(crate::rtl::widget(ui, text)).clicked() {
crate::effects_dialog::open(app, page);
ui.close();
}
}
});
}
pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {