diff --git a/ROADMAP.md b/ROADMAP.md index 0d4cc3a5e..c1d16dd7e 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 Tools panel's Fill/Stroke keys work: D (default fill and stroke), X (fill or stroke in front), Shift+X (swap per object, tints included) and / (Apply None), as `color.*` commands; swap and default are one undo step (`object.swapFillStroke`, `object.defaultFillStroke`); the Color panel follows the proxy's fill/stroke choice. No readiness percentage change. - **2026-10-12** · With a placed graphic selected (Direct Selection), the Transform values act on the graphic inside its frame, as in InDesign: a tilted frame's image can be straightened to 0°. No readiness percentage change. - **2026-10-11** · Font menu laid out like InDesign's: filters (classification, favourites, recently added), families opening to their styles, the selected text as the sample in three diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 34ebd72ed..4a9f6a0ac 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -208,6 +208,24 @@ pub fn specs() -> Vec { ) } ), + cmd!( + "object.swapFillStroke", + "Swap Fill and Stroke", + [], + None, + "{ids?} — each object's fill and stroke colours (and tints) trade places; locked objects stay as they are", + has_editable_selection, + |s, p| edit_unlocked(s, p, swap_fill_stroke) + ), + cmd!( + "object.defaultFillStroke", + "Default Fill and Stroke", + [], + None, + "{ids?} — no fill and a black stroke; locked objects stay as they are", + has_editable_selection, + |s, p| edit_unlocked(s, p, default_fill_stroke) + ), cmd!( "object.stroke", "Stroke", @@ -1873,6 +1891,60 @@ fn set_flag(s: &mut Session, p: &Value, f: impl Fn(&mut Item), deselect: bool) - }) } +/// An object a colour command may change: neither it nor its layer is locked. +fn unlocked(d: &Document, it: &Item) -> bool { + !it.locked && d.layer(it.layer).is_none_or(|l| !l.locked) +} + +fn has_editable_selection(s: &Session) -> std::result::Result<(), String> { + has_selection(s)?; + let st = s.active().ok_or_else(|| super::NOTHING_SELECTED.to_string())?; + if st.selection.items.iter().any(|id| st.doc.item(*id).is_some_and(|it| unlocked(&st.doc, it))) { + Ok(()) + } else { + Err("the selected objects are locked".into()) + } +} + +/// Runs `f` on each unlocked target in one edit (one undo step). +fn edit_unlocked(s: &mut Session, p: &Value, f: impl Fn(&mut Item)) -> Result { + let ids = targets(s, p)?; + s.edit(|d, _| { + let mut changed = 0; + for id in &ids { + if d.item(*id).is_some_and(|it| unlocked(d, it)) + && let Some(it) = d.item_mut(*id) + { + f(it); + changed += 1; + } + } + Ok(json!({"changed": changed})) + }) +} + +/// A stroke that gets a colour needs a weight to show it. +fn visible_weight(st: &mut Stroke) { + if st.swatch != designcraft_color::swatch::NONE && st.weight <= 0.0 { + st.weight = Stroke::default().weight; + } +} + +fn swap_fill_stroke(it: &mut Item) { + let fill = std::mem::take(&mut it.fill); + it.fill = Fill { swatch: std::mem::take(&mut it.stroke.swatch), tint: it.stroke.tint, ..fill.clone() }; + it.stroke.swatch = fill.swatch; + it.stroke.tint = fill.tint; + visible_weight(&mut it.stroke); +} + +fn default_fill_stroke(it: &mut Item) { + it.fill = Fill { swatch: designcraft_color::swatch::NONE.into(), tint: 1.0, ..it.fill.clone() }; + it.stroke.swatch = designcraft_color::swatch::BLACK.into(); + it.stroke.tint = 1.0; + visible_weight(&mut it.stroke); +} + fn all_flag(s: &mut Session, f: impl Fn(&mut Item)) -> Result { s.edit(|d, _| { for i in 0..d.spreads.len() { @@ -2656,6 +2728,44 @@ fn step_and_repeat(s: &mut Session, p: &Value) -> Result { mod step_repeat_tests { use super::*; + #[test] + fn swap_and_default_fill_stroke_are_one_undo_step_per_object() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let id = |v: Value| ItemId(v["id"].as_u64().unwrap()); + let a = id(s.execute("frame.create", &json!({"rect": [0, 0, 10, 10]})).unwrap()); + let b = id(s.execute("frame.create", &json!({"rect": [20, 0, 30, 10]})).unwrap()); + let locked = id(s.execute("frame.create", &json!({"rect": [40, 0, 50, 10]})).unwrap()); + s.execute("object.fill", &json!({"ids": [a.0], "swatch": "[Paper]", "tint": 0.5})).unwrap(); + s.execute("object.stroke", &json!({"ids": [a.0], "swatch": "[Black]", "weight": 2})).unwrap(); + s.execute("object.fill", &json!({"ids": [b.0, locked.0], "swatch": "[Black]"})).unwrap(); + s.execute("object.stroke", &json!({"ids": [b.0, locked.0], "swatch": "[None]"})).unwrap(); + s.execute("object.lock", &json!({"ids": [locked.0]})).unwrap(); + s.execute("selection.set", &json!({"ids": [a.0, b.0, locked.0]})).unwrap(); + let colors = |s: &Session, i: ItemId| { + let it = s.doc().unwrap().doc.item(i).unwrap().clone(); + (it.fill.swatch, it.fill.tint, it.stroke.swatch, it.stroke.tint) + }; + let before = [colors(&s, a), colors(&s, b), colors(&s, locked)]; + + assert_eq!(s.execute("object.swapFillStroke", &json!({})).unwrap()["changed"], 2); + assert_eq!(colors(&s, a), ("[Black]".into(), 1.0, "[Paper]".into(), 0.5)); + assert_eq!(colors(&s, b), ("[None]".into(), 1.0, "[Black]".into(), 1.0)); + assert_eq!(colors(&s, locked), before[2], "a locked object keeps its colours"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!([colors(&s, a), colors(&s, b), colors(&s, locked)], before, "one undo restores both"); + + s.execute("object.defaultFillStroke", &json!({})).unwrap(); + for i in [a, b] { + assert_eq!(colors(&s, i), ("[None]".into(), 1.0, "[Black]".into(), 1.0)); + } + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!([colors(&s, a), colors(&s, b), colors(&s, locked)], before); + + s.execute("selection.set", &json!({"ids": [locked.0]})).unwrap(); + assert!(s.execute("object.swapFillStroke", &json!({})).is_err(), "nothing unlocked to change"); + } + #[test] fn step_repeat_validates_counts_and_offsets() { assert_eq!(step_repeat_offsets(&json!({"count": MAX_STEP_REPEAT_COPIES})).unwrap().len(), MAX_STEP_REPEAT_COPIES as usize); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 91194d1bf..cd5cd5901 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -999,6 +999,15 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Fill (X)", ["Fill (X)", "Fill (X)", "Fill (X)", "Fill (X)", "填色 (X)"]), ("Stroke (X)", ["Stroke (X)", "Stroke (X)", "Stroke (X)", "Stroke (X)", "描边 (X)"]), ("Apply None (/)", ["Apply None (/)", "Apply None (/)", "Apply None (/)", "Apply None (/)", "应用无 (/)"]), + ( + "Default Fill and Stroke", + ["Default Fill and Stroke", "Default Fill and Stroke", "Default Fill and Stroke", "Default Fill and Stroke", "默认填色和描边"], + ), + ("Swap Fill and Stroke", ["Swap Fill and Stroke", "Swap Fill and Stroke", "Swap Fill and Stroke", "Swap Fill and Stroke", "交换填色和描边"]), + ( + "Toggle Fill and Stroke", + ["Toggle Fill and Stroke", "Toggle Fill and Stroke", "Toggle Fill and Stroke", "Toggle Fill and Stroke", "切换填色和描边"], + ), ("Reverse the gradient", ["Reverse the gradient", "Reverse the gradient", "Reverse the gradient", "Reverse the gradient", "反转渐变"]), ("Style source", ["Style source", "Style source", "Style source", "Style source", "样式源"]), ("Open", ["Open", "Open", "Open", "Open", "打开"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 11e28aa19..eb77c55e7 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -533,6 +533,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Superscript", "النص المرتفع"), ("Tag the selection", "إضافة وسم للتحديد"), ("The structure is valid.", "البنية صالحة."), + ("Toggle Fill and Stroke", "التبديل بين التعبئة والحد"), ("Toggle double column", "تبديل التخطيط بعمودين"), ("Type:", "النوع:"), ("Underline", "تسطير"), @@ -858,6 +859,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Crop to:", "القص إلى:"), ("Cross-Reference Format", "تنسيق المرجع المتقاطع"), ("Cursor Key:", "مفتاح المؤشر:"), + ("Default Fill and Stroke", "التعبئة والحد الافتراضيان"), ("Default Fill and Stroke (D)", "التعبئة والحد الافتراضيان (D)"), ("Default View:", "العرض الافتراضي:"), ("Delete Column", "حذف العمود"), @@ -1004,6 +1006,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Style Name Conflicts", "تعارض أسماء الأنماط"), ("Stylistic Sets", "المجموعات الأسلوبية"), ("Subdivisions:", "التقسيمات الفرعية:"), + ("Swap Fill and Stroke", "تبديل التعبئة والحد"), ("Swap Fill and Stroke (Shift+X)", "تبديل التعبئة والحد (Shift+X)"), ("Synchronize", "مزامنة"), ("System default", "افتراضي النظام"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 9dbce2297..2302dd0db 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -378,6 +378,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Fill (X)", "Riempimento (X)"), ("Stroke (X)", "Traccia (X)"), ("Apply None (/)", "Applica nessuno (/)"), + ("Default Fill and Stroke", "Riempimento e traccia predefiniti"), + ("Swap Fill and Stroke", "Scambia riempimento e traccia"), + ("Toggle Fill and Stroke", "Alterna riempimento e traccia"), ("Reverse the gradient", "Inverti la sfumatura"), ("Style source", "Origine stile"), ("Open", "Apri"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index ed68d4907..568fda587 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -496,6 +496,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Fill (X)", "塗り (X)"), ("Stroke (X)", "線 (X)"), ("Apply None (/)", "カラーなしを適用 (/)"), + ("Default Fill and Stroke", "塗りと線をデフォルトに戻す"), + ("Swap Fill and Stroke", "塗りと線を入れ替え"), + ("Toggle Fill and Stroke", "塗りと線を切り替え"), ("Reverse the gradient", "グラデーションを反転"), ("Style source", "スタイルの同期元"), ("Open", "開く"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index ade595b15..c284ffcf5 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -525,6 +525,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Superscript", "Sobrescrito"), ("Tag the selection", "Marcar a seleção"), ("The structure is valid.", "A estrutura é válida."), + ("Toggle Fill and Stroke", "Alternar entre preenchimento e traçado"), ("Toggle double column", "Alternar coluna dupla"), ("Type:", "Tipo:"), ("Underline", "Sublinhado"), @@ -853,6 +854,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Crop to:", "Cortar para:"), ("Cross-Reference Format", "Formato de referência cruzada"), ("Cursor Key:", "Tecla do cursor:"), + ("Default Fill and Stroke", "Preenchimento e traçado padrão"), ("Default Fill and Stroke (D)", "Preenchimento e traçado padrão (D)"), ("Default View:", "Exibição padrão:"), ("Delete Column", "Excluir coluna"), @@ -1005,6 +1007,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Style Name Conflicts", "Conflitos de nomes de estilos"), ("Stylistic Sets", "Conjuntos estilísticos"), ("Subdivisions:", "Subdivisões:"), + ("Swap Fill and Stroke", "Alternar preenchimento e traçado"), ("Swap Fill and Stroke (Shift+X)", "Alternar preenchimento e traçado (Shift+X)"), ("Synchronize", "Sincronizar"), ("System default", "Padrão do sistema"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 057b658ab..1f9a8309f 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -379,6 +379,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Fill (X)", "Заливка (X)"), ("Stroke (X)", "Обведення (X)"), ("Apply None (/)", "Без кольору (/)"), + ("Default Fill and Stroke", "Заливка та обведення за замовчуванням"), + ("Swap Fill and Stroke", "Змінити заливку та обведення"), + ("Toggle Fill and Stroke", "Перемкнути заливку та обведення"), ("Reverse the gradient", "Змінити напрямок градієнта"), ("Style source", "Джерело стилів"), ("Open", "Відкрити"), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 0070a8fd2..bdfe2e0f5 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -309,6 +309,10 @@ pub struct UiState { /// Type › Tabs: the selected stop and where the panel sits. #[serde(skip)] pub tabs_panel: panels::tabs::PanelState, + /// Fill/Stroke proxy (Tools and Color panels): the stroke is in front, so Apply None and the + /// Color panel act on it (X toggles). + #[serde(skip)] + pub stroke_front: bool, } impl Default for UiState { @@ -390,6 +394,7 @@ impl Default for UiState { palette: None, flyout: None, tabs_panel: Default::default(), + stroke_front: false, } } } diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9a8013c1a..9c1c04ac7 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -137,6 +137,20 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ Some("J"), "{on?: bool} — with text frames selected, colours go to their text (on) or the frames (off); toggles without `on`", ), + ( + "color.defaultFillStroke", + "Default Fill and Stroke", + Some("D"), + "{} — the selection gets no fill and a black stroke (object.defaultFillStroke)", + ), + ( + "color.swapFillStroke", + "Swap Fill and Stroke", + Some("Shift+X"), + "{} — each selected object's fill and stroke colours (and tints) trade places (object.swapFillStroke)", + ), + ("color.toggleFillStroke", "Toggle Fill and Stroke", Some("X"), "{} → {strokeFront} — which of fill and stroke the proxy has in front"), + ("color.applyNone", "Apply None", Some("/"), "{} — [None] for the fill or stroke in front of the proxy"), ("view.frameEdges", "Show/Hide Frame Edges", Some("Cmd+H"), "{}"), ("view.rulers", "Show/Hide Rulers", Some("Cmd+R"), "{}"), ("view.guides", "Show/Hide Guides", Some("Cmd+;"), "{}"), @@ -1256,6 +1270,10 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option crate::toolbar::default_fill_stroke(app), + "color.swapFillStroke" => crate::toolbar::swap_fill_stroke(app), + "color.toggleFillStroke" => crate::toolbar::toggle_fill_stroke(app), + "color.applyNone" => crate::toolbar::apply_none(app), "view.frameEdges" => flag(&mut app.ui.frame_edges), "view.overprintPreview" => { app.canvas.shown = None; @@ -2541,6 +2559,7 @@ fn parse_shortcut(sc: &str) -> Option<(egui::Modifiers, egui::Key)> { "," => key = Some(egui::Key::Comma), "." => key = Some(egui::Key::Period), "\\" => key = Some(egui::Key::Backslash), + "/" => key = Some(egui::Key::Slash), "Delete" => key = Some(egui::Key::Delete), k => key = egui::Key::from_name(k), } diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index d5432035a..eb8f3ae83 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -234,9 +234,8 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { }; let text = super::colors_affect_text(app); let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return }; - let stroke_id = egui::Id::new("color_panel_stroke"); let mode_id = egui::Id::new("color_panel_mode"); - let mut stroke: bool = ui.data(|d| d.get_temp(stroke_id)).unwrap_or(false); + let mut stroke = app.ui.stroke_front; let default_mode = if doc.settings.intent == designcraft_doc::Intent::Print { ColorMode::Cmyk } else { ColorMode::Rgb }; let mut mode: ColorMode = ui.data(|d| d.get_temp(mode_id)).unwrap_or(default_mode); let target = if stroke { "stroke" } else { "fill" }; @@ -283,10 +282,8 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) { }); }); }); - ui.data_mut(|d| { - d.insert_temp(stroke_id, stroke); - d.insert_temp(mode_id, mode); - }); + app.ui.stroke_front = stroke; + ui.data_mut(|d| d.insert_temp(mode_id, mode)); let sw = doc.swatch(&name).cloned(); let Some(c) = doc.resolve_color(&name, 1.0) else { ui.label(crate::rtl::widget( diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 5c591e11a..ed2f3d707 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -197,8 +197,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) { let t = Tokens::get(ui.ctx()); let target = crate::panels::color_target(app); let doc = app.session.active().map(|d| d.doc.clone()); - let stroke_front_id = egui::Id::new("proxy_stroke_front"); - let stroke_front: bool = ui.data(|d| d.get_temp(stroke_front_id)).unwrap_or(false); + let stroke_front = app.ui.stroke_front; let text_mode = crate::panels::colors_affect_text(app) || app.ui.formatting_affects_text; let (fill_sw, stroke_sw) = match &target { Some((fill, _, stroke, _)) => (fill.clone(), stroke.clone()), @@ -282,7 +281,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) { if fresp.clicked() || sresp.clicked() { let front = if stroke_front { !fresp.clicked() } else { sresp.clicked() && !fill_r.contains(sresp.interact_pointer_pos().unwrap_or_default()) }; - ui.data_mut(|d| d.insert_temp(stroke_front_id, front)); + app.ui.stroke_front = front; } // A double-click opens the Color Picker for that box (as InDesign's), with the colour of what // colour edits apply to (see `colors_affect_text`). @@ -424,6 +423,28 @@ fn tool_button(ui: &mut egui::Ui, icon: &str, active: bool, tip: &str) -> egui:: }) } +/// Default Fill and Stroke (D): no fill, a black stroke. +pub(crate) fn default_fill_stroke(app: &mut DesignApp) -> Result { + app.run("object.defaultFillStroke", json!({})) +} + +/// Swap Fill and Stroke (Shift+X): each selected object's fill and stroke trade places. +pub(crate) fn swap_fill_stroke(app: &mut DesignApp) -> Result { + app.run("object.swapFillStroke", json!({})) +} + +/// Fill/Stroke toggle (X): brings the other of fill and stroke to the front of the proxy. +pub(crate) fn toggle_fill_stroke(app: &mut DesignApp) -> Result { + app.ui.stroke_front = !app.ui.stroke_front; + Ok(json!({"strokeFront": app.ui.stroke_front})) +} + +/// Apply None (/): to the fill or the stroke, whichever is in front of the proxy. +pub(crate) fn apply_none(app: &mut DesignApp) -> Result { + let target = if app.ui.stroke_front { "object.stroke" } else { "object.fill" }; + app.run(target, json!({"swatch": "[None]"})) +} + #[cfg(test)] mod tests { use egui::{pos2, vec2}; @@ -556,4 +577,42 @@ mod tests { wheel(&mut h, panel.center(), vec2(0.0, -2000.0)); assert_eq!((rect(&h, LAST_TOOL), rect(&h, FILL)), before); } + /// The Fill/Stroke proxy's keys run on the selection and stay out of text being typed. + #[test] + fn fill_stroke_proxy_keys() { + use egui::{Event, Key, Modifiers}; + use serde_json::json; + fn press(app: &mut crate::DesignApp, ctx: &egui::Context, key: Key, modifiers: Modifiers) { + let events = [true, false].map(|pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).to_vec(); + ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| crate::menus::shortcuts(app, ui.ctx())).textures_delta.clear(); + } + fn colors(app: &crate::DesignApp) -> (String, f32, String, f32) { + let st = app.session.active().unwrap(); + let it = st.doc.item(st.selection.items[0]).unwrap(); + (it.fill.swatch.clone(), it.fill.tint, it.stroke.swatch.clone(), it.stroke.tint) + } + let mut app = app(false); + let ctx = egui::Context::default(); + app.run("file.new", json!({})).unwrap(); + let f = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hello"})).unwrap(); + app.run("selection.set", json!({"ids": [f["id"]]})).unwrap(); + app.run("object.fill", json!({"swatch": "[Paper]", "tint": 0.5})).unwrap(); + app.run("object.stroke", json!({"swatch": "[Black]", "tint": 1.0})).unwrap(); + + press(&mut app, &ctx, Key::X, Modifiers::SHIFT); + assert_eq!(colors(&app), ("[Black]".into(), 1.0, "[Paper]".into(), 0.5), "Shift+X swaps, tints included"); + press(&mut app, &ctx, Key::D, Modifiers::NONE); + assert_eq!(colors(&app), ("[None]".into(), 1.0, "[Black]".into(), 1.0), "D: no fill, black stroke"); + press(&mut app, &ctx, Key::X, Modifiers::NONE); + assert!(app.ui.stroke_front, "X brings the stroke to the front"); + press(&mut app, &ctx, Key::Slash, Modifiers::NONE); + assert_eq!(colors(&app).2, "[None]", "/ applies None to the stroke in front"); + + // Typing in a story: the keys are text, not shortcuts. + app.session.execute("text.select", &json!({"story": f["story"], "anchor": 5})).unwrap(); + app.select_tool("type"); + assert!(app.session.wants_text()); + press(&mut app, &ctx, Key::X, Modifiers::NONE); + assert!(app.ui.stroke_front, "X while typing leaves the proxy alone"); + } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9bf2a0812..9c838827b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -268,7 +268,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Text engine: Knuth–Plass paragraph composer and single-line composer, dictionary hyphenation (public-domain Moby list + our own trained Liang patterns), justification with word/letter/glyph-scaling ranges, keep options (keep with next, keep lines together — at start/end of paragraph by default, as in InDesign — widows/orphans), balance ragged lines, optical margin alignment, hyphenation zone/limit, columns, threading across frames and spreads, tabs, bullets/numbering, rules, shading, baseline grid, first-baseline options, vertical justification, wrap exclusions, overset detection, caret/hit testing. - FID2: IDML underline/strikethrough Text Color resets survive style inheritance and roundtrip. Raster and PDF paint underlines behind glyphs, retain strikethrough above, and center rules on their offsets; synthetic overlap tests cover both backends. - Rendering: vello_cpu (SIMD, multithreaded) — pages, items, images with mip levels, gradients, composed text. -- UI (egui): application bar + menus, Control panel (object and text modes), Tools panel with flyouts, document tabs, rulers, pasteboard, guides (margins, columns, bleed, baseline grid), document grid (View ▸ Grids & Guides, ⌘', on the snap positions, behind or in front of items), frame edges, selection handles, text ports and threads, Properties / Pages / Layers dock, Swatches, Styles, Character, Paragraph, Stroke, Text Wrap panels, New Document and Preferences dialogs, Quick Apply (⌘Return), 4 brightness themes. +- UI (egui): application bar + menus, Control panel (object and text modes), Tools panel with flyouts and the Fill/Stroke proxy (D, X, Shift+X, / as `color.*` commands), document tabs, rulers, pasteboard, guides (margins, columns, bleed, baseline grid), document grid (View ▸ Grids & Guides, ⌘', on the snap positions, behind or in front of items), frame edges, selection handles, text ports and threads, Properties / Pages / Layers dock, Swatches, Styles, Character, Paragraph, Stroke, Text Wrap panels, New Document and Preferences dialogs, Quick Apply (⌘Return), 4 brightness themes. - Pen tool, Direct Selection anchor/handle editing, rotate from corners, Rotate/Scale/Shear tools, Eyedropper, snapping & smart guides, Align/Distribute, Find/Change (text + GREP), Story Editor, Paragraph Style Options, Effects panel, hidden characters, native macOS menu bar, measured InDesign 2026 look (Medium Dark): Properties sections per selection state, 21 pt spinner fields with arithmetic, Contextual Task Bar, inverse (black) text selection, Pages panel with drag-and-drop reorder / parent apply, Layers panel with per-object rows, expandable groups and drag-and-drop reorder of layers and objects (the selection square moves the selection to another layer). - Tools: Selection (click, marquee, move, Alt-duplicate, resize handles), Direct Selection, Type (draw frame, click caret, select, type), Rectangle/Ellipse/Polygon (+ frame variants), Line, Hand, Zoom. - PDF export (krilla): real selectable text with embedded font subsets, DeviceCMYK/RGB + spot Separations, bleed boxes, crop/bleed marks + page info, pages or spreads, PDF/A-2b (PDF/X-4 output intent pending) — `file.exportPdf`, File › Export PDF…, `designcraft-cli run --export out.pdf`. File › Export PDF… now opens an "Export PDF" options dialog (General / Compression / Marks and Bleeds / Advanced) and remembers the last-used settings across exports and sessions.