diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index f687b0f2c..a27dfa3ec 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -13,7 +13,8 @@ //! (dropped on its row) or onto the object; Alt copies instead. Dropped on Duplicate Selected Item //! or Delete Selected Item, a fill/stroke row (with the other selected rows, when it is one of //! them) or an effect row is copied or removed. Cmd-click and Shift-click select several -//! fills/strokes for Duplicate and Delete. +//! fills/strokes for Duplicate and Delete. Delete on a selected Opacity row sets it back to +//! Default (100%, Normal). use std::sync::OnceLock; @@ -59,11 +60,16 @@ impl Sel { Sel::None | Sel::Effect(_) | Sel::Contents | Sel::Opacity => None, } } - /// Is a fill/stroke or effect row selected (what Duplicate and Delete act on)? An Opacity row - /// is not an item: it can't be copied or removed. + /// Is a fill/stroke or effect row selected (what Duplicate acts on)? An Opacity row is not an + /// item: it can't be copied. fn editable(self) -> bool { matches!(self, Sel::Item(_) | Sel::Effect(_) | Sel::ItemEffect(..)) } + /// Does Delete act on the selected row? A fill/stroke or effect row is removed; deleting an + /// Opacity row sets it back to Default (100%, Normal) (#5742). + fn deletable(self) -> bool { + self.editable() || matches!(self, Sel::Opacity | Sel::ItemOpacity(_)) + } } /// The selected row of `node` (the first selected object): a selected effect row while that @@ -490,7 +496,7 @@ fn delete_key(app: &mut VectorcraftApp, ui: &mut Ui) { let node = first_subject(app); let sel = current_sel(app, &ctx, node.as_ref()); if keys - && sel.editable() + && sel.deletable() && ctx.memory(|m| m.focused().is_none()) && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Delete) || i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace)) { @@ -1088,7 +1094,8 @@ fn opacity_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, opaci let link = name_link(ui, pos2(lx, r.center().y), ("ap-op-link", item), tl!("Opacity:")).clicked(); if link || resp.clicked() { select_row(app, ui.ctx(), this); - // The panel has the keyboard (Delete then removes nothing, rather than the object). + // The panel has the keyboard (Delete then sets the row back to Default, rather than + // deleting the object). super::set_row_keys(ui.ctx(), ROW_KEYS, true); } if link { @@ -1415,7 +1422,8 @@ fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, node: Option<&Node>, sel: Sel) if duplicate.clicked() { duplicate_selected(app, ui, node, sel); } - let delete = widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Selected Item"), false, can, 24.0); + let can_delete = can || (has && sel.deletable()); + let delete = widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Selected Item"), false, can_delete, 24.0); if delete.clicked() { delete_selected(app, ui, node, sel); } @@ -1558,8 +1566,20 @@ fn highlight_copies(app: &mut VectorcraftApp, ctx: &egui::Context, n: &Node, sel set_pstate(ctx, "ap-multi", Highlight { owner: Some(n.id), rows, order, anchor, active }); } -/// Remove the highlighted fills/strokes or effects; a removed item effect leaves its fill/stroke selected. +/// Remove the highlighted fills/strokes or effects; a removed item effect leaves its fill/stroke +/// selected. A selected Opacity row is set back to Default (100%, Normal) and stays selected. fn delete_selected(app: &mut VectorcraftApp, ui: &Ui, node: Option<&Node>, sel: Sel) { + match sel { + Sel::Opacity => { + app.run("transparency.set", json!({"item": null, "opacity": 100, "blend": "Normal"})).ok(); + return; + } + Sel::ItemOpacity(i) => { + app.run("transparency.set", aligned(app, json!({"item": i, "opacity": 100, "blend": "Normal"}))).ok(); + return; + } + _ => {} + } let ok = match selected_effects(ui.ctx(), node, sel) { Some((item, ks)) => app.run("effect.remove", aligned(app, json!({"item": item, "indices": ks}))).is_ok(), None if matches!(sel, Sel::Item(_)) => { @@ -1616,7 +1636,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, tl!("Duplicate Item"), has && sel.editable(), false) { duplicate_selected(app, ui, node.as_ref(), sel); } - if menu_item(ui, tl!("Remove Item"), has && sel.editable(), false) { + if menu_item(ui, tl!("Remove Item"), has && sel.deletable(), false) { delete_selected(app, ui, node.as_ref(), sel); } if menu_item(ui, tl!("Clear Appearance"), has, false) { diff --git a/crates/ui-egui/src/panels/appearance/tests.rs b/crates/ui-egui/src/panels/appearance/tests.rs index 7d9cdd329..82042a9a3 100644 --- a/crates/ui-egui/src/panels/appearance/tests.rs +++ b/crates/ui-egui/src/panels/appearance/tests.rs @@ -85,6 +85,47 @@ fn removing_items_keeps_a_surviving_item_opacity_row_selected() { assert_eq!(app.session.appearance_item(), None); } +/// #5742: Delete Selected Item on an Opacity row sets it back to Default (100%, Normal): the +/// object's own, or a fill's, leaving the other untouched. Duplicate stays off for it. +#[test] +fn deleting_an_opacity_row_sets_it_back_to_default() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({ "ids": [id] })).unwrap(); + app.run("transparency.set", json!({"item": null, "opacity": 50, "blend": "Multiply"})).unwrap(); + app.run("transparency.set", json!({"item": 0, "opacity": 40, "blend": "Screen"})).unwrap(); + let owner = Some(NodeId(id)); + let ctx = egui::Context::default(); + let delete = |app: &mut VectorcraftApp| { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + let node = first_subject(app); + let sel = current_sel(app, ui.ctx(), node.as_ref()); + assert!(sel.deletable() && !sel.editable(), "{sel:?}"); + delete_selected(app, ui, node.as_ref(), sel); + }); + out.textures_delta.clear(); + }; + let node = |app: &VectorcraftApp| first_subject(app).unwrap(); + // The fill's Opacity row: only the fill goes back to Default. + app.run("appearance.setActiveItem", json!({ "index": 0 })).unwrap(); + set_pstate(&ctx, "ap-sel", (owner, Sel::ItemOpacity(0))); + delete(&mut app); + let n = node(&app); + assert_eq!((n.appearance.items[0].opacity(), n.appearance.items[0].blend()), (1.0, BlendMode::Normal)); + assert_eq!((n.opacity, n.blend), (0.5, BlendMode::Multiply)); + assert_eq!(pstate::<(Option, Sel)>(&ctx, "ap-sel").1, Sel::ItemOpacity(0)); + // The object's Opacity row. + app.run("appearance.setActiveItem", json!({ "index": null })).unwrap(); + set_pstate(&ctx, "ap-sel", (owner, Sel::Opacity)); + delete(&mut app); + let n = node(&app); + assert_eq!(opacity_text(n.opacity, n.blend), "Default"); + // One step, so Undo brings the object's opacity back. + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(node(&app).opacity, 0.5); +} + #[test] fn opacity_labels() { assert_eq!(opacity_text(1.0, BlendMode::Normal), "Default"); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index e7cefcdb1..1a7f0174d 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Pencil and Paintbrush: a new stroke with Keep selected off leaves nothing selected, #5752; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Appearance panel: Delete Selected Item on an Opacity row sets it back to Default, #5742; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -38,6 +38,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | Appearance panel: Delete Selected Item (and Delete/Backspace, and Remove Item in the panel menu) on a selected Opacity row, the object's or a fill's or stroke's, sets it back to Default (100%, Normal) as one undo step instead of being dimmed (#5742); scores and estimates unchanged | | 2026-11-05 | minor | Selection Tools add-on: Select Linked, Embedded, Missing and Modified select the images and placed documents with that link status (Object › Plug-ins › Selection Tools and the Links panel menu; Shift adds), and Select by Link Status… lists them by name, status and format to pick from, optionally zooming to them; what is hidden, locked, on a template layer or outside the isolated group is left out and the status bar says how many (#5526, a VectorCraft addition); scores and estimates unchanged (estimated). | | 2026-11-05 | minor | Pencil and Paintbrush: a new stroke with Keep selected off leaves nothing selected, the earlier selection included (#5752); scores and estimates unchanged | | 2026-11-05 | minor | Layers panel: a layer whose Layer Options › Print is off shows its name italic, as the reference application shows non-printing layers, and keeps it while the row is dragged (#5739); scores and estimates unchanged |