diff --git a/ROADMAP.md b/ROADMAP.md index 175a78328..ffba7dc3c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index e46bb9d33..4fea84763 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -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…", diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 7202f2d33..5962b7250 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -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", "العرض فقط"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 741085c36..abbe1cdfb 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -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…"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index a5829b83c..01dfb0986 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -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 アプリページ…"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 3590fea81..eca9d10ee 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -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"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 59d210001..93d648077 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -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…"), diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 44794eb52..07dcc9c5f 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -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) -> 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 = 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 = 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); + } +}