feat(ui): Effects panel shows the level, Isolate Blending and Knockout Group

The Effects panel had the blend mode, opacity and Drop Shadow only. Like InDesign's it now names the level the settings apply to with its blend mode and opacity (Group: Color Dodge 100%, Object: Normal 100%), and has Isolate Blending and Knockout Group, enabled for a selected group, through object.transparencyGroup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
moppeeerandClaude Opus 5.5 authored and paaspaas00 committed 2026-10-12 18:17:34 +02:00
1 parent d020b4e85d
commit 45a228426a
8 files changed
+111

No files matched your search

+1
View File
@@ -132,6 +132,7 @@ Newest first.
- **2026-10-12** · Window close and application/menu Quit ask about unsaved documents; later Cancel keeps every tab open, including earlier Don't Save choices. Approvals are rechecked after edits. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
- **2026-10-12** · Each running app keeps recovery data in its own locked folder; current folder and lock ownership are checked before writes and cleanup. Failed isolation disables recovery with a warning instead of sharing document IDs. No readiness percentage change.
- **2026-10-12** · Native engine saves and exports replace files atomically; failed writes retain the original, private staging preserves Unix permissions, and valid long filenames work. Desktop Services and CLI raster exports remain separate follow-ups. No readiness percentage change.
- **2026-10-12** · The Effects panel shows the level its settings apply to ("Group: Color Dodge 100%") and, as in InDesign, Isolate Blending and Knockout Group for a selected group. No readiness percentage change.
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change.
- **2026-10-11** · `object.fill` and `object.stroke` refuse a swatch name the document doesn't have (it painted nothing) and suggest the name meant (`Paper` → `[Paper]`). No readiness percentage change.
+4
View File
@@ -739,6 +739,10 @@ const TABLE: &[(&str, [&str; 5])] = &[
),
("Apply Cell Style", ["Apply Cell Style", "Apply Cell Style", "Apply Cell Style", "Apply Cell Style", "应用单元格样式"]),
("Apply Color", ["Apply Color", "Apply Color", "Apply Color", "Apply Color", "应用颜色"]),
("Isolate Blending", ["Füllmethode isolieren", "Isoler le mode de fusion", "Aislar fusión", "描画モードを分離", "分离混合"]),
("Knockout Group", ["Aussparungsgruppe", "Groupe de masquage", "Grupo de exclusión", "グループの抜き", "挖空组"]),
("Object:", ["Objekt:", "Objet :", "Objeto:", "オブジェクト:", "对象:"]),
("Group:", ["Gruppe:", "Groupe :", "Grupo:", "グループ:", "编组:"]),
("Apply Condition", ["Apply Condition", "Apply Condition", "Apply Condition", "Apply Condition", "应用条件"]),
(
"Apply Parent to Pages…",
+4
View File
@@ -1041,6 +1041,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Formatting affects container (J)", "تطبيق التنسيق على الحاوية (J)"),
("Formatting affects text (J)", "تطبيق التنسيق على النص (J)"),
("Apply Color", "تطبيق اللون"),
("Isolate Blending", "عزل المزج"),
("Knockout Group", "مجموعة مفرغة"),
("Object:", "الكائن:"),
("Group:", "المجموعة:"),
("Apply None", "إزالة اللون"),
("Height Only", "الارتفاع فقط"),
("Width Only", "العرض فقط"),
+4
View File
@@ -412,6 +412,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Apply Attributes (Eyedropper)", "Applica attributi (Contagocce)"),
("Apply Cell Style", "Applica stile di cella"),
("Apply Color", "Applica colore"),
("Isolate Blending", "Isola fusione"),
("Knockout Group", "Gruppo di foratura"),
("Object:", "Oggetto:"),
("Group:", "Gruppo:"),
("Apply Condition", "Applica condizione"),
("Apply Parent to Pages…", "Applica pagina mastro alle pagine…"),
("ArtCraft App Page…", "Pagina dell'app ArtCraft…"),
+4
View File
@@ -414,6 +414,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Apply Attributes (Eyedropper)", "属性を適用 (スポイト)"),
("Apply Cell Style", "セルスタイルを適用"),
("Apply Color", "カラーを適用"),
("Isolate Blending", "描画モードを分離"),
("Knockout Group", "グループの抜き"),
("Object:", "オブジェクト:"),
("Group:", "グループ:"),
("Apply Condition", "条件を適用"),
("Apply Parent to Pages…", "ページに親ページを適用…"),
("ArtCraft App Page…", "ArtCraft アプリページ…"),
+4
View File
@@ -1045,6 +1045,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Formatting affects container (J)", "Formatação afeta o contêiner (J)"),
("Formatting affects text (J)", "Formatação afeta o texto (J)"),
("Apply Color", "Aplicar cor"),
("Isolate Blending", "Isolar mesclagem"),
("Knockout Group", "Grupo de vazamento"),
("Object:", "Objeto:"),
("Group:", "Grupo:"),
("Apply None", "Aplicar nenhum"),
("Height Only", "Somente altura"),
("Width Only", "Somente largura"),
+4
View File
@@ -415,6 +415,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Apply Attributes (Eyedropper)", "Застосувати атрибути (піпетка)"),
("Apply Cell Style", "Застосувати стиль клітинки"),
("Apply Color", "Застосувати колір"),
("Isolate Blending", "Ізолювати змішування"),
("Knockout Group", "Група вибивання"),
("Object:", "Об’єкт:"),
("Group:", "Група:"),
("Apply Condition", "Застосувати умову"),
("Apply Parent to Pages…", "Застосувати шаблон до сторінок…"),
("ArtCraft App Page…", "Сторінка програми ArtCraft…"),
+86
View File
@@ -1988,6 +1988,13 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
let _ = app.run("object.opacity", json!({"opacity": (v / 100.0).clamp(0.0, 1.0)}));
}
});
// The level the settings apply to, with its blend mode and opacity ("Group: Color Dodge 100%").
let group = matches!(it.content, designcraft_doc::Content::Group { .. });
ui.horizontal(|ui| {
let level = crate::i18n::tr(&app.ui.language, if group { "Group:" } else { "Object:" });
crate::rtl::label(ui, egui::RichText::new(level).font(crate::theme::semibold(12.0)));
crate::rtl::label(ui, format!("{} {}%", crate::i18n::tr(&app.ui.language, it.blend.label()), (it.opacity * 100.0).round()));
});
divider(ui);
let ds = it.effects.drop_shadow.clone();
let mut on = ds.on;
@@ -2030,6 +2037,20 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
ui.end_row();
});
}
divider(ui);
// Groups only, as in InDesign (greyed for other objects).
ui.horizontal(|ui| {
let mut iso = it.isolate;
let label = crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Isolate Blending"));
if ui.add_enabled(group, egui::Checkbox::new(&mut iso, label)).changed() {
let _ = app.run("object.transparencyGroup", json!({"isolate": iso}));
}
let mut ko = it.knockout;
let label = crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Knockout Group"));
if ui.add_enabled(group, egui::Checkbox::new(&mut ko, label)).changed() {
let _ = app.run("object.transparencyGroup", json!({"knockout": ko}));
}
});
}
pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
@@ -2208,3 +2229,68 @@ mod tests {
assert_eq!(after, name, "clicking the swatch chip applies it");
}
}
#[cfg(test)]
mod effects_panel_tests {
use super::*;
/// One frame of the Effects panel: the text painted and where.
fn frame(app: &mut DesignApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<(String, egui::Rect)> {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(320.0, 400.0));
let time = ctx.input(|i| i.time) + 1.0 / 60.0;
let mut out =
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), time: Some(time), events, ..Default::default() }, |ui| effects_panel(app, ui));
out.textures_delta.clear();
let mut v = Vec::new();
for s in out.shapes {
if let egui::Shape::Text(t) = s.shape {
v.push((t.galley.job.text.clone(), t.visual_bounding_rect()));
}
}
v
}
fn click(app: &mut DesignApp, ctx: &egui::Context, label: &str) {
let labels = frame(app, ctx, vec![]);
let pos = labels.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("{label} is painted")).1.center();
for pressed in [true, false] {
frame(
app,
ctx,
vec![
egui::Event::PointerMoved(pos),
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE },
],
);
}
}
#[test]
fn the_effects_panel_shows_the_level_and_isolate_blending_and_knockout_group_for_groups() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({})).unwrap();
let a = app.run("frame.create", json!({"rect": [72, 72, 144, 144]})).unwrap()["id"].as_u64().unwrap();
let b = app.run("frame.create", json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap();
let g = app.run("object.group", json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
app.run("selection.set", json!({"ids": [g]})).unwrap();
app.run("object.opacity", json!({"opacity": 1.0, "blend": "colorDodge"})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx, "en");
crate::theme::apply(&ctx, &Tokens::for_brightness(crate::theme::Brightness::default()));
let texts: Vec<String> = frame(&mut app, &ctx, vec![]).into_iter().map(|(t, _)| t).collect();
for expected in ["Group:", "Color Dodge 100%", "Isolate Blending", "Knockout Group"] {
assert!(texts.iter().any(|t| t == expected), "{expected} in {texts:?}");
}
let item = |app: &DesignApp, id: u64| app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).cloned().unwrap();
click(&mut app, &ctx, "Isolate Blending");
assert!(item(&app, g).isolate);
click(&mut app, &ctx, "Knockout Group");
assert!(item(&app, g).knockout);
// Another object: the level is Object, and the group options are greyed.
app.run("selection.set", json!({"ids": [a]})).unwrap();
let texts: Vec<String> = frame(&mut app, &ctx, vec![]).into_iter().map(|(t, _)| t).collect();
assert!(texts.iter().any(|t| t == "Object:"), "{texts:?}");
click(&mut app, &ctx, "Knockout Group");
assert!(!item(&app, a).knockout);
}
}