From ae3a8fe4c32a142199e21a9fffdbbc354f575aec Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Sun, 18 Oct 2026 11:59:19 +0300 Subject: [PATCH 1/5] G2: Effect Gallery dialog to browse and apply raster filter stacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Effect → Effect Gallery with category browsing, procedural thumbnails, and a reorderable stack applied via command.batch as separate appearance effects. Co-authored-by: Cursor --- crates/ui-egui/src/dialogs/effect_gallery.rs | 330 +++++++++++++++++++ crates/ui-egui/src/dialogs/mod.rs | 2 + crates/ui-egui/src/menus.rs | 10 +- 3 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/src/dialogs/effect_gallery.rs diff --git a/crates/ui-egui/src/dialogs/effect_gallery.rs b/crates/ui-egui/src/dialogs/effect_gallery.rs new file mode 100644 index 000000000..81a379b1c --- /dev/null +++ b/crates/ui-egui/src/dialogs/effect_gallery.rs @@ -0,0 +1,330 @@ +//! Effect → Effect Gallery: browse Photoshop-style raster filters by category, build a stack, and +//! apply it as separate appearance effects in one undo step. + +use egui::{Color32, Rect, Sense, Stroke}; +use serde_json::{Value, json}; +use vectorcraft_effects::{EffectInfo, PIXEL_EFFECTS}; + +use super::DialogSpec; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; + +/// Dialog kind for the Effect Gallery. +pub const KIND: &str = "effectGallery"; + +/// Raster effect submenus shown in the gallery (same paths as the Effect menu). +const CATEGORIES: [(&str, &[&str]); 10] = [ + ("Artistic", &["Effect", "Artistic"]), + ("Blur", &["Effect", "Blur"]), + ("Brush Strokes", &["Effect", "Brush Strokes"]), + ("Distort", &["Effect", "Distort"]), + ("Pixelate", &["Effect", "Pixelate"]), + ("Sharpen", &["Effect", "Sharpen"]), + ("Sketch", &["Effect", "Sketch"]), + ("Stylize", &["Effect", "Stylize"]), + ("Texture", &["Effect", "Texture"]), + ("Video", &["Effect", "Video"]), +]; + +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Effect Gallery").into(), + body, + confirm, + min_width: 720.0, + max_width: Some(820.0), + ..DialogSpec::FORM +}; + +#[derive(Clone)] +struct StackEntry { + effect: String, + visible: bool, +} + +/// Open the gallery on the current selection. +pub fn open(app: &mut VectorcraftApp, _p: &Value) -> Result { + if !app.session.active().is_some_and(|s| !s.selection.is_empty()) { + return Err(tl!("Select artwork to apply effects").into()); + } + let selected = effects_in_category(0).first().map(|e| e.id).unwrap_or(""); + app.ui.dialog = Some(Dialog::new(KIND, json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0 }))); + Ok(Value::Null) +} + +fn effects_in_category(cat: usize) -> Vec { + let path = CATEGORIES.get(cat).map(|(_, p)| *p).unwrap_or(CATEGORIES[0].1); + let mut out: Vec = vectorcraft_effects::effect_catalog() + .into_iter() + .filter(|e| e.menu == path && PIXEL_EFFECTS.contains(&e.id)) + .collect(); + out.sort_by(|a, b| a.label.cmp(b.label)); + out +} + +fn stack_of(d: &Dialog) -> Vec { + d.fields + .get("stack") + .and_then(Value::as_array) + .map(|rows| { + rows.iter() + .filter_map(|row| { + let effect = row.get("effect").and_then(Value::as_str)?; + let visible = row.get("visible").and_then(Value::as_bool).unwrap_or(true); + Some(StackEntry { effect: effect.to_string(), visible }) + }) + .collect() + }) + .unwrap_or_default() +} + +fn write_stack(d: &mut Dialog, stack: &[StackEntry]) { + let rows: Vec = stack.iter().map(|e| json!({ "effect": e.effect, "visible": e.visible })).collect(); + d.fields.insert("stack".into(), Value::Array(rows)); +} + +fn category_ix(d: &Dialog) -> usize { + d.fields.get("category").and_then(Value::as_u64).unwrap_or(0) as usize +} + +fn selected_id(d: &Dialog) -> String { + d.str("selected") +} + +fn set_selected(d: &mut Dialog, id: &str) { + d.fields.insert("selected".into(), json!(id)); +} + +/// A small procedural thumbnail from the effect id (no copied artwork). +fn thumb(ui: &mut egui::Ui, id: &str, size: f32) { + let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); + let hash = id.bytes().fold(0u32, |h, b| h.wrapping_mul(31).wrapping_add(b as u32)); + let c1 = Color32::from_rgb((hash & 0xFF) as u8 | 40, ((hash >> 8) & 0xFF) as u8 | 40, ((hash >> 16) & 0xFF) as u8 | 40); + let c2 = Color32::from_rgb(((hash >> 4) & 0xFF) as u8 | 20, ((hash >> 12) & 0xFF) as u8 | 20, ((hash >> 20) & 0xFF) as u8 | 20); + let painter = ui.painter(); + let steps = 6; + for i in 0..steps { + let t0 = i as f32 / steps as f32; + let t1 = (i + 1) as f32 / steps as f32; + let mix = |t: f32| { + let u = t; + Color32::from_rgba_unmultiplied( + (c1.r() as f32 * (1.0 - u) + c2.r() as f32 * u) as u8, + (c1.g() as f32 * (1.0 - u) + c2.g() as f32 * u) as u8, + (c1.b() as f32 * (1.0 - u) + c2.b() as f32 * u) as u8, + 255, + ) + }; + let band = Rect::from_min_max( + egui::pos2(rect.left() + rect.width() * t0, rect.top()), + egui::pos2(rect.left() + rect.width() * t1, rect.bottom()), + ); + painter.rect_filled(band, 0.0, mix((t0 + t1) * 0.5)); + } + painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Tokens::get(ui.ctx()).input_border), egui::StrokeKind::Inside); +} + +fn effect_label(id: &str) -> String { + vectorcraft_effects::effect_info(id).map(|e| tl!(e.label).trim_end_matches('…').to_string()).unwrap_or_else(|| id.to_string()) +} + +fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let t = Tokens::get(ui.ctx()); + let cat = category_ix(d); + let effects = effects_in_category(cat); + let mut sel = selected_id(d); + if !effects.iter().any(|e| e.id == sel) { + sel = effects.first().map(|e| e.id.to_string()).unwrap_or_default(); + set_selected(d, &sel); + } + + ui.horizontal_top(|ui| { + ui.vertical(|ui| { + widgets::subheader(ui, tl!("Categories")); + ui.set_width(120.0); + for (i, (name, _)) in CATEGORIES.iter().enumerate() { + let count = effects_in_category(i).len(); + if count == 0 { + continue; + } + let label = tl!(name); + if ui.add(egui::Button::selectable(cat == i, label)).clicked() { + d.fields.insert("category".into(), json!(i)); + let first = effects_in_category(i).first().map(|e| e.id).unwrap_or(""); + set_selected(d, first); + } + } + }); + ui.add_space(8.0); + ui.vertical(|ui| { + widgets::subheader(ui, tl!("Filters")); + ui.set_width(220.0); + egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { + for e in &effects { + let picked = e.id == sel; + ui.horizontal(|ui| { + thumb(ui, e.id, 28.0); + let name = tl!(e.label).trim_end_matches('…').to_string(); + let resp = ui.add(egui::Button::selectable(picked, name)); + if resp.clicked() { + set_selected(d, e.id); + } + if resp.double_clicked() { + set_selected(d, e.id); + add_selected(d, e.id); + } + }); + } + }); + if widgets::flat_button(ui, tl!("Add to Stack"), 120.0).clicked() && !sel.is_empty() { + add_selected(d, &sel); + } + }); + ui.add_space(8.0); + ui.vertical(|ui| { + widgets::subheader(ui, tl!("Preview")); + ui.set_width(200.0); + if !sel.is_empty() { + thumb(ui, &sel, 160.0); + ui.label(egui::RichText::new(effect_label(&sel)).color(t.text_strong)); + } else { + ui.label(egui::RichText::new(tl!("Choose a filter")).color(t.text_dim)); + } + }); + }); + + ui.add_space(10.0); + widgets::subheader(ui, tl!("Applied Filters")); + let mut stack = stack_of(d); + let mut stack_sel = d.fields.get("stackSelected").and_then(Value::as_u64).unwrap_or(0) as usize; + if stack_sel >= stack.len() && !stack.is_empty() { + stack_sel = stack.len() - 1; + } + enum StackOp { + Toggle(usize), + Up(usize), + Down(usize), + Remove(usize), + Select(usize), + } + let mut op = None; + let stack_len = stack.len(); + egui::ScrollArea::vertical().max_height(120.0).show(ui, |ui| { + if stack.is_empty() { + ui.label(egui::RichText::new(tl!("Add filters from the list above")).color(t.text_dim)); + return; + } + for (i, entry) in stack.iter().enumerate().take(stack_len) { + ui.horizontal(|ui| { + let eye = if entry.visible { "eye" } else { "eye-off" }; + if widgets::icon_button(ui, eye, tl!("Click to toggle visibility"), false, 22.0).clicked() { + op = Some(StackOp::Toggle(i)); + } + let name = effect_label(&entry.effect); + if ui.add(egui::Button::selectable(stack_sel == i, name)).clicked() { + op = Some(StackOp::Select(i)); + } + if widgets::icon_button_enabled(ui, "dc-arrow-up", tl!("Move Up"), false, i > 0, 22.0).clicked() { + op = Some(StackOp::Up(i)); + } + if widgets::icon_button_enabled(ui, "dc-arrow-down", tl!("Move Down"), false, i + 1 < stack_len, 22.0).clicked() { + op = Some(StackOp::Down(i)); + } + if widgets::flat_button(ui, tl!("Remove"), 64.0).clicked() { + op = Some(StackOp::Remove(i)); + } + }); + } + }); + match op { + Some(StackOp::Toggle(i)) if stack.get(i).is_some() => stack[i].visible = !stack[i].visible, + Some(StackOp::Select(i)) => stack_sel = i, + Some(StackOp::Up(i)) if i > 0 => { + stack.swap(i, i - 1); + stack_sel = i - 1; + } + Some(StackOp::Down(i)) if i + 1 < stack.len() => { + stack.swap(i, i + 1); + stack_sel = i + 1; + } + Some(StackOp::Remove(i)) if i < stack.len() => { + stack.remove(i); + if stack_sel >= stack.len() && !stack.is_empty() { + stack_sel = stack.len() - 1; + } + } + _ => {} + } + write_stack(d, &stack); + d.fields.insert("stackSelected".into(), json!(stack_sel)); + false +} + +fn add_selected(d: &mut Dialog, id: &str) { + let mut stack = stack_of(d); + stack.push(StackEntry { effect: id.to_string(), visible: true }); + write_stack(d, &stack); + d.fields.insert("stackSelected".into(), json!(stack.len().saturating_sub(1))); +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let stack = stack_of(d); + if stack.is_empty() { + app.ui.dialog = None; + return Ok(Value::Null); + } + let mut commands = Vec::with_capacity(stack.len() * 2); + for (i, entry) in stack.iter().enumerate() { + let params = vectorcraft_effects::effect_info(&entry.effect).map(|e| e.defaults.clone()).unwrap_or(json!({})); + commands.push(json!({ "command": "effect.apply", "params": { "effect": entry.effect, "params": params } })); + if !entry.visible { + commands.push(json!({ "command": "effect.setParams", "params": { "index": i, "visible": false } })); + } + } + let r = app.run("command.batch", json!({ "label": "Effect Gallery", "commands": commands })); + if r.is_ok() { + app.ui.dialog = None; + } + r +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use vectorcraft_engine::Session; + + use crate::VectorcraftApp; + + fn frame(app: &mut VectorcraftApp) { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + crate::theme::apply(&ctx, Default::default()); + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 900.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| crate::dialogs::show(app, ui.ctx())); + out.textures_delta.clear(); + } + + #[test] + fn stack_ok_is_one_undo_step() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap(); + app.run("ui.effectGalleryDialog", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(super::KIND)); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert( + "stack".into(), + json!([{ "effect": "blur.gaussian", "visible": true }, { "effect": "stylize.glowingEdges", "visible": false }]), + ); + frame(&mut app); + let steps = app.session.active().unwrap().history.undo.len(); + crate::dialogs::confirm(&mut app).unwrap(); + let st = app.session.active().unwrap(); + let n = st.doc.node(vectorcraft_doc::NodeId(id)).unwrap(); + assert_eq!(n.appearance.effects.len(), 2); + assert_eq!(n.appearance.effects[0].id, "blur.gaussian"); + assert_eq!(n.appearance.effects[1].id, "stylize.glowingEdges"); + assert!(!n.appearance.effects[1].visible); + assert_eq!(st.history.undo.len(), steps + 1); + } +} diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index f02f75f48..ac2c422fb 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -24,6 +24,7 @@ pub mod dxf_import; pub mod dxf_options; pub mod edit_selection; mod effect; +pub mod effect_gallery; pub mod envelope; pub mod eps_options; pub mod expand; @@ -301,6 +302,7 @@ registry! { SwatchConflict: [swatch_conflict::KIND] => swatch_conflict::SPEC, FileInfo: [file_info::KIND] => file_info::SPEC, RasterEffectsSettings: [raster_effects::KIND] => raster_effects::SPEC, + EffectGallery: [effect_gallery::KIND] => effect_gallery::SPEC, MissingLinks: [missing_links::KIND] => missing_links::SPEC, MissingFonts: [missing_fonts::KIND] => missing_fonts::SPEC, FindLinks: [find_links::KIND] => find_links::SPEC, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 76f0b3176..16d1fd808 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -857,6 +857,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{} open Document Raster Effects Settings (dialog `rasterEffectsSettings`: the document.rasterEffectsSettings fields); OK runs it. The menu item opens it too", ), + ( + "ui.effectGalleryDialog", + "Effect Gallery Dialog", + "", + "{} open Effect Gallery (dialog `effectGallery`: browse raster filters, build a stack, OK applies it as separate effects in one undo step). Effect → Effect Gallery… opens it too", + ), ( "ui.pdfPresetsDialog", "PDF Presets…", @@ -1770,6 +1776,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option io::export(app, s("format").as_deref(), s("path"), p), "ui.fileInfoDialog" => crate::dialogs::file_info::open(app), "ui.rasterEffectsSettingsDialog" => crate::dialogs::raster_effects::open(app), + "ui.effectGalleryDialog" => crate::dialogs::effect_gallery::open(app, p), "ui.pdfPresetsDialog" => { crate::dialogs::pdf_presets::open(app, s("selected").as_deref()); Ok(Value::Null) @@ -2436,7 +2443,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { id if id.starts_with("view.goto") => { id["view.goto".len()..].parse::().is_ok_and(|n| n >= 1 && app.session.active().is_some_and(|d| n <= d.doc.views.len())) } - "effect.dialog" | "ui.recolorDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()), + "effect.dialog" | "ui.recolorDialog" | "ui.effectGalleryDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()), "ui.cropImage" => selected_image(app, |_| true), "effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()), "file.export.pdf" | "ui.savePdfDialog" | "ui.fileInfoDialog" | "ui.rasterEffectsSettingsDialog" => app.session.active().is_some(), @@ -4055,6 +4062,7 @@ fn effect_menu() -> Vec { if !raster.is_empty() { out.push(Sep); out.push(Item::Header("Raster Effects")); + out.push(c("Effect Gallery…", "ui.effectGalleryDialog")); out.extend(raster); } // Anything not placed above (future effects) still shows up. From dd9c285252699db7f013f506f775dc972698b97d Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Sun, 18 Oct 2026 16:33:42 +0300 Subject: [PATCH 2/5] G2: Effect Gallery six categories and per-filter settings Limit gallery folders to the six Photoshop-style sets and edit stack params via the existing effect dialog body before apply. Co-authored-by: Cursor --- crates/ui-egui/src/dialogs/effect_gallery.rs | 173 ++++++++++++++++--- 1 file changed, 152 insertions(+), 21 deletions(-) diff --git a/crates/ui-egui/src/dialogs/effect_gallery.rs b/crates/ui-egui/src/dialogs/effect_gallery.rs index 81a379b1c..25a12d706 100644 --- a/crates/ui-egui/src/dialogs/effect_gallery.rs +++ b/crates/ui-egui/src/dialogs/effect_gallery.rs @@ -2,10 +2,10 @@ //! apply it as separate appearance effects in one undo step. use egui::{Color32, Rect, Sense, Stroke}; -use serde_json::{Value, json}; +use serde_json::{Map, Value, json}; use vectorcraft_effects::{EffectInfo, PIXEL_EFFECTS}; -use super::DialogSpec; +use super::{DialogSpec, effect, form}; use crate::state::Dialog; use crate::theme::Tokens; use crate::{VectorcraftApp, widgets}; @@ -13,18 +13,14 @@ use crate::{VectorcraftApp, widgets}; /// Dialog kind for the Effect Gallery. pub const KIND: &str = "effectGallery"; -/// Raster effect submenus shown in the gallery (same paths as the Effect menu). -const CATEGORIES: [(&str, &[&str]); 10] = [ +/// Raster effect submenus in the gallery (the six Photoshop-style folders it holds). +const CATEGORIES: [(&str, &[&str]); 6] = [ ("Artistic", &["Effect", "Artistic"]), - ("Blur", &["Effect", "Blur"]), ("Brush Strokes", &["Effect", "Brush Strokes"]), ("Distort", &["Effect", "Distort"]), - ("Pixelate", &["Effect", "Pixelate"]), - ("Sharpen", &["Effect", "Sharpen"]), ("Sketch", &["Effect", "Sketch"]), ("Stylize", &["Effect", "Stylize"]), ("Texture", &["Effect", "Texture"]), - ("Video", &["Effect", "Video"]), ]; pub(super) const SPEC: DialogSpec = DialogSpec { @@ -40,6 +36,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { struct StackEntry { effect: String, visible: bool, + params: Map, } /// Open the gallery on the current selection. @@ -48,7 +45,10 @@ pub fn open(app: &mut VectorcraftApp, _p: &Value) -> Result { return Err(tl!("Select artwork to apply effects").into()); } let selected = effects_in_category(0).first().map(|e| e.id).unwrap_or(""); - app.ui.dialog = Some(Dialog::new(KIND, json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0 }))); + app.ui.dialog = Some(Dialog::new( + KIND, + json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false }), + )); Ok(Value::Null) } @@ -62,6 +62,20 @@ fn effects_in_category(cat: usize) -> Vec { out } +fn default_params(id: &str) -> Map { + match vectorcraft_effects::effect_info(id).map(|e| e.defaults) { + Some(Value::Object(m)) => m, + _ => Map::new(), + } +} + +fn merged_params(id: &str, stored: &Map) -> Map { + match vectorcraft_effects::merged_params(id, &Value::Object(stored.clone())) { + Value::Object(m) => m, + _ => default_params(id), + } +} + fn stack_of(d: &Dialog) -> Vec { d.fields .get("stack") @@ -71,7 +85,13 @@ fn stack_of(d: &Dialog) -> Vec { .filter_map(|row| { let effect = row.get("effect").and_then(Value::as_str)?; let visible = row.get("visible").and_then(Value::as_bool).unwrap_or(true); - Some(StackEntry { effect: effect.to_string(), visible }) + let params = row + .get("params") + .and_then(Value::as_object) + .cloned() + .map(|m| merged_params(effect, &m)) + .unwrap_or_else(|| default_params(effect)); + Some(StackEntry { effect: effect.to_string(), visible, params }) }) .collect() }) @@ -79,7 +99,10 @@ fn stack_of(d: &Dialog) -> Vec { } fn write_stack(d: &mut Dialog, stack: &[StackEntry]) { - let rows: Vec = stack.iter().map(|e| json!({ "effect": e.effect, "visible": e.visible })).collect(); + let rows: Vec = stack + .iter() + .map(|e| json!({ "effect": e.effect, "visible": e.visible, "params": e.params })) + .collect(); d.fields.insert("stack".into(), Value::Array(rows)); } @@ -95,6 +118,82 @@ fn set_selected(d: &mut Dialog, id: &str) { d.fields.insert("selected".into(), json!(id)); } +fn stack_focus(d: &Dialog) -> bool { + d.fields.get("stackFocus").and_then(Value::as_bool).unwrap_or(false) +} + +fn set_stack_focus(d: &mut Dialog, on: bool) { + d.fields.insert("stackFocus".into(), json!(on)); +} + +fn browse_params(d: &Dialog) -> Map { + let effect = d.fields.get("browseEffect").and_then(Value::as_str).unwrap_or(""); + let stored = d.fields.get("browseParams").and_then(Value::as_object).cloned().unwrap_or_default(); + if effect.is_empty() { + stored + } else { + merged_params(effect, &stored) + } +} + +fn set_browse_params(d: &mut Dialog, effect: &str, params: Map) { + d.fields.insert("browseEffect".into(), json!(effect)); + d.fields.insert("browseParams".into(), Value::Object(params)); +} + +/// The effect whose parameters the panel edits, and its values. +fn param_target(d: &Dialog, stack: &[StackEntry], stack_sel: usize, browse: &str) -> (String, Map) { + if stack_focus(d) { + let entry = stack.get(stack_sel); + if let Some(e) = entry { + return (e.effect.clone(), e.params.clone()); + } + } + let id = browse.to_string(); + let params = if d.fields.get("browseEffect").and_then(Value::as_str) == Some(browse) { + browse_params(d) + } else { + default_params(browse) + }; + (id, params) +} + +fn write_param_target(d: &mut Dialog, stack: &mut [StackEntry], stack_sel: usize, browse: &str, effect: &str, params: Map) { + if stack_focus(d) { + if let Some(e) = stack.get_mut(stack_sel) { + if e.effect == effect { + e.params = params; + } + } + return; + } + if browse == effect { + set_browse_params(d, effect, params); + } +} + +/// Reuse the live effect dialog's fields for `effect_id` and `params`. +fn effect_param_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, effect_id: &str, params: &mut Map) { + if effect_id.is_empty() { + return; + } + let label = vectorcraft_effects::effect_info(effect_id) + .map(|e| e.label.trim_end_matches('…').to_string()) + .unwrap_or_else(|| effect_id.to_string()); + let mut fields = params.clone(); + fields.insert("__effect".into(), json!(effect_id)); + fields.insert("__label".into(), json!(label)); + fields.insert("preview".into(), json!(false)); + let mut temp = Dialog::new("effect", Value::Object(fields)); + (effect::SPEC.body)(app, ui, &mut temp); + if let Value::Object(m) = form::params(&temp) { + *params = m; + } + if let Value::Object(m) = vectorcraft_effects::upgrade_params(effect_id, &Value::Object(params.clone())) { + *params = m; + } +} + /// A small procedural thumbnail from the effect id (no copied artwork). fn thumb(ui: &mut egui::Ui, id: &str, size: f32) { let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); @@ -128,7 +227,7 @@ fn effect_label(id: &str) -> String { vectorcraft_effects::effect_info(id).map(|e| tl!(e.label).trim_end_matches('…').to_string()).unwrap_or_else(|| id.to_string()) } -fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let cat = category_ix(d); let effects = effects_in_category(cat); @@ -138,6 +237,20 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { set_selected(d, &sel); } + let mut stack = stack_of(d); + let mut stack_sel = d.fields.get("stackSelected").and_then(Value::as_u64).unwrap_or(0) as usize; + if stack_sel >= stack.len() && !stack.is_empty() { + stack_sel = stack.len() - 1; + } + let (param_effect, mut param_values) = param_target(d, &stack, stack_sel, &sel); + if !param_effect.is_empty() { + widgets::subheader(ui, tl!("Settings")); + egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| { + effect_param_fields(app, ui, ¶m_effect, &mut param_values); + }); + write_param_target(d, &mut stack, stack_sel, &sel, ¶m_effect, param_values); + } + ui.horizontal_top(|ui| { ui.vertical(|ui| { widgets::subheader(ui, tl!("Categories")); @@ -152,6 +265,7 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { d.fields.insert("category".into(), json!(i)); let first = effects_in_category(i).first().map(|e| e.id).unwrap_or(""); set_selected(d, first); + set_stack_focus(d, false); } } }); @@ -168,25 +282,30 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let resp = ui.add(egui::Button::selectable(picked, name)); if resp.clicked() { set_selected(d, e.id); + set_stack_focus(d, false); } if resp.double_clicked() { set_selected(d, e.id); + set_stack_focus(d, false); add_selected(d, e.id); + stack = stack_of(d); } }); } }); if widgets::flat_button(ui, tl!("Add to Stack"), 120.0).clicked() && !sel.is_empty() { add_selected(d, &sel); + stack = stack_of(d); } }); ui.add_space(8.0); ui.vertical(|ui| { widgets::subheader(ui, tl!("Preview")); ui.set_width(200.0); - if !sel.is_empty() { - thumb(ui, &sel, 160.0); - ui.label(egui::RichText::new(effect_label(&sel)).color(t.text_strong)); + let (param_effect, _) = param_target(d, &stack, stack_sel, &sel); + if !param_effect.is_empty() { + thumb(ui, ¶m_effect, 160.0); + ui.label(egui::RichText::new(effect_label(¶m_effect)).color(t.text_strong)); } else { ui.label(egui::RichText::new(tl!("Choose a filter")).color(t.text_dim)); } @@ -195,7 +314,6 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(10.0); widgets::subheader(ui, tl!("Applied Filters")); - let mut stack = stack_of(d); let mut stack_sel = d.fields.get("stackSelected").and_then(Value::as_u64).unwrap_or(0) as usize; if stack_sel >= stack.len() && !stack.is_empty() { stack_sel = stack.len() - 1; @@ -238,7 +356,10 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); match op { Some(StackOp::Toggle(i)) if stack.get(i).is_some() => stack[i].visible = !stack[i].visible, - Some(StackOp::Select(i)) => stack_sel = i, + Some(StackOp::Select(i)) => { + stack_sel = i; + set_stack_focus(d, true); + } Some(StackOp::Up(i)) if i > 0 => { stack.swap(i, i - 1); stack_sel = i - 1; @@ -261,10 +382,16 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } fn add_selected(d: &mut Dialog, id: &str) { + let params = if !stack_focus(d) && d.fields.get("browseEffect").and_then(Value::as_str) == Some(id) { + browse_params(d) + } else { + default_params(id) + }; let mut stack = stack_of(d); - stack.push(StackEntry { effect: id.to_string(), visible: true }); + stack.push(StackEntry { effect: id.to_string(), visible: true, params }); write_stack(d, &stack); d.fields.insert("stackSelected".into(), json!(stack.len().saturating_sub(1))); + set_stack_focus(d, true); } fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { @@ -275,7 +402,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { } let mut commands = Vec::with_capacity(stack.len() * 2); for (i, entry) in stack.iter().enumerate() { - let params = vectorcraft_effects::effect_info(&entry.effect).map(|e| e.defaults.clone()).unwrap_or(json!({})); + let params = Value::Object(entry.params.clone()); commands.push(json!({ "command": "effect.apply", "params": { "effect": entry.effect, "params": params } })); if !entry.visible { commands.push(json!({ "command": "effect.setParams", "params": { "index": i, "visible": false } })); @@ -314,7 +441,10 @@ mod tests { let d = app.ui.dialog.as_mut().unwrap(); d.fields.insert( "stack".into(), - json!([{ "effect": "blur.gaussian", "visible": true }, { "effect": "stylize.glowingEdges", "visible": false }]), + json!([ + { "effect": "artistic.cutout", "visible": true, "params": { "levels": 3 } }, + { "effect": "stylize.glowingEdges", "visible": false, "params": { "edgeWidth": 2 } } + ]), ); frame(&mut app); let steps = app.session.active().unwrap().history.undo.len(); @@ -322,7 +452,8 @@ mod tests { let st = app.session.active().unwrap(); let n = st.doc.node(vectorcraft_doc::NodeId(id)).unwrap(); assert_eq!(n.appearance.effects.len(), 2); - assert_eq!(n.appearance.effects[0].id, "blur.gaussian"); + assert_eq!(n.appearance.effects[0].id, "artistic.cutout"); + assert_eq!(n.appearance.effects[0].params.get("levels").and_then(|v| v.as_i64()), Some(3)); assert_eq!(n.appearance.effects[1].id, "stylize.glowingEdges"); assert!(!n.appearance.effects[1].visible); assert_eq!(st.history.undo.len(), steps + 1); From e72735bcf9b46ac9afbf2ab6d7fd4c883b567c8a Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Tue, 20 Oct 2026 21:40:51 +0300 Subject: [PATCH 3/5] G2: satisfy clippy and rustfmt on Effect Gallery dialog. Co-authored-by: Cursor --- crates/ui-egui/src/dialogs/effect_gallery.rs | 64 ++++++-------------- 1 file changed, 18 insertions(+), 46 deletions(-) diff --git a/crates/ui-egui/src/dialogs/effect_gallery.rs b/crates/ui-egui/src/dialogs/effect_gallery.rs index 25a12d706..263d6a54f 100644 --- a/crates/ui-egui/src/dialogs/effect_gallery.rs +++ b/crates/ui-egui/src/dialogs/effect_gallery.rs @@ -23,14 +23,8 @@ const CATEGORIES: [(&str, &[&str]); 6] = [ ("Texture", &["Effect", "Texture"]), ]; -pub(super) const SPEC: DialogSpec = DialogSpec { - heading: |_| tl!("Effect Gallery").into(), - body, - confirm, - min_width: 720.0, - max_width: Some(820.0), - ..DialogSpec::FORM -}; +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Effect Gallery").into(), body, confirm, min_width: 720.0, max_width: Some(820.0), ..DialogSpec::FORM }; #[derive(Clone)] struct StackEntry { @@ -45,19 +39,14 @@ pub fn open(app: &mut VectorcraftApp, _p: &Value) -> Result { return Err(tl!("Select artwork to apply effects").into()); } let selected = effects_in_category(0).first().map(|e| e.id).unwrap_or(""); - app.ui.dialog = Some(Dialog::new( - KIND, - json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false }), - )); + app.ui.dialog = Some(Dialog::new(KIND, json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false }))); Ok(Value::Null) } fn effects_in_category(cat: usize) -> Vec { let path = CATEGORIES.get(cat).map(|(_, p)| *p).unwrap_or(CATEGORIES[0].1); - let mut out: Vec = vectorcraft_effects::effect_catalog() - .into_iter() - .filter(|e| e.menu == path && PIXEL_EFFECTS.contains(&e.id)) - .collect(); + let mut out: Vec = + vectorcraft_effects::effect_catalog().into_iter().filter(|e| e.menu == path && PIXEL_EFFECTS.contains(&e.id)).collect(); out.sort_by(|a, b| a.label.cmp(b.label)); out } @@ -99,10 +88,7 @@ fn stack_of(d: &Dialog) -> Vec { } fn write_stack(d: &mut Dialog, stack: &[StackEntry]) { - let rows: Vec = stack - .iter() - .map(|e| json!({ "effect": e.effect, "visible": e.visible, "params": e.params })) - .collect(); + let rows: Vec = stack.iter().map(|e| json!({ "effect": e.effect, "visible": e.visible, "params": e.params })).collect(); d.fields.insert("stack".into(), Value::Array(rows)); } @@ -129,11 +115,7 @@ fn set_stack_focus(d: &mut Dialog, on: bool) { fn browse_params(d: &Dialog) -> Map { let effect = d.fields.get("browseEffect").and_then(Value::as_str).unwrap_or(""); let stored = d.fields.get("browseParams").and_then(Value::as_object).cloned().unwrap_or_default(); - if effect.is_empty() { - stored - } else { - merged_params(effect, &stored) - } + if effect.is_empty() { stored } else { merged_params(effect, &stored) } } fn set_browse_params(d: &mut Dialog, effect: &str, params: Map) { @@ -150,20 +132,16 @@ fn param_target(d: &Dialog, stack: &[StackEntry], stack_sel: usize, browse: &str } } let id = browse.to_string(); - let params = if d.fields.get("browseEffect").and_then(Value::as_str) == Some(browse) { - browse_params(d) - } else { - default_params(browse) - }; + let params = if d.fields.get("browseEffect").and_then(Value::as_str) == Some(browse) { browse_params(d) } else { default_params(browse) }; (id, params) } fn write_param_target(d: &mut Dialog, stack: &mut [StackEntry], stack_sel: usize, browse: &str, effect: &str, params: Map) { if stack_focus(d) { - if let Some(e) = stack.get_mut(stack_sel) { - if e.effect == effect { - e.params = params; - } + if let Some(e) = stack.get_mut(stack_sel) + && e.effect == effect + { + e.params = params; } return; } @@ -177,9 +155,8 @@ fn effect_param_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, effect_id: & if effect_id.is_empty() { return; } - let label = vectorcraft_effects::effect_info(effect_id) - .map(|e| e.label.trim_end_matches('…').to_string()) - .unwrap_or_else(|| effect_id.to_string()); + let label = + vectorcraft_effects::effect_info(effect_id).map(|e| e.label.trim_end_matches('…').to_string()).unwrap_or_else(|| effect_id.to_string()); let mut fields = params.clone(); fields.insert("__effect".into(), json!(effect_id)); fields.insert("__label".into(), json!(label)); @@ -214,10 +191,8 @@ fn thumb(ui: &mut egui::Ui, id: &str, size: f32) { 255, ) }; - let band = Rect::from_min_max( - egui::pos2(rect.left() + rect.width() * t0, rect.top()), - egui::pos2(rect.left() + rect.width() * t1, rect.bottom()), - ); + let band = + Rect::from_min_max(egui::pos2(rect.left() + rect.width() * t0, rect.top()), egui::pos2(rect.left() + rect.width() * t1, rect.bottom())); painter.rect_filled(band, 0.0, mix((t0 + t1) * 0.5)); } painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Tokens::get(ui.ctx()).input_border), egui::StrokeKind::Inside); @@ -382,11 +357,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } fn add_selected(d: &mut Dialog, id: &str) { - let params = if !stack_focus(d) && d.fields.get("browseEffect").and_then(Value::as_str) == Some(id) { - browse_params(d) - } else { - default_params(id) - }; + let params = + if !stack_focus(d) && d.fields.get("browseEffect").and_then(Value::as_str) == Some(id) { browse_params(d) } else { default_params(id) }; let mut stack = stack_of(d); stack.push(StackEntry { effect: id.to_string(), visible: true, params }); write_stack(d, &stack); From d275fbf725d408a8a1ac00c74431084c1a6b2c9e Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Wed, 21 Oct 2026 00:45:21 +0300 Subject: [PATCH 4/5] G2: catalog Effect Gallery dialog tl! strings for all locales. Co-authored-by: Cursor --- crates/ui-egui/src/i18n/bg.tsv | 8 ++++++++ crates/ui-egui/src/i18n/cs.tsv | 8 ++++++++ crates/ui-egui/src/i18n/da.tsv | 8 ++++++++ crates/ui-egui/src/i18n/de.tsv | 8 ++++++++ crates/ui-egui/src/i18n/el.tsv | 8 ++++++++ crates/ui-egui/src/i18n/es.tsv | 8 ++++++++ crates/ui-egui/src/i18n/fi.tsv | 8 ++++++++ crates/ui-egui/src/i18n/fr.tsv | 8 ++++++++ crates/ui-egui/src/i18n/hu.tsv | 8 ++++++++ crates/ui-egui/src/i18n/id.tsv | 8 ++++++++ crates/ui-egui/src/i18n/it.tsv | 8 ++++++++ crates/ui-egui/src/i18n/ja.tsv | 8 ++++++++ crates/ui-egui/src/i18n/ko.tsv | 8 ++++++++ crates/ui-egui/src/i18n/nl.tsv | 8 ++++++++ crates/ui-egui/src/i18n/pl.tsv | 8 ++++++++ crates/ui-egui/src/i18n/pt-br.tsv | 8 ++++++++ crates/ui-egui/src/i18n/ru.tsv | 8 ++++++++ crates/ui-egui/src/i18n/sv.tsv | 8 ++++++++ crates/ui-egui/src/i18n/uk.tsv | 8 ++++++++ crates/ui-egui/src/i18n/vi.tsv | 8 ++++++++ crates/ui-egui/src/i18n/zh-hans.tsv | 8 ++++++++ crates/ui-egui/src/i18n/zh-hant.tsv | 8 ++++++++ 22 files changed, 176 insertions(+) diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a44438be7..f8ab8ca23 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5310,6 +5310,14 @@ theme Appearance Външен вид Full Justify Пълно двустранно Minimum must not exceed Desired, nor Desired Maximum. Минималната стойност не може да надвишава желаната, нито желаната – максималната. Justification Подравняване + Add filters from the list above Добавете филтри от списъка по-горе + Add to Stack Добавяне към стека + Applied Filters Приложени филтри + Categories Категории + Choose a filter Изберете филтър + Effect Gallery Галерия с ефекти + Filters Филтри + Select artwork to apply effects Изберете графика, за да приложите ефекти @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them променливите и наборите от данни на файла не бяха прочетени (променливи: {variables}, набори от данни: {data_sets}): панелът „Променливи“ не ги показва # Select by Color (select.color) Select by Color Избиране по цвят diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 0bed38936..0124a1f20 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5353,6 +5353,14 @@ mark-style Japanese Japonské Full Justify Zarovnat do bloku Minimum must not exceed Desired, nor Desired Maximum. Minimum nesmí překročit požadovanou hodnotu ani požadovaná hodnota maximum. Justification Zarovnání do bloku + Add filters from the list above Přidejte filtry ze seznamu výše + Add to Stack Přidat na zásobník + Applied Filters Použité filtry + Categories Kategorie + Choose a filter Vyberte filtr + Effect Gallery Galerie efektů + Filters Filtry + Select artwork to apply effects Vyberte grafiku, na kterou chcete použít efekty @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them proměnné a sady dat souboru nebyly načteny (proměnné: {variables}, sady dat: {data_sets}): panel Proměnné je nezobrazuje # Select by Color (select.color) Select by Color Vybrat podle barvy diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e5c9c6a74..047f7b073 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5306,6 +5306,14 @@ swatch Bright Violet Kraftig violet Full Justify Lige margener Minimum must not exceed Desired, nor Desired Maximum. Minimum må ikke overstige Ønsket, og Ønsket ikke Maksimum. Justification Justering + Add filters from the list above Tilføj filtre fra listen ovenfor + Add to Stack Tilføj til stak + Applied Filters Anvendte filtre + Categories Kategorier + Choose a filter Vælg et filter + Effect Gallery Effektgalleri + Filters Filtre + Select artwork to apply effects Vælg grafik for at anvende effekter # Select by Color (select.color) Select by Color Markér efter farve Smudge Stick… Udtværingsstift… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a356bacb5..8fca8c3e9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4934,6 +4934,14 @@ effect Soft Weich Minimum must not exceed Desired, nor Desired Maximum. Das Minimum darf den optimalen Wert nicht übersteigen, der optimale Wert nicht das Maximum. Minimum Minimum Justification Abstände + Add filters from the list above Filter aus der Liste oben hinzufügen + Add to Stack Zum Stapel hinzufügen + Applied Filters Angewendete Filter + Categories Kategorien + Choose a filter Filter wählen + Effect Gallery Effekt-Galerie + Filters Filter + Select artwork to apply effects Wählen Sie Grafik aus, um Effekte anzuwenden @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them die Variablen und Datensätze der Datei wurden nicht gelesen (Variablen: {variables}, Datensätze: {data_sets}): Das Bedienfeld „Variablen“ listet sie nicht auf # Select by Color (select.color) Select by Color Nach Farbe auswählen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 47459f07c..c97154f1f 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5305,6 +5305,14 @@ effect Points Σημεία Full Justify Στοίχιση και στα δύο άκρα Minimum must not exceed Desired, nor Desired Maximum. Το ελάχιστο δεν πρέπει να υπερβαίνει το επιθυμητό, ούτε το επιθυμητό το μέγιστο. Justification Πλήρης στοίχιση + Add filters from the list above Προσθέστε φίλτρα από τη λίστα παραπάνω + Add to Stack Προσθήκη στη στοίβα + Applied Filters Εφαρμοσμένα φίλτρα + Categories Κατηγορίες + Choose a filter Επιλέξτε φίλτρο + Effect Gallery Συλλογή εφέ + Filters Φίλτρα + Select artwork to apply effects Επιλέξτε γραφικά για να εφαρμόσετε εφέ @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them οι μεταβλητές και τα σύνολα δεδομένων του αρχείου δεν διαβάστηκαν (μεταβλητές: {variables}, σύνολα δεδομένων: {data_sets}): ο πίνακας Μεταβλητές δεν τα εμφανίζει # Select by Color (select.color) Select by Color Επιλογή βάσει χρώματος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index db7a047b7..a5d9eae6d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5295,6 +5295,14 @@ mark-style Japanese Japonesas Full Justify Justificar completamente Minimum must not exceed Desired, nor Desired Maximum. El mínimo no puede superar el deseado, ni el deseado el máximo. Justification Justificación + Add filters from the list above Añada filtros de la lista de arriba + Add to Stack Añadir a la pila + Applied Filters Filtros aplicados + Categories Categorías + Choose a filter Elija un filtro + Effect Gallery Galería de efectos + Filters Filtros + Select artwork to apply effects Seleccione gráficos para aplicar efectos @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them no se leyeron las variables ni los conjuntos de datos del archivo (variables: {variables}, conjuntos de datos: {data_sets}): el panel Variables no los muestra # Select by Color (select.color) Select by Color Seleccionar por color diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index d04facf00..88f14d09b 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5317,6 +5317,14 @@ mark-style Japanese Japanilainen Full Justify Täysi tasaus Minimum must not exceed Desired, nor Desired Maximum. Minimi ei saa ylittää haluttua arvoa eikä haluttu arvo maksimia. Justification Tasaus + Add filters from the list above Lisää suodattimia yllä olevasta luettelosta + Add to Stack Lisää pinoon + Applied Filters Käytetyt suodattimet + Categories Luokat + Choose a filter Valitse suodatin + Effect Gallery Tehostegalleria + Filters Suodattimet + Select artwork to apply effects Valitse grafiikka, johon tehosteet otetaan @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them tiedoston muuttujia ja tietojoukkoja ei luettu (muuttujia: {variables}, tietojoukkoja: {data_sets}): Muuttujat-paneeli ei näytä niitä # Select by Color (select.color) Select by Color Valitse värin mukaan diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9993f344e..69cd9ab56 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5352,6 +5352,14 @@ theme Appearance Apparence Full Justify Justification complète Minimum must not exceed Desired, nor Desired Maximum. Le minimum ne doit pas dépasser la valeur optimale, ni la valeur optimale le maximum. Justification Justification + Add filters from the list above Ajoutez des filtres à partir de la liste ci-dessus + Add to Stack Ajouter à la pile + Applied Filters Filtres appliqués + Categories Catégories + Choose a filter Choisir un filtre + Effect Gallery Galerie d'effets + Filters Filtres + Select artwork to apply effects Sélectionnez des éléments graphiques pour appliquer des effets @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them les variables et jeux de données du fichier n’ont pas été lus (variables : {variables}, jeux de données : {data_sets}) : le panneau Variables ne les affiche pas # Select by Color (select.color) Select by Color Sélectionner par couleur diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f9f65f9fe..992560a4d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5339,6 +5339,14 @@ imageTrace Outline Körvonal Full Justify Teljes sorkizárás Minimum must not exceed Desired, nor Desired Maximum. A minimum nem haladhatja meg a kívánt értéket, sem a kívánt érték a maximumot. Justification Sorkizárás + Add filters from the list above Adjon hozzá szűrőket a fenti listából + Add to Stack Hozzáadás a veremhez + Applied Filters Alkalmazott szűrők + Categories Kategóriák + Choose a filter Válasszon szűrőt + Effect Gallery Effektusgaléria + Filters Szűrők + Select artwork to apply effects Válasszon ki grafikát az effektusok alkalmazásához @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them a fájl változói és adatkészletei nem lettek beolvasva (változók: {variables}, adatkészletek: {data_sets}): a Változók panel nem listázza őket # Select by Color (select.color) Select by Color Kijelölés szín szerint diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 17c6c7ceb..230b914f0 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5306,6 +5306,14 @@ swatch Bright Violet Violet cerah Full Justify Rata Penuh Minimum must not exceed Desired, nor Desired Maximum. Minimum tidak boleh melebihi Diinginkan, dan Diinginkan tidak boleh melebihi Maksimum. Justification Justifikasi + Add filters from the list above Tambahkan filter dari daftar di atas + Add to Stack Tambahkan ke Tumpukan + Applied Filters Filter yang Diterapkan + Categories Kategori + Choose a filter Pilih filter + Effect Gallery Galeri Efek + Filters Filter + Select artwork to apply effects Pilih grafis untuk menerapkan efek # Select by Color (select.color) Select by Color Pilih berdasarkan warna Smudge Stick… Tongkat olesan… diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 7333cb719..14824bafd 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4968,6 +4968,14 @@ lighting Light Luce Minimum must not exceed Desired, nor Desired Maximum. Il minimo non può superare il valore desiderato, né il desiderato il massimo. Minimum Minimo Justification Giustificazione + Add filters from the list above Aggiungi filtri dall'elenco sopra + Add to Stack Aggiungi alla pila + Applied Filters Filtri applicati + Categories Categorie + Choose a filter Scegli un filtro + Effect Gallery Galleria effetti + Filters Filtri + Select artwork to apply effects Seleziona la grafica a cui applicare gli effetti @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them le variabili e i set di dati del file non sono stati letti (variabili: {variables}, set di dati: {data_sets}): il pannello Variabili non li elenca # Select by Color (select.color) Select by Color Seleziona per colore diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e58f4e0d7..21709cca2 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5008,6 +5008,14 @@ lighting Light ライト Minimum must not exceed Desired, nor Desired Maximum. 最小値は最適値を、最適値は最大値を超えることはできません。 Minimum 最小 Justification 文字組み + Add filters from the list above 上のリストからフィルターを追加 + Add to Stack スタックに追加 + Applied Filters 適用したフィルター + Categories カテゴリ + Choose a filter フィルターを選択 + Effect Gallery 効果ギャラリー + Filters フィルター + Select artwork to apply effects 効果を適用するグラフィックを選択してください @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them ファイルの変数とデータセットは読み込まれませんでした (変数: {variables}、データセット: {data_sets})。変数パネルには表示されません # Select by Color (select.color) Select by Color カラーで選択 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index eb4baa015..71b16eb8c 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4931,6 +4931,14 @@ lighting Light 광원 Minimum must not exceed Desired, nor Desired Maximum. 최소값은 희망값을, 희망값은 최대값을 넘을 수 없습니다. Minimum 최소 Justification 양끝 맞춤 + Add filters from the list above 위 목록에서 필터 추가 + Add to Stack 스택에 추가 + Applied Filters 적용된 필터 + Categories 범주 + Choose a filter 필터 선택 + Effect Gallery 효과 갤러리 + Filters 필터 + Select artwork to apply effects 효과를 적용할 그래픽을 선택하세요 @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them 파일의 변수와 데이터 세트를 읽지 않았습니다(변수: {variables}, 데이터 세트: {data_sets}). 변수 패널에 표시되지 않습니다 # Select by Color (select.color) Select by Color 색상별 선택 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 70ffeafc6..452a7d650 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5348,6 +5348,14 @@ swatch Bright Violet Felviolet Full Justify Volledig uitvullen Minimum must not exceed Desired, nor Desired Maximum. Minimum mag niet groter zijn dan Gewenst, en Gewenst niet groter dan Maximum. Justification Uitvullen + Add filters from the list above Voeg filters toe uit de lijst hierboven + Add to Stack Toevoegen aan stapel + Applied Filters Toegepaste filters + Categories Categorieën + Choose a filter Kies een filter + Effect Gallery Effectgalerij + Filters Filters + Select artwork to apply effects Selecteer grafiek om effecten toe te passen # Select by Color (select.color) Select by Color Selecteren op kleur Smudge Stick… Uitsmeerstift… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 1192bb98f..16e2a9cfc 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5205,6 +5205,14 @@ mark-style Japanese Japońskie Full Justify Pełne justowanie Minimum must not exceed Desired, nor Desired Maximum. Minimum nie może przekraczać wartości optymalnej, a wartość optymalna maksimum. Justification Justowanie + Add filters from the list above Dodaj filtry z listy powyżej + Add to Stack Dodaj do stosu + Applied Filters Zastosowane filtry + Categories Kategorie + Choose a filter Wybierz filtr + Effect Gallery Galeria efektów + Filters Filtry + Select artwork to apply effects Wybierz grafikę, aby zastosować efekty @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them zmienne i zestawy danych pliku nie zostały wczytane (zmienne: {variables}, zestawy danych: {data_sets}): panel Zmienne ich nie wyświetla # Select by Color (select.color) Select by Color Zaznacz według koloru diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 0af3fe6c2..c54d1ad32 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5270,6 +5270,14 @@ mark-style Japanese Japonesas Full Justify Justificação Completa Minimum must not exceed Desired, nor Desired Maximum. O mínimo não pode passar do desejado, nem o desejado do máximo. Justification Justificação + Add filters from the list above Adicione filtros da lista acima + Add to Stack Adicionar à Pilha + Applied Filters Filtros Aplicados + Categories Categorias + Choose a filter Escolha um filtro + Effect Gallery Galeria de Efeitos + Filters Filtros + Select artwork to apply effects Selecione a arte para aplicar efeitos @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them as variáveis e os conjuntos de dados do arquivo não foram lidos (variáveis: {variables}, conjuntos de dados: {data_sets}): o painel Variáveis não os lista # Select by Color (select.color) Select by Color Selecionar por Cor diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index a1b7dd319..62d504105 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4964,6 +4964,14 @@ lighting Light Свет Minimum must not exceed Desired, nor Desired Maximum. Минимум не может превышать желательное значение, а желательное — максимум. Minimum Минимум Justification Выключка + Add filters from the list above Добавьте фильтры из списка выше + Add to Stack Добавить в стек + Applied Filters Применённые фильтры + Categories Категории + Choose a filter Выберите фильтр + Effect Gallery Галерея эффектов + Filters Фильтры + Select artwork to apply effects Выберите графику, чтобы применить эффекты @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them переменные и наборы данных файла не прочитаны (переменных: {variables}, наборов данных: {data_sets}): панель «Переменные» их не показывает # Select by Color (select.color) Select by Color Выделить по цвету diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 843b3cbfd..6d0b1c417 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5309,6 +5309,14 @@ theme Appearance Utseende Full Justify Fullständig marginaljustering Minimum must not exceed Desired, nor Desired Maximum. Minimum får inte överstiga Önskat, och Önskat inte Maximum. Justification Justering + Add filters from the list above Lägg till filter från listan ovan + Add to Stack Lägg till i stacken + Applied Filters Tillämpade filter + Categories Kategorier + Choose a filter Välj ett filter + Effect Gallery Effektgalleri + Filters Filter + Select artwork to apply effects Välj grafik för att tillämpa effekter @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them filens variabler och datauppsättningar lästes inte (variabler: {variables}, datauppsättningar: {data_sets}): panelen Variabler visar dem inte # Select by Color (select.color) Select by Color Markera efter färg diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index b422ebcad..cb4a2e80a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4948,6 +4948,14 @@ lighting Light Світло Minimum must not exceed Desired, nor Desired Maximum. Мінімум не може перевищувати бажане значення, а бажане — максимум. Minimum Мінімум Justification Вирівнювання за шириною + Add filters from the list above Додайте фільтри зі списку вище + Add to Stack Додати до стека + Applied Filters Застосовані фільтри + Categories Категорії + Choose a filter Виберіть фільтр + Effect Gallery Галерея ефектів + Filters Фільтри + Select artwork to apply effects Виберіть графіку, щоб застосувати ефекти @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them змінні та набори даних файлу не прочитано (змінних: {variables}, наборів даних: {data_sets}): панель «Змінні» їх не показує # Select by Color (select.color) Select by Color Виділити за кольором diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 38ce6e879..bef27182b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4948,6 +4948,14 @@ theme Appearance Giao diện Minimum must not exceed Desired, nor Desired Maximum. Giá trị tối thiểu không được vượt quá giá trị mong muốn, và giá trị mong muốn không được vượt quá tối đa. Minimum Tối thiểu Justification Căn đều + Add filters from the list above Thêm bộ lọc từ danh sách phía trên + Add to Stack Thêm vào ngăn xếp + Applied Filters Bộ lọc đã áp dụng + Categories Danh mục + Choose a filter Chọn bộ lọc + Effect Gallery Thư viện hiệu ứng + Filters Bộ lọc + Select artwork to apply effects Chọn đồ họa để áp dụng hiệu ứng @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them các biến và tập dữ liệu của tệp chưa được đọc (biến: {variables}, tập dữ liệu: {data_sets}): bảng Biến không liệt kê chúng # Select by Color (select.color) Select by Color Chọn theo màu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 4fb69a12d..95f9fd49c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5192,6 +5192,14 @@ lighting Light 光源 Minimum must not exceed Desired, nor Desired Maximum. 最小值不能超过所需值,所需值不能超过最大值。 Minimum 最小值 Justification 对齐设置 + Add filters from the list above 从上方列表添加滤镜 + Add to Stack 添加到堆栈 + Applied Filters 已应用的滤镜 + Categories 类别 + Choose a filter 选择滤镜 + Effect Gallery 效果库 + Filters 滤镜 + Select artwork to apply effects 选择要应用效果的图稿 @msg the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them 文件的变量和数据集未被读取(变量:{variables},数据集:{data_sets}):“变量”面板不会列出它们 # Select by Color (select.color) Select by Color 按颜色选择 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 8a0043f58..8654c00c8 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4982,6 +4982,14 @@ lighting Light 光源 Minimum must not exceed Desired, nor Desired Maximum. 最小值不可超過所需值,所需值不可超過最大值。 Minimum 最小值 Justification 對齊設定 + Add filters from the list above 從上方清單新增濾鏡 + Add to Stack 加入堆疊 + Applied Filters 已套用的濾鏡 + Categories 類別 + Choose a filter 選擇濾鏡 + Effect Gallery 效果庫 + Filters 濾鏡 + Select artwork to apply effects 選取要套用效果的圖稿 # Select by Color (select.color) Select by Color 依顏色選取 @msg missing integer `{key}` 未提供整數 `{key}` From 757eaf9afa5b6a06573de90fa5b914b5d80b6c60 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 21 Oct 2026 17:40:07 +0000 Subject: [PATCH 5/5] Effect Gallery: plain filter list and live stack preview Remove hash-coloured thumbnails; preview the selection with the applied filter stack on the canvas (Cancel rolls back). OK commits via batch. Co-authored-by: nrq6993-a11y --- crates/ui-egui/src/dialogs/effect_gallery.rs | 171 ++++++++++++------- crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 9 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + 24 files changed, 161 insertions(+), 63 deletions(-) diff --git a/crates/ui-egui/src/dialogs/effect_gallery.rs b/crates/ui-egui/src/dialogs/effect_gallery.rs index 263d6a54f..c2bfd76b4 100644 --- a/crates/ui-egui/src/dialogs/effect_gallery.rs +++ b/crates/ui-egui/src/dialogs/effect_gallery.rs @@ -1,7 +1,7 @@ //! Effect → Effect Gallery: browse Photoshop-style raster filters by category, build a stack, and //! apply it as separate appearance effects in one undo step. -use egui::{Color32, Rect, Sense, Stroke}; +use egui; use serde_json::{Map, Value, json}; use vectorcraft_effects::{EffectInfo, PIXEL_EFFECTS}; @@ -23,8 +23,15 @@ const CATEGORIES: [(&str, &[&str]); 6] = [ ("Texture", &["Effect", "Texture"]), ]; -pub(super) const SPEC: DialogSpec = - DialogSpec { heading: |_| tl!("Effect Gallery").into(), body, confirm, min_width: 720.0, max_width: Some(820.0), ..DialogSpec::FORM }; +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Effect Gallery").into(), + body, + confirm, + preview: true, + min_width: 720.0, + max_width: Some(820.0), + ..DialogSpec::FORM +}; #[derive(Clone)] struct StackEntry { @@ -39,7 +46,10 @@ pub fn open(app: &mut VectorcraftApp, _p: &Value) -> Result { return Err(tl!("Select artwork to apply effects").into()); } let selected = effects_in_category(0).first().map(|e| e.id).unwrap_or(""); - app.ui.dialog = Some(Dialog::new(KIND, json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false }))); + app.ui.dialog = Some(Dialog::new( + KIND, + json!({ "category": 0, "selected": selected, "stack": [], "stackSelected": 0, "stackFocus": false, "preview": true }), + )); Ok(Value::Null) } @@ -171,31 +181,66 @@ fn effect_param_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, effect_id: & } } -/// A small procedural thumbnail from the effect id (no copied artwork). -fn thumb(ui: &mut egui::Ui, id: &str, size: f32) { - let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); - let hash = id.bytes().fold(0u32, |h, b| h.wrapping_mul(31).wrapping_add(b as u32)); - let c1 = Color32::from_rgb((hash & 0xFF) as u8 | 40, ((hash >> 8) & 0xFF) as u8 | 40, ((hash >> 16) & 0xFF) as u8 | 40); - let c2 = Color32::from_rgb(((hash >> 4) & 0xFF) as u8 | 20, ((hash >> 12) & 0xFF) as u8 | 20, ((hash >> 20) & 0xFF) as u8 | 20); - let painter = ui.painter(); - let steps = 6; - for i in 0..steps { - let t0 = i as f32 / steps as f32; - let t1 = (i + 1) as f32 / steps as f32; - let mix = |t: f32| { - let u = t; - Color32::from_rgba_unmultiplied( - (c1.r() as f32 * (1.0 - u) + c2.r() as f32 * u) as u8, - (c1.g() as f32 * (1.0 - u) + c2.g() as f32 * u) as u8, - (c1.b() as f32 * (1.0 - u) + c2.b() as f32 * u) as u8, - 255, - ) - }; - let band = - Rect::from_min_max(egui::pos2(rect.left() + rect.width() * t0, rect.top()), egui::pos2(rect.left() + rect.width() * t1, rect.bottom())); - painter.rect_filled(band, 0.0, mix((t0 + t1) * 0.5)); +fn stack_batch_commands(stack: &[StackEntry]) -> Vec { + let mut commands = Vec::with_capacity(stack.len() * 2); + for (i, entry) in stack.iter().enumerate() { + let params = Value::Object(entry.params.clone()); + commands.push(json!({ "command": "effect.apply", "params": { "effect": entry.effect, "params": params } })); + if !entry.visible { + commands.push(json!({ "command": "effect.setParams", "params": { "index": i, "visible": false } })); + } + } + commands +} + +fn stack_batch_params(stack: &[StackEntry]) -> Value { + json!({ "label": "Effect Gallery", "commands": stack_batch_commands(stack) }) +} + +/// Live canvas preview of the stack (`command.batch` would settle the dialog interaction each frame). +fn preview_stack_on_canvas(app: &mut VectorcraftApp, stack: &[StackEntry]) { + if app.session.in_interaction() { + let _ = app.session.cancel_interaction(); + } + let _ = app.session.begin_interaction("Effect Gallery"); + for (i, entry) in stack.iter().enumerate() { + let params = Value::Object(entry.params.clone()); + if app.run("effect.apply", json!({ "effect": entry.effect, "params": params })).is_err() { + let _ = app.session.cancel_interaction(); + return; + } + if !entry.visible && app.run("effect.setParams", json!({ "index": i, "visible": false })).is_err() { + let _ = app.session.cancel_interaction(); + return; + } + } +} + +fn stack_preview_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, stack: &[StackEntry]) { + const LAST: &str = super::form::PREVIEWED; + ui.add_space(8.0); + let mut on = d.bool("preview"); + if widgets::check(ui, tl!("Preview"), on, true) { + on = !on; + d.fields.insert("preview".into(), json!(on)); + } + if !on { + if d.fields.remove(LAST).is_some() { + let _ = app.session.cancel_interaction(); + } + return; + } + if stack.is_empty() { + if d.fields.remove(LAST).is_some() { + let _ = app.session.cancel_interaction(); + } + return; + } + let batch = stack_batch_params(stack); + if d.fields.get(LAST) != Some(&batch) || !app.session.in_interaction() { + preview_stack_on_canvas(app, stack); + d.fields.insert(LAST.into(), batch); } - painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Tokens::get(ui.ctx()).input_border), egui::StrokeKind::Inside); } fn effect_label(id: &str) -> String { @@ -251,21 +296,18 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { for e in &effects { let picked = e.id == sel; - ui.horizontal(|ui| { - thumb(ui, e.id, 28.0); - let name = tl!(e.label).trim_end_matches('…').to_string(); - let resp = ui.add(egui::Button::selectable(picked, name)); - if resp.clicked() { - set_selected(d, e.id); - set_stack_focus(d, false); - } - if resp.double_clicked() { - set_selected(d, e.id); - set_stack_focus(d, false); - add_selected(d, e.id); - stack = stack_of(d); - } - }); + let name = tl!(e.label).trim_end_matches('…').to_string(); + let resp = ui.add(egui::Button::selectable(picked, name)); + if resp.clicked() { + set_selected(d, e.id); + set_stack_focus(d, false); + } + if resp.double_clicked() { + set_selected(d, e.id); + set_stack_focus(d, false); + add_selected(d, e.id); + stack = stack_of(d); + } } }); if widgets::flat_button(ui, tl!("Add to Stack"), 120.0).clicked() && !sel.is_empty() { @@ -273,18 +315,6 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { stack = stack_of(d); } }); - ui.add_space(8.0); - ui.vertical(|ui| { - widgets::subheader(ui, tl!("Preview")); - ui.set_width(200.0); - let (param_effect, _) = param_target(d, &stack, stack_sel, &sel); - if !param_effect.is_empty() { - thumb(ui, ¶m_effect, 160.0); - ui.label(egui::RichText::new(effect_label(¶m_effect)).color(t.text_strong)); - } else { - ui.label(egui::RichText::new(tl!("Choose a filter")).color(t.text_dim)); - } - }); }); ui.add_space(10.0); @@ -353,6 +383,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } write_stack(d, &stack); d.fields.insert("stackSelected".into(), json!(stack_sel)); + stack_preview_row(app, ui, d, &stack); false } @@ -369,18 +400,17 @@ fn add_selected(d: &mut Dialog, id: &str) { fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let stack = stack_of(d); if stack.is_empty() { + if app.session.in_interaction() { + let _ = app.session.cancel_interaction(); + } app.ui.dialog = None; return Ok(Value::Null); } - let mut commands = Vec::with_capacity(stack.len() * 2); - for (i, entry) in stack.iter().enumerate() { - let params = Value::Object(entry.params.clone()); - commands.push(json!({ "command": "effect.apply", "params": { "effect": entry.effect, "params": params } })); - if !entry.visible { - commands.push(json!({ "command": "effect.setParams", "params": { "index": i, "visible": false } })); - } + let batch = stack_batch_params(&stack); + if d.bool("preview") && app.session.in_interaction() { + return super::form::commit_preview(app, "command.batch", batch); } - let r = app.run("command.batch", json!({ "label": "Effect Gallery", "commands": commands })); + let r = app.run("command.batch", batch); if r.is_ok() { app.ui.dialog = None; } @@ -403,6 +433,21 @@ mod tests { out.textures_delta.clear(); } + #[test] + fn stack_preview_is_one_interaction() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap(); + app.run("ui.effectGalleryDialog", json!({})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("stack".into(), json!([{ "effect": "artistic.cutout", "visible": true, "params": { "levels": 3 } }])); + frame(&mut app); + assert!(app.session.in_interaction(), "the stack previews on the canvas"); + crate::dialogs::cancel(&mut app); + assert!(!app.session.in_interaction()); + assert!(app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects.is_empty()); + } + #[test] fn stack_ok_is_one_undo_step() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f8ab8ca23..0ab57b7a3 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5351,3 +5351,5 @@ theme Appearance Външен вид Shift: Изместване: Fresco… Фреска… Fresco Фреска + Effect Gallery… Галерия с ефекти… + Effect Gallery Dialog Диалог „Галерия с ефекти“ diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 0124a1f20..54f38c94b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5394,3 +5394,5 @@ mark-style Japanese Japonské Shift: Posun: Fresco… Freska… Fresco Freska + Effect Gallery… Galerie efektů… + Effect Gallery Dialog Dialog Galerie efektů diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 047f7b073..14bdad93a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5367,3 +5367,5 @@ swatch Bright Violet Kraftig violet Shift: Forskydning: Fresco… Fresko… Fresco Fresko + Effect Gallery… Effektgalleri… + Effect Gallery Dialog Dialogboksen Effektgalleri diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 8fca8c3e9..6aacd6e3c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4975,3 +4975,5 @@ effect Soft Weich Shift: Verschiebung: Fresco… Fresko… Fresco Fresko + Effect Gallery… Effekt-Galerie… + Effect Gallery Dialog Dialogfeld „Effekt-Galerie“ diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index c97154f1f..2ea3887d3 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5346,3 +5346,5 @@ effect Points Σημεία Shift: Μετατόπιση: Fresco… Νωπογραφία… Fresco Νωπογραφία + Effect Gallery… Συλλογή εφέ… + Effect Gallery Dialog Παράθυρο διαλόγου Συλλογή εφέ diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a5d9eae6d..00a483a19 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5336,3 +5336,5 @@ mark-style Japanese Japonesas Shift: Desplazamiento: Fresco… Fresco… Fresco Fresco + Effect Gallery… Galería de efectos… + Effect Gallery Dialog Cuadro de diálogo Galería de efectos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 88f14d09b..19db20bff 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5425,3 +5425,5 @@ swatch Bright Violet Kirkas violetti Shift: Siirtymä: Fresco… Fresko… Fresco Fresko + Effect Gallery… Tehostegalleria… + Effect Gallery Dialog Tehostegalleria-valintaikkuna diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 69cd9ab56..a1abcc6ba 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5393,3 +5393,5 @@ theme Appearance Apparence Shift: Décalage : Fresco… Fresque… Fresco Fresque + Effect Gallery… Galerie d'effets… + Effect Gallery Dialog Boîte de dialogue Galerie d'effets diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 992560a4d..448ee605f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5380,3 +5380,5 @@ imageTrace Outline Körvonal Shift: Eltolás: Fresco… Freskó… Fresco Freskó + Effect Gallery… Effektusgaléria… + Effect Gallery Dialog Effektusgaléria párbeszédpanel diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 230b914f0..917c48816 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5367,3 +5367,5 @@ swatch Bright Violet Violet cerah Shift: Pergeseran: Fresco… Fresko… Fresco Fresko + Effect Gallery… Galeri Efek… + Effect Gallery Dialog Dialog Galeri Efek diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 14824bafd..ac59807ef 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5009,3 +5009,5 @@ lighting Light Luce Shift: Spostamento: Fresco… Affresco… Fresco Affresco + Effect Gallery… Galleria effetti… + Effect Gallery Dialog Finestra di dialogo Galleria effetti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 21709cca2..fd68fd1f9 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5056,3 +5056,5 @@ lighting Light ライト Shift: シフト: Fresco… フレスコ… Fresco フレスコ + Effect Gallery… 効果ギャラリー… + Effect Gallery Dialog 効果ギャラリーダイアログ diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index abebbb73d..db1915b3b 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5296,3 +5296,12 @@ imageTrace Outline კონტური Shift: წანაცვლება: Fresco… ფრესკა… Fresco ფრესკა + Effect Gallery ეფექტების გალერეა + Add filters from the list above დაამატეთ ფილტრები ზემოთ მოცემული სიიდან + Add to Stack დასტაში დამატება + Applied Filters გამოყენებული ფილტრები + Categories კატეგორიები + Filters ფილტრები + Select artwork to apply effects აირჩიეთ ნამუშევარი ეფექტების გამოსაყენებლად + Effect Gallery… ეფექტების გალერეა… + Effect Gallery Dialog ეფექტების გალერეა დიალოგი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 71b16eb8c..2337fad72 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4972,3 +4972,5 @@ lighting Light 광원 Shift: 이동: Fresco… 프레스코… Fresco 프레스코 + Effect Gallery… 효과 갤러리… + Effect Gallery Dialog 효과 갤러리 대화 상자 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 452a7d650..a0040b531 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5409,3 +5409,5 @@ swatch Bright Violet Felviolet Shift: Verschuiving: Fresco… Fresco… Fresco Fresco + Effect Gallery… Effectgalerij… + Effect Gallery Dialog Dialoogvenster Effectgalerij diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 16e2a9cfc..761bb0dae 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5246,3 +5246,5 @@ mark-style Japanese Japońskie Shift: Przesunięcie: Fresco… Fresk… Fresco Fresk + Effect Gallery… Galeria efektów… + Effect Gallery Dialog Okno dialogowe Galeria efektów diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index c54d1ad32..e40d6801b 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5311,3 +5311,5 @@ mark-style Japanese Japonesas Shift: Deslocamento: Fresco… Afresco… Fresco Afresco + Effect Gallery… Galeria de Efeitos… + Effect Gallery Dialog Caixa de diálogo Galeria de Efeitos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 62d504105..2925e2fd8 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5005,3 +5005,5 @@ lighting Light Свет Shift: Сдвиг: Fresco… Фреска… Fresco Фреска + Effect Gallery… Галерея эффектов… + Effect Gallery Dialog Диалог «Галерея эффектов» diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 6d0b1c417..2ba5a4201 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5350,3 +5350,5 @@ theme Appearance Utseende Shift: Förskjutning: Fresco… Fresk… Fresco Fresk + Effect Gallery… Effektgalleri… + Effect Gallery Dialog Dialogrutan Effektgalleri diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index cb4a2e80a..e9fea9f52 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4989,3 +4989,5 @@ lighting Light Світло Shift: Зсув: Fresco… Фреска… Fresco Фреска + Effect Gallery… Галерея ефектів… + Effect Gallery Dialog Діалог «Галерея ефектів» diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bef27182b..1858de13b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4989,3 +4989,5 @@ theme Appearance Giao diện Shift: Dịch chuyển: Fresco… Bích họa… Fresco Bích họa + Effect Gallery… Thư viện hiệu ứng… + Effect Gallery Dialog Hộp thoại Thư viện hiệu ứng diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 95f9fd49c..849044e52 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5233,3 +5233,5 @@ lighting Light 光源 Shift: 偏移: Fresco… 壁画… Fresco 壁画 + Effect Gallery… 效果库… + Effect Gallery Dialog 效果库对话框 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 8654c00c8..05ac2082a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5028,3 +5028,5 @@ lighting Light 光源 Shift: 偏移: Fresco… 壁畫… Fresco 壁畫 + Effect Gallery… 效果庫… + Effect Gallery Dialog 效果庫對話框