Text styles: the panel menu's Duplicate highlights the copy and Delete clears the highlight

Squashed from #4419 onto main.
This commit is contained in:
dotCooCoo
2026-10-29 10:12:16 +07:00
committed by bflatastic
parent 02c9a3959d
commit 5ed2fec4fd
3 changed files with 138 additions and 5 deletions
+28 -4
View File
@@ -34,6 +34,16 @@ impl Kind {
fn key(self) -> &'static str {
if self == Kind::Char { "char-style-sel" } else { "para-style-sel" }
}
/// The style row the panel scrolls into view the next time it draws.
fn scroll_key(self) -> &'static str {
if self == Kind::Char { "char-style-scroll" } else { "para-style-scroll" }
}
}
/// Highlights a style the panel just made and scrolls its row into view the next time it draws.
fn highlight_made(ctx: &egui::Context, kind: Kind, name: Option<String>) {
set_pstate(ctx, kind.scroll_key(), name.clone());
set_pstate(ctx, kind.key(), name);
}
/// A row dragged in the list: the style's kind and name. Dropped on Create New Style it is
@@ -104,6 +114,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
let cur_name = cur.as_ref().map(|(n, _)| n.clone().unwrap_or_else(|| normal.clone()));
let mut clicked: Option<(String, bool)> = None;
let mut sel: Option<String> = pstate(ui.ctx(), kind.key());
let scroll_to: Option<String> = pstate(ui.ctx(), kind.scroll_key());
widgets::list_box(ui, |ui| {
for (name, attrs, _) in &all {
let is_cur = cur_name.as_deref() == Some(name.as_str());
@@ -113,6 +124,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
if resp.drag_started() {
egui::DragAndDrop::set_payload(ui.ctx(), RowDrag { kind, name: name.clone() });
}
if scroll_to.as_deref() == Some(name.as_str()) {
ui.scroll_to_rect(r, None);
}
if is_cur || sel.as_deref() == Some(name.as_str()) {
ui.painter().rect_filled(r, 0.0, if is_cur { t.row_selected } else { t.hover });
} else if resp.hovered() {
@@ -128,6 +142,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
}
}
});
if scroll_to.is_some() {
set_pstate::<Option<String>>(ui.ctx(), kind.scroll_key(), None);
}
let has_text = cur.is_some();
if let Some((name, clear)) = clicked {
sel = Some(name.clone());
@@ -136,6 +153,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
app.run(&format!("{}.apply", kind.prefix()), p).ok();
}
}
// A highlighted style that is no longer listed (renamed in Style Options, deleted by a command
// or undone) is no longer highlighted.
if sel.as_ref().is_some_and(|s| !all.iter().any(|(n, ..)| n == s)) {
sel = None;
}
set_pstate(ui.ctx(), kind.key(), sel.clone());
let deletable = sel.as_ref().is_some_and(|s| *s != normal);
widgets::bottom_bar(ui, |ui| {
@@ -150,12 +172,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
);
if let Some(name) = row_dropped(ui, &new, kind, |_| true) {
if let Ok(r) = app.run(&format!("{}.duplicate", kind.prefix()), json!({ "name": name })) {
set_pstate(ui.ctx(), kind.key(), r["name"].as_str().map(str::to_string));
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
}
} else if new.clicked() {
end_typing(app);
if let Ok(r) = app.run(&format!("{}.new", kind.prefix()), target(app, kind, json!({}))) {
set_pstate(ui.ctx(), kind.key(), r["name"].as_str().map(str::to_string));
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
}
}
let delete = widgets::icon_button_enabled(
@@ -192,13 +214,15 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
}
if menu_item(ui, &crate::i18n::fmt(tl!("Duplicate {kind} Style"), &[("kind", kind.what_label())]), sel.is_some(), false)
&& let Some(n) = &sel
&& let Ok(r) = app.run(&format!("{p}.duplicate"), json!({ "name": n }))
{
app.run(&format!("{p}.duplicate"), json!({ "name": n })).ok();
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
}
if menu_item(ui, &crate::i18n::fmt(tl!("Delete {kind} Style"), &[("kind", kind.what_label())]), user, false)
&& let Some(n) = &sel
&& app.run(&format!("{p}.delete"), json!({ "name": n })).is_ok()
{
app.run(&format!("{p}.delete"), json!({ "name": n })).ok();
set_pstate::<Option<String>>(ui.ctx(), kind.key(), None);
}
ui.separator();
if menu_item(ui, tl!("Clear Overrides"), sel.is_some() && has_text, false)
+109 -1
View File
@@ -1,7 +1,7 @@
use super::*;
use vectorcraft_doc::{NodeId, NodeKind};
use super::super::tests_appearance::{click, frame_events, text_rect};
use super::super::tests_appearance::{click, frame_events, frame_raw, text_rect};
#[test]
fn panels_draw_and_apply_headless() {
@@ -240,6 +240,114 @@ fn click_style_ui(ctx: &egui::Context, app: &mut VectorcraftApp, id: NodeId, ran
assert_eq!(text_editing(app), expected, "Type selection after {label}");
}
/// Click `label` where `draw` paints it (a style row of the panel, or a panel menu item with
/// its three-space prefix). → how many commands the click ran.
fn click_label(ctx: &egui::Context, app: &mut VectorcraftApp, label: &str, mut draw: impl FnMut(&mut VectorcraftApp, &mut egui::Ui)) -> u64 {
frame_events(ctx, app, vec![], &mut draw);
let texts = frame_events(ctx, app, vec![], &mut draw);
let pos = text_rect(&texts, label).center();
let before = app.run_count;
click(ctx, app, pos, &mut draw);
app.run_count.wrapping_sub(before)
}
/// The panel menu's Duplicate highlights the copy and its Delete clears the highlight, as the
/// panel's New and Delete buttons do; after a menu delete, Duplicate runs nothing.
#[test]
fn panel_menu_duplicate_and_delete_update_the_highlighted_style() {
for kind in [Kind::Char, Kind::Para] {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Body"})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let what = kind.what();
let has = |app: &mut VectorcraftApp, name: &str| styles(app, kind).iter().any(|s| s.0 == name);
let highlighted = |ctx: &egui::Context| pstate::<Option<String>>(ctx, kind.key());
click_label(&ctx, &mut app, "Body", |app, ui| show(app, ui, kind));
assert_eq!(highlighted(&ctx).as_deref(), Some("Body"), "{what}: clicking the row highlights it");
let duplicate = format!(" Duplicate {what} Style");
assert_eq!(click_label(&ctx, &mut app, &duplicate, |app, ui| menu(app, ui, kind)), 1, "{what}: Duplicate runs");
assert!(has(&mut app, "Body copy"), "{what}: the copy exists");
assert_eq!(highlighted(&ctx).as_deref(), Some("Body copy"), "{what}: the menu's Duplicate highlights the copy");
let delete = format!(" Delete {what} Style");
assert_eq!(click_label(&ctx, &mut app, &delete, |app, ui| menu(app, ui, kind)), 1, "{what}: Delete runs");
assert!(!has(&mut app, "Body copy"), "{what}: the copy is deleted");
assert_eq!(highlighted(&ctx), None, "{what}: the menu's Delete clears the highlight");
assert_eq!(click_label(&ctx, &mut app, &duplicate, |app, ui| menu(app, ui, kind)), 0, "{what}: Duplicate is disabled after the delete");
assert!(has(&mut app, "Body"), "{what}: the original stays");
}
}
/// In a panel scrolled to the top of a long list, the menu's Duplicate scrolls the copy's row into
/// view, and so does the New button's `highlight_made` for the new style.
#[test]
fn a_style_made_from_the_panel_is_scrolled_into_view() {
for kind in [Kind::Char, Kind::Para] {
let what = kind.what();
for from_menu in [true, false] {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Body"})).unwrap();
for i in 1..=12 {
app.run(&format!("{}.new", kind.prefix()), json!({"name": format!("Item {i:02}")})).unwrap();
}
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let panel = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
egui::ScrollArea::vertical().id_salt("styles").max_height(80.0).show(ui, |ui| show(app, ui, kind));
};
let before = frame_events(&ctx, &mut app, vec![], panel);
let body = text_rect(&before, "Body");
let made = if from_menu {
set_pstate(&ctx, kind.key(), Some("Body".to_string()));
let duplicate = format!(" Duplicate {what} Style");
assert_eq!(click_label(&ctx, &mut app, &duplicate, |app, ui| menu(app, ui, kind)), 1, "{what}: Duplicate runs");
"Body copy".to_string()
} else {
// The New button's path: the style it made, highlighted with `highlight_made`.
let name = app.run(&format!("{}.new", kind.prefix()), json!({})).unwrap()["name"].as_str().unwrap_or_default().to_string();
highlight_made(&ctx, kind, Some(name.clone()));
name
};
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()).as_deref(), Some(made.as_str()), "{what}: the new style is highlighted");
let mut after = vec![];
for _ in 0..3 {
let time = ctx.input(|i| i.time) + 1.0;
after = frame_raw(&ctx, &mut app, egui::RawInput { time: Some(time), ..Default::default() }, panel);
}
let row = text_rect(&after, &made);
let top = body.top() - 30.0;
assert!(row.top() >= top && row.bottom() <= top + 80.0 + 30.0, "{what}, menu {from_menu}: {made} at {row:?}, panel from {top}");
}
}
}
/// A highlighted style renamed in Style Options (`<kind>.rename`) or deleted by a command is no
/// longer highlighted once the panel draws, so the panel menu's Duplicate runs nothing.
#[test]
fn a_highlighted_style_that_is_no_longer_listed_is_not_highlighted() {
for kind in [Kind::Char, Kind::Para] {
for (change, p) in [("rename", json!({"name": "Body", "to": "Heading"})), ("delete", json!({"name": "Body"}))] {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Body"})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let what = kind.what();
click_label(&ctx, &mut app, "Body", |app, ui| show(app, ui, kind));
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()).as_deref(), Some("Body"), "{what}: clicking the row highlights it");
app.run(&format!("{}.{change}", kind.prefix()), p).unwrap();
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, kind));
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()), None, "{what}, {change}: the old name is not highlighted");
let duplicate = format!(" Duplicate {what} Style");
assert_eq!(click_label(&ctx, &mut app, &duplicate, |app, ui| menu(app, ui, kind)), 0, "{what}, {change}: Duplicate is disabled");
}
}
}
fn text_runs(app: &VectorcraftApp, id: NodeId) -> Vec<(String, f64, Option<String>)> {
let st = app.session.doc().unwrap();
let NodeKind::Text(text) = &st.doc.node(id).unwrap().kind else { panic!("expected text") };
+1
View File
@@ -491,6 +491,7 @@ since 2026-10-01.
- **Text styles redefined to the defaults:** New and Redefine Character Style and Paragraph Style capture every attribute of the selected text, including those at their defaults (Auto kerning, Roman baseline alignment, the default Hyphenation options, the Every-line Composer, no tab stops), and `charStyle.setAttrs` and `paraStyle.setAttrs` keep an attribute set to its default value. After a redefinition, text whose value still equals the old definition's takes the new value, a default included; local overrides and attributes the new definition leaves out stay. The style panels show "+" only where the text differs from its style, including type with Auto alignment. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Text styles dragged onto the panel's buttons:** a style dragged from the Character Styles or Paragraph Styles panel onto Create New Style is copied (`charStyle.duplicate`, `paraStyle.duplicate`, which name the copy `<name> copy`) and the copy is highlighted; dragged onto Delete it is deleted (`charStyle.delete`, `paraStyle.delete`); the Normal style is not deleted. Each drop is one undo step. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Auto Leading outside the layout:** a paragraph's Justification… › Auto Leading (<https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/adjust-word-and-letterspacing-in-justified-text.html>) also sets the Leading the Character panel shows for Auto type (blank where the selected paragraphs differ), the leading the Alt+↑/↓ nudge (`type.step`) steps from, each paragraph's own, and the lines Fill with Placeholder Text counts in area type. Evidence: `ParaStyle::effective_leading` (`crates/doc/src/text.rs` test `paragraph_effective_leading_follows_auto_leading`), `tests_prefs::auto_leading_steps_and_fills_from_the_paragraph_auto_leading`, `panels::character` test `leading_field_shows_the_paragraph_auto_leading`.
- **The text style panel menus' Duplicate and Delete:** in the Character Styles and Paragraph Styles panel menus, Duplicate highlights the copy, as Create New Style highlights the new style, and Delete leaves no style highlighted, as the Delete button does, so Duplicate, Clear Overrides, Redefine and Style Options are disabled until another style is clicked. A highlighted style that is renamed in Style Options or deleted by a command is no longer highlighted either. The tests `panels::text_styles::tests::panel_menu_duplicate_and_delete_update_the_highlighted_style` and `a_highlighted_style_that_is_no_longer_listed_is_not_highlighted` cover this. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Names of new styles and symbols:** `charStyle.new` and `paraStyle.new` trim the white space around the name they are given, as `charStyle.rename` and `paraStyle.rename` do, and refuse a name that is then empty; Duplicate names its copy from the trimmed name. `symbol.new` and `symbol.duplicate` trim the name as Symbol Options does, and `library.use` adds a library's text style under its trimmed name. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
- **Width Point Edit:** Shift-double-click enables Adjust Adjoining Width Points; plain double-click and direct opening retain the default. Tool dispatch and headless dialog tests cover neighboring widths, Undo/Redo and Cancel. Public reference: [Variable-width strokes](https://helpx.adobe.com/illustrator/using/stroke-object.html).
Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a feature lands.