Merge #575: Effects panel: the level row, Isolate Blending and Knockout Group

Conflict: crates/ui-egui/src/i18n.rs: kept next's Step and Repeat rows and the PR's Effects rows
Conflict: crates/ui-egui/src/i18n/ar.rs, it.rs, ja.rs, pt_br.rs, uk.rs: kept both sides' rows
Conflict: ROADMAP.md: kept both progress-log lines
This commit is contained in:
aiongg committed 2026-10-12 12:25:51 +00:00
commit 1f69461ff5
8 files changed
+111

No files matched your search

+1
View File
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-12** · The Effects 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** · Edit › Step and Repeat has its InDesign dialog: Repeat (Count, or Rows and Columns with Create as a grid), Offset (Vertical, Horizontal, in ruler units) and Preview, which shows the copies while the dialog is open; Cancel takes them away and OK makes them as one undo step. No readiness percentage change.
- **2026-10-12** · Load Styles: the Paragraph Styles and Character Styles panels load styles from another DesignCraft document or an IDML file, with the styles, swatches and lists they name; a dialog picks the styles and what happens to one the document defines differently (`style.load`). No readiness percentage change.
- **2026-10-12** · On screen, blend modes blend with the objects below and not with the paper, as in InDesign and in PDF export: a Color Dodge or Screen object over blank paper shows as it is. No readiness percentage change.
+4
View File
@@ -1100,6 +1100,10 @@ const TABLE: &[(&str, [&str; 5])] = &[
("Vertical:", ["Vertikal:", "Vertical :", "Vertical:", "垂直方向:", "垂直:"]),
("Horizontal:", ["Horizontal:", "Horizontal :", "Horizontal:", "水平方向:", "水平:"]),
("Invalid value", ["Ungültiger Wert", "Valeur non valide", "Valor no válido", "無効な値", "无效值"]),
("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
@@ -1068,6 +1068,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Vertical:", "رأسي:"),
("Horizontal:", "أفقي:"),
("Invalid value", "قيمة غير صالحة"),
("Isolate Blending", "عزل المزج"),
("Knockout Group", "مجموعة مفرغة"),
("Object:", "الكائن:"),
("Group:", "المجموعة:"),
("Apply None", "إزالة اللون"),
("Height Only", "الارتفاع فقط"),
("Width Only", "العرض فقط"),
+4
View File
@@ -422,6 +422,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Vertical:", "Verticale:"),
("Horizontal:", "Orizzontale:"),
("Invalid value", "Valore non valido"),
("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
@@ -537,6 +537,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Create as a grid", "グリッドとして作成"),
("Rows:", "行:"),
("Invalid value", "無効な値"),
("Isolate Blending", "描画モードを分離"),
("Knockout Group", "グループの抜き"),
("Object:", "オブジェクト:"),
("Group:", "グループ:"),
("Apply Condition", "条件を適用"),
("Apply Parent to Pages…", "ページに親ページを適用…"),
("ArtCraft App Page…", "ArtCraft アプリページ…"),
+4
View File
@@ -1065,6 +1065,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Vertical:", "Vertical:"),
("Horizontal:", "Horizontal:"),
("Invalid value", "Valor inválido"),
("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
@@ -423,6 +423,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Vertical:", "Вертикально:"),
("Horizontal:", "Горизонтально:"),
("Invalid value", "Неприпустиме значення"),
("Isolate Blending", "Ізолювати змішування"),
("Knockout Group", "Група вибивання"),
("Object:", "Об’єкт:"),
("Group:", "Група:"),
("Apply Condition", "Застосувати умову"),
("Apply Parent to Pages…", "Застосувати шаблон до сторінок…"),
("ArtCraft App Page…", "Сторінка програми ArtCraft…"),
+86
View File
@@ -2240,6 +2240,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;
@@ -2282,6 +2289,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) {
@@ -2615,3 +2636,68 @@ mod tests {
assert!(h.query_by(|n| n.role() == egui::accesskit::Role::TextInput && n.value().as_deref() == Some("90°")).is_some());
}
}
#[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);
}
}