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 <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-10-22 07:03:19 +07:00
committed by bflatastic
co-authored by nrq6993-a11y
parent d275fbf725
commit 757eaf9afa
24 changed files with 161 additions and 63 deletions
+108 -63
View File
@@ -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<Value, String> {
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<Value> {
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, &param_effect, 160.0);
ui.label(egui::RichText::new(effect_label(&param_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<Value, String> {
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());
+2
View File
@@ -5351,3 +5351,5 @@ theme Appearance Външен вид
Shift: Изместване:
Fresco… Фреска…
Fresco Фреска
Effect Gallery… Галерия с ефекти…
Effect Gallery Dialog Диалог „Галерия с ефекти“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5394,3 +5394,5 @@ mark-style Japanese Japonské
Shift: Posun:
Fresco… Freska…
Fresco Freska
Effect Gallery… Galerie efektů…
Effect Gallery Dialog Dialog Galerie efektů
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5367,3 +5367,5 @@ swatch Bright Violet Kraftig violet
Shift: Forskydning:
Fresco… Fresko…
Fresco Fresko
Effect Gallery… Effektgalleri…
Effect Gallery Dialog Dialogboksen Effektgalleri
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4975,3 +4975,5 @@ effect Soft Weich
Shift: Verschiebung:
Fresco… Fresko…
Fresco Fresko
Effect Gallery… Effekt-Galerie…
Effect Gallery Dialog Dialogfeld „Effekt-Galerie“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5346,3 +5346,5 @@ effect Points Σημεία
Shift: Μετατόπιση:
Fresco… Νωπογραφία…
Fresco Νωπογραφία
Effect Gallery… Συλλογή εφέ…
Effect Gallery Dialog Παράθυρο διαλόγου Συλλογή εφέ
Can't render this file because it contains an unexpected character in line 2040 and column 19.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5425,3 +5425,5 @@ swatch Bright Violet Kirkas violetti
Shift: Siirtymä:
Fresco… Fresko…
Fresco Fresko
Effect Gallery… Tehostegalleria…
Effect Gallery Dialog Tehostegalleria-valintaikkuna
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 3811 and column 19.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5056,3 +5056,5 @@ lighting Light ライト
Shift: シフト:
Fresco… フレスコ…
Fresco フレスコ
Effect Gallery… 効果ギャラリー…
Effect Gallery Dialog 効果ギャラリーダイアログ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+9
View File
@@ -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 ეფექტების გალერეა დიალოგი
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4972,3 +4972,5 @@ lighting Light 광원
Shift: 이동:
Fresco… 프레스코…
Fresco 프레스코
Effect Gallery… 효과 갤러리…
Effect Gallery Dialog 효과 갤러리 대화 상자
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5409,3 +5409,5 @@ swatch Bright Violet Felviolet
Shift: Verschuiving:
Fresco… Fresco…
Fresco Fresco
Effect Gallery… Effectgalerij…
Effect Gallery Dialog Dialoogvenster Effectgalerij
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -5005,3 +5005,5 @@ lighting Light Свет
Shift: Сдвиг:
Fresco… Фреска…
Fresco Фреска
Effect Gallery… Галерея эффектов…
Effect Gallery Dialog Диалог «Галерея эффектов»
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5350,3 +5350,5 @@ theme Appearance Utseende
Shift: Förskjutning:
Fresco… Fresk…
Fresco Fresk
Effect Gallery… Effektgalleri…
Effect Gallery Dialog Dialogrutan Effektgalleri
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -4989,3 +4989,5 @@ lighting Light Світло
Shift: Зсув:
Fresco… Фреска…
Fresco Фреска
Effect Gallery… Галерея ефектів…
Effect Gallery Dialog Діалог «Галерея ефектів»
Can't render this file because it contains an unexpected character in line 733 and column 40.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 4023 and column 10.
+2
View File
@@ -5233,3 +5233,5 @@ lighting Light 光源
Shift: 偏移:
Fresco… 壁画…
Fresco 壁画
Effect Gallery… 效果库…
Effect Gallery Dialog 效果库对话框
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5028,3 +5028,5 @@ lighting Light 光源
Shift: 偏移:
Fresco… 壁畫…
Fresco 壁畫
Effect Gallery… 效果庫…
Effect Gallery Dialog 效果庫對話框
Can't render this file because it contains an unexpected character in line 6 and column 47.