From d0ef81e9f7afeb5123328ec637daf9e46aa85198 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 29 Oct 2026 20:00:04 +0700 Subject: [PATCH] Effects: Roughen and Pucker & Bloat show sliders (#4488) --- crates/ui-egui/src/dialogs/effect.rs | 48 +++++++++++++++++++++------- crates/ui-egui/src/dialogs/tests.rs | 29 +++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index b668f0791..5c91cb962 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -166,7 +166,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { None if id == TEXTURIZER => glass_fields(app, ui, d, TEXTURIZER), None if id == TRANSFORM => transform_fields(ui, d, app.session.general_unit()), None if id == SCRIBBLE => scribble_fields(ui, d, app.session.general_unit()), - None if id == ZIG_ZAG => zig_zag_fields(ui, d, app.session.general_unit()), + None if id == ZIG_ZAG || id == ROUGHEN => size_fields(ui, d, &id, app.session.general_unit()), + None if id == PUCKER_BLOAT => pucker_bloat_fields(ui, d), None if id == DROP_SHADOW => drop_shadow_fields(ui, d, app.session.general_unit()), None if id == "artistic.filmGrain" => film_grain_fields(ui, d), None if id == free_distort::FREE_DISTORT => { @@ -559,27 +560,40 @@ fn transform_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Un /// The Zig Zag effect (Effect › Distort & Transform › Zig Zag…). const ZIG_ZAG: &str = "distort.zigZag"; -/// Zig Zag's dialog (#1133): Size and Ridges per segment as sliders with their fields, Relative or -/// Absolute and the Points as radio buttons. Returns whether the values differ from those last -/// previewed. -fn zig_zag_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit) -> bool { +/// The Roughen effect (Effect › Distort & Transform › Roughen…). +const ROUGHEN: &str = "distort.roughen"; + +/// The Pucker & Bloat effect (Effect › Distort & Transform › Pucker & Bloat…). +const PUCKER_BLOAT: &str = "distort.puckerBloat"; + +/// The second slider of [`size_fields`]: Zig Zag's Ridges per segment, Roughen's Detail (per inch), +/// as (key, label, default, suffix). +fn second_slider(effect: &str) -> (&'static str, &'static str, f64, &'static str) { + if effect == ROUGHEN { ("detail", "Detail:", 10.0, "/in") } else { ("ridges", "Ridges per segment:", 4.0, "") } +} + +/// Zig Zag's and Roughen's dialog (#1133, #4488): Size and Ridges per segment (Roughen: Detail) as +/// sliders with their fields, Relative or Absolute and the Points as radio buttons. Returns +/// whether the values differ from those last previewed. +fn size_fields(ui: &mut egui::Ui, d: &mut Dialog, effect: &str, unit: vectorcraft_doc::Unit) -> bool { const LABEL_W: f32 = 124.0; const FIELD_W: f32 = 70.0; let rail = Tokens::get(ui.ctx()).input_border; let track = move |_| rail; let relative = d.bool("relative"); + let size = if effect == ROUGHEN { 5.0 } else { 10.0 }; widgets::subheader(ui, tl!("Options")); // Size is a share of the object's size (Relative) or a distance (Absolute). widgets::label_row(ui, tl!("Size:"), LABEL_W, |ui| { let shown = if relative { - Some(widgets::SliderValue::field(d.f64("size", 10.0).clamp(0.0, 100.0), 0.0..=100.0, "%", 0)) + Some(widgets::SliderValue::field(d.f64("size", size).clamp(0.0, 100.0), 0.0..=100.0, "%", 0)) } else { form::length_value(d, "size", unit, 0.0..=100.0) }; form::slider_rail_valued(ui, d, "size", tl!("Size:"), 0.0..=100.0, shown, &track); if !relative { form::length(ui, d, "size", unit, FIELD_W); - } else if let Some(n) = widgets::range_field(ui, ("zz", "size"), d.f64("size", 10.0), 0.0..=100.0, "%", 0, FIELD_W) { + } else if let Some(n) = widgets::range_field(ui, ("zz", "size"), d.f64("size", size), 0.0..=100.0, "%", 0, FIELD_W) { d.fields.insert("size".into(), json!(n)); } }); @@ -591,17 +605,18 @@ fn zig_zag_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit } } }); - widgets::label_row(ui, tl!("Ridges per segment:"), LABEL_W, |ui| { - form::slider_rail_named(ui, d, "ridges", tl!("Ridges per segment:"), 0.0..=100.0, &track); - if let Some(n) = widgets::range_field(ui, ("zz", "ridges"), d.f64("ridges", 4.0), 0.0..=100.0, "", 0, FIELD_W) { - d.fields.insert("ridges".into(), json!(n)); + let (key, label, default, suffix) = second_slider(effect); + widgets::label_row(ui, tl!(label), LABEL_W, |ui| { + form::slider_rail_named(ui, d, key, tl!(label), 0.0..=100.0, &track); + if let Some(n) = widgets::range_field(ui, ("zz", key), d.f64(key, default), 0.0..=100.0, suffix, 0, FIELD_W) { + d.fields.insert(key.into(), json!(n)); } }); ui.add_space(4.0); widgets::subheader(ui, crate::i18n::tr_ctx(crate::i18n::current(), "effect", "Points")); let points = d.str("points"); ui.horizontal(|ui| { - for (label, value) in choices(ZIG_ZAG, "points").unwrap_or_default() { + for (label, value) in choices(effect, "points").unwrap_or_default() { if widgets::radio(ui, crate::i18n::tr_ctx(crate::i18n::current(), "effect", label), points == *value, true) { d.fields.insert("points".into(), json!(value)); } @@ -610,6 +625,15 @@ fn zig_zag_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit values_changed(d) } +/// Pucker & Bloat's dialog (#4488): its amount as a slider from Pucker (−200 %) to Bloat (200 %) +/// with its field. Returns whether the value differs from the one last previewed. +fn pucker_bloat_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let rail = Tokens::get(ui.ctx()).input_border; + form::slider(ui, d, "amount", tl!("Amount:"), -200.0..=200.0, "%", &move |_| rail); + form::slider_ends(ui, form::SLIDER_LABEL, (tl!("Pucker"), tl!("Bloat"))); + values_changed(d) +} + /// Drop Shadow (Effect › Stylize › Drop Shadow…). const DROP_SHADOW: &str = "stylize.dropShadow"; diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index cd3871f0e..3ce0099bd 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -1413,6 +1413,35 @@ fn zig_zag_has_sliders_and_radio_buttons() { assert!(!lines.contains(&"Ridges:"), "{lines:?}"); } +/// #4488: Roughen's dialog has Size and Detail sliders laid out as Zig Zag's, and Pucker & Bloat's +/// its amount as a slider from Pucker to Bloat. +#[test] +fn roughen_and_pucker_bloat_have_sliders() { + use crate::widgets::accessibility_tests::{slider_value, tree}; + 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.roughen"})).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", "Detail:", "Points", "Smooth", "Corner"] { + assert!(lines.contains(&want), "no {want:?} in {lines:?}"); + } + let shown = tree(|ui| show(&mut app, ui.ctx())); + assert_eq!(slider_value(&shown, "Size:"), (Some(5.0), Some(0.0), Some(100.0), Some("5%".into()))); + assert_eq!(slider_value(&shown, "Detail:"), (Some(10.0), Some(0.0), Some(100.0), None)); + app.ui.dialog = None; + app.run("effect.dialog", json!({"effect": "distort.puckerBloat"})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("amount".into(), json!(-60)); + 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 ["Amount:", "Pucker", "Bloat"] { + assert!(lines.contains(&want), "no {want:?} in {lines:?}"); + } + let shown = tree(|ui| show(&mut app, ui.ctx())); + let (value, min, max, _) = slider_value(&shown, "Amount:"); + assert_eq!((value, min, max), (Some(-60.0), Some(-200.0), Some(200.0))); +} + /// 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